chore(): Reorganize modules (#7210)
**What** Move all modules to the modules directory
This commit is contained in:
committed by
GitHub
parent
7a351eef09
commit
4eae25e1ef
6
packages/modules/product/.gitignore
vendored
Normal file
6
packages/modules/product/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/dist
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
.env
|
||||
*.sql
|
||||
297
packages/modules/product/CHANGELOG.md
Normal file
297
packages/modules/product/CHANGELOG.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# @medusajs/product
|
||||
|
||||
## 0.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6906](https://github.com/medusajs/medusa/pull/6906) [`3dee91426e`](https://github.com/medusajs/medusa/commit/3dee91426e1c98f43caff4b4515b2f1b7359d753) Thanks [@sradevski](https://github.com/sradevski)! - Correctly define all product module models and SQL migration script
|
||||
|
||||
- [#6880](https://github.com/medusajs/medusa/pull/6880) [`1a48fe0282`](https://github.com/medusajs/medusa/commit/1a48fe0282a8bc1f8548a4736255e457d173da09) Thanks [@sradevski](https://github.com/sradevski)! - Add v2 product type endpoints and adjust the product module
|
||||
|
||||
- [#6944](https://github.com/medusajs/medusa/pull/6944) [`09a2220569`](https://github.com/medusajs/medusa/commit/09a22205693da62fbf8fd450535d5024cb9c01d1) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Add parents to product categories
|
||||
|
||||
- Updated dependencies [[`0c0b425de7`](https://github.com/medusajs/medusa/commit/0c0b425de7b154b80b712ab17b16215cf62d1e83), [`8d356217bd`](https://github.com/medusajs/medusa/commit/8d356217bd31c97a196e861ee243822a4d924df7), [`1eeb1e9de3`](https://github.com/medusajs/medusa/commit/1eeb1e9de3e0b571735437b00968ee96e4aabad5), [`20e8df914e`](https://github.com/medusajs/medusa/commit/20e8df914ec5fdf8d562d4fa84f72c58c7056195), [`27f4f0d724`](https://github.com/medusajs/medusa/commit/27f4f0d7243367c2dfc6012bf1f6b7400a77ec7b), [`e0b02a1012`](https://github.com/medusajs/medusa/commit/e0b02a1012981c29830d7779f59ebe805bbfd137), [`e944a627f0`](https://github.com/medusajs/medusa/commit/e944a627f074fb39a56f4bc7b3d6d315736ebf7c), [`1a48fe0282`](https://github.com/medusajs/medusa/commit/1a48fe0282a8bc1f8548a4736255e457d173da09), [`86f499de2f`](https://github.com/medusajs/medusa/commit/86f499de2f31356ab36ad5e93f27345443b3e5f6), [`09a2220569`](https://github.com/medusajs/medusa/commit/09a22205693da62fbf8fd450535d5024cb9c01d1), [`78f603e4f1`](https://github.com/medusajs/medusa/commit/78f603e4f18c9d16f4b58a2189c959026453d8b2), [`cc557c8752`](https://github.com/medusajs/medusa/commit/cc557c8752fd0554f5a1b58522d9a88dc43a8509), [`dd35a4dbff`](https://github.com/medusajs/medusa/commit/dd35a4dbff10c86ea3c5f7f817c18b6e60d599e3), [`58c68f6715`](https://github.com/medusajs/medusa/commit/58c68f67156e993255fbc25d91db15ae23bc95c0), [`1bcb13f892`](https://github.com/medusajs/medusa/commit/1bcb13f892bc61db21b3fc6bdbce85f747aeec4c), [`82a176e30e`](https://github.com/medusajs/medusa/commit/82a176e30e47a7d11caaf31c3023bd8db588b465), [`11517f0faf`](https://github.com/medusajs/medusa/commit/11517f0fafdf00af256240448b58d149d8b6f600), [`62b9dcc6c1`](https://github.com/medusajs/medusa/commit/62b9dcc6c1ce46aadb7944215006c12da3c9f619), [`5d9aea053c`](https://github.com/medusajs/medusa/commit/5d9aea053ce6e04f242f86fb9053c13dec515d5b), [`e26cda4b6a`](https://github.com/medusajs/medusa/commit/e26cda4b6afb7fb25f0b0a7a7ce20b7f914d35db), [`bc06ad2db4`](https://github.com/medusajs/medusa/commit/bc06ad2db48c999023ab823fefc1375196976e9b), [`18f3aacee6`](https://github.com/medusajs/medusa/commit/18f3aacee6752854d377faa806f4cc67bc71456b), [`232322d035`](https://github.com/medusajs/medusa/commit/232322d03515f81e56867ff8c765b8409399ee68), [`38c971f111`](https://github.com/medusajs/medusa/commit/38c971f111af69f176e7e9892eb59f5bae831fa7), [`45c49e89f2`](https://github.com/medusajs/medusa/commit/45c49e89f28123ef622fc1c07253bae94fd74875), [`528ef4ca90`](https://github.com/medusajs/medusa/commit/528ef4ca90bb2cf6173dccc9fd6a9f9932ff9b76), [`65794f4bb5`](https://github.com/medusajs/medusa/commit/65794f4bb56e4fd3f0ccb7656a948f856f05324e), [`93ef94cad3`](https://github.com/medusajs/medusa/commit/93ef94cad3ddc5b6973b4e48e422b0aa0e6ddbbe), [`4cf71af07d`](https://github.com/medusajs/medusa/commit/4cf71af07d1807c83df3889c1774f82cbd1b9a6f), [`4b57c5d286`](https://github.com/medusajs/medusa/commit/4b57c5d286f9dc6e2098c67e9fecb0d93175b5a1), [`c78915c7c5`](https://github.com/medusajs/medusa/commit/c78915c7c5e91a99c1b1bae932656c8d86b17daf), [`18f3aacee6`](https://github.com/medusajs/medusa/commit/18f3aacee6752854d377faa806f4cc67bc71456b), [`667c8609cc`](https://github.com/medusajs/medusa/commit/667c8609ccf3850f5df8cf784723a95bd0d6d2a6), [`f175cac4af`](https://github.com/medusajs/medusa/commit/f175cac4af63b71066a8398ecf9beaa6f28b20cc), [`0a9b9b073d`](https://github.com/medusajs/medusa/commit/0a9b9b073dd2d3f4aa5e5cb1c16e2221a7200e0d), [`a6562d2a41`](https://github.com/medusajs/medusa/commit/a6562d2a41453cbe7aa43be352c4924e3e4c79d5), [`00e6b21bb5`](https://github.com/medusajs/medusa/commit/00e6b21bb50dbc886bc37ad052a1c40ce865294e), [`8fd1488938`](https://github.com/medusajs/medusa/commit/8fd148893850eb66c5eae00c4ca9391a80ea2eb9), [`1c6ba4468e`](https://github.com/medusajs/medusa/commit/1c6ba4468eab1440931c88929affd5b4c593f377)]:
|
||||
- @medusajs/types@1.11.16
|
||||
- @medusajs/modules-sdk@1.12.11
|
||||
- @medusajs/utils@1.11.9
|
||||
|
||||
## 0.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6748](https://github.com/medusajs/medusa/pull/6748) [`05e857d256`](https://github.com/medusajs/medusa/commit/05e857d25657b5576a891c9b48c19c1759c70701) Thanks [@sradevski](https://github.com/sradevski)! - Updated the signature of the product module variant and options endpoints to follow our conventions
|
||||
|
||||
- Updated dependencies [[`06f22bb48a`](https://github.com/medusajs/medusa/commit/06f22bb48ad1fe73577657b8c5db055312f16a0d), [`deab12e27e`](https://github.com/medusajs/medusa/commit/deab12e27e8249e26d24d7bc904c18195679ff24), [`56481e683d`](https://github.com/medusajs/medusa/commit/56481e683d33ff98f0d4c4e144873bb23f993c9c), [`9073d7aba3`](https://github.com/medusajs/medusa/commit/9073d7aba3419e4dc0a206473291a46ebd79b8c1), [`4974f5e455`](https://github.com/medusajs/medusa/commit/4974f5e4557bd64a328a881ec02b91e15485bd23), [`05e857d256`](https://github.com/medusajs/medusa/commit/05e857d25657b5576a891c9b48c19c1759c70701), [`1ef9c78cea`](https://github.com/medusajs/medusa/commit/1ef9c78cea080c3b7c136f909c6cddec9d8f0c62)]:
|
||||
- @medusajs/modules-sdk@1.12.10
|
||||
- @medusajs/types@1.11.15
|
||||
- @medusajs/utils@1.11.8
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6358](https://github.com/medusajs/medusa/pull/6358) [`b91a1ca5b8`](https://github.com/medusajs/medusa/commit/b91a1ca5b804e66cf5bfc51403f4367a935e6349) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: cleanup inspection
|
||||
|
||||
- [#6408](https://github.com/medusajs/medusa/pull/6408) [`1d91b7429b`](https://github.com/medusajs/medusa/commit/1d91b7429beebd6f09d5027f7f7e1fe74ce3a8ff) Thanks [@adrien2p](https://github.com/adrien2p)! - feat(fulfillment): implementation part 2
|
||||
|
||||
- [#6700](https://github.com/medusajs/medusa/pull/6700) [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Version all modules to allow for initial testing
|
||||
|
||||
- Updated dependencies [[`1fd0457c15`](https://github.com/medusajs/medusa/commit/1fd0457c153b2ef7657c052878d8e5364e1b324a), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`d4b921f3db`](https://github.com/medusajs/medusa/commit/d4b921f3dbe0a38f1565a8de759996c70798d58e), [`ac86362e81`](https://github.com/medusajs/medusa/commit/ac86362e81d8523cb8e3dfad026fc94658513018), [`e4acde1aa2`](https://github.com/medusajs/medusa/commit/e4acde1aa2eb57f07e6692fe8b61f728948b9a96), [`1a661adf3e`](https://github.com/medusajs/medusa/commit/1a661adf3ef4991aa6e237dd894b6a5c47cd4aca), [`56cbf88115`](https://github.com/medusajs/medusa/commit/56cbf88115994adea7037c3f2814f0c96af3cfc0), [`36a61658f9`](https://github.com/medusajs/medusa/commit/36a61658f969a7b19c84a1e621ad1464927cafb1), [`04a532e5ef`](https://github.com/medusajs/medusa/commit/04a532e5efabbf75b1e4155520b1da175b686ffc), [`c319edb8e0`](https://github.com/medusajs/medusa/commit/c319edb8e0ecd13d086652147667916e5abab2d8), [`0b9fcb6324`](https://github.com/medusajs/medusa/commit/0b9fcb6324eee9f2556c7e6317775fae93b12a47), [`586df9da25`](https://github.com/medusajs/medusa/commit/586df9da250e492442769f5bac2f8b3de1d46f05), [`b3d826497b`](https://github.com/medusajs/medusa/commit/b3d826497b3dae5e1b26b7924706c24fd5e87ca5), [`a86c87fe14`](https://github.com/medusajs/medusa/commit/a86c87fe1442afce9285e39255914e01012b4449), [`640eccd5dd`](https://github.com/medusajs/medusa/commit/640eccd5ddbb163e0f987ce6c772f1129c2e2632), [`8ea37d03c9`](https://github.com/medusajs/medusa/commit/8ea37d03c914a5004a3e42770668b2d1f7f8f564), [`339a946f38`](https://github.com/medusajs/medusa/commit/339a946f389033c21e05338f9dbf07d88e140533), [`ac829fc67f`](https://github.com/medusajs/medusa/commit/ac829fc67f7495b08f28e55923c59f0fd6320311), [`d9d5afc3cf`](https://github.com/medusajs/medusa/commit/d9d5afc3cfc29221d0e65bff7b78474a8fb8f31f), [`c3c4f49fc2`](https://github.com/medusajs/medusa/commit/c3c4f49fc2126f950e69e291ca939ca88a15afd3), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`0d46abf0ff`](https://github.com/medusajs/medusa/commit/0d46abf0ffa4c5e03bf7d2a9cdf1db828a76bea8), [`fafde4f54d`](https://github.com/medusajs/medusa/commit/fafde4f54d3ef75a7d382e6cbf94e38b3deae99b), [`8dad2b51a2`](https://github.com/medusajs/medusa/commit/8dad2b51a26c4c3c14a6c95f70424c8bef2ad63e), [`0c705d7bd4`](https://github.com/medusajs/medusa/commit/0c705d7bd41a768c48017ae95b3c8414d96c6acb), [`a6d7070dd6`](https://github.com/medusajs/medusa/commit/a6d7070dd669c21ea19d70434d42c2f8167dc309), [`1d91b7429b`](https://github.com/medusajs/medusa/commit/1d91b7429beebd6f09d5027f7f7e1fe74ce3a8ff), [`168f02f138`](https://github.com/medusajs/medusa/commit/168f02f138ad101e1013f2c8c3f8dc19de12accf), [`1ed5f918c3`](https://github.com/medusajs/medusa/commit/1ed5f918c31794a70aca4a4e4cd83cf456593baa), [`c20eb15cd9`](https://github.com/medusajs/medusa/commit/c20eb15cd9b1bd90c8d01f68eca6f0f181cd902d), [`e5945479e0`](https://github.com/medusajs/medusa/commit/e5945479e091d9560ae3e7240306a31031ef4584), [`f5c2256286`](https://github.com/medusajs/medusa/commit/f5c22562867f412040f8bc6c55ab5de3a3735e62), [`000eb61e33`](https://github.com/medusajs/medusa/commit/000eb61e33e0302db95ee6ad1656ea9b430ed471), [`d550be3685`](https://github.com/medusajs/medusa/commit/d550be3685423218d47a20c57a5e06758f4a961a), [`62a7bcc30c`](https://github.com/medusajs/medusa/commit/62a7bcc30cbc7b234b2b51d7858439951a84edeb), [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8), [`6500f18b9b`](https://github.com/medusajs/medusa/commit/6500f18b9b80c5c9c473489e7e740d55dca74303), [`ce39b9b66e`](https://github.com/medusajs/medusa/commit/ce39b9b66e8c277ec0691ea6d0a950003be09cc1), [`a6a4b3f01a`](https://github.com/medusajs/medusa/commit/a6a4b3f01a6d2bd97b1580c59134279a1b033a5d), [`4d51f095b3`](https://github.com/medusajs/medusa/commit/4d51f095b3f98f468cefb760512563f7b77bb9cf), [`4625bd1241`](https://github.com/medusajs/medusa/commit/4625bd12416275b09c22cde4a09cb0f68df5d7c1), [`56b0b45304`](https://github.com/medusajs/medusa/commit/56b0b4530401a6ec5aa155874d371e45bb388fe2), [`cc1b66842c`](https://github.com/medusajs/medusa/commit/cc1b66842cbb37c6eab84e2d8b74844c214f38d7), [`24fb102a56`](https://github.com/medusajs/medusa/commit/24fb102a564b1253d1f8b039bb1e435cc5312fbb), [`e85463b2a7`](https://github.com/medusajs/medusa/commit/e85463b2a717751de2e21c39a4c745449b31affe)]:
|
||||
- @medusajs/types@1.11.14
|
||||
- @medusajs/utils@1.11.7
|
||||
- @medusajs/modules-sdk@1.12.9
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6263](https://github.com/medusajs/medusa/pull/6263) [`45134e4d1`](https://github.com/medusajs/medusa/commit/45134e4d11cfcdc08dbd10aae687bfbe9e848ab9) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Workflows passing MedusaContext as argument
|
||||
|
||||
- [#6218](https://github.com/medusajs/medusa/pull/6218) [`884428a1b`](https://github.com/medusajs/medusa/commit/884428a1b573e499d7659aefed639bf797147428) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Feat: Event Aggregator
|
||||
|
||||
- Updated dependencies [[`12054f5c0`](https://github.com/medusajs/medusa/commit/12054f5c01915899223ddc6da734151b31fbb23b), [`3db2f95e65`](https://github.com/medusajs/medusa/commit/3db2f95e65909f4fff432990b48be74509052e83), [`96ba49329`](https://github.com/medusajs/medusa/commit/96ba49329b6b05922c90f0c55f16455cb40aa5ca), [`45134e4d1`](https://github.com/medusajs/medusa/commit/45134e4d11cfcdc08dbd10aae687bfbe9e848ab9), [`82c728bec`](https://github.com/medusajs/medusa/commit/82c728bec7232a245a67dca0b01b28572ebea75d), [`e749dd653`](https://github.com/medusajs/medusa/commit/e749dd653c755bfc3632b134d32c15ceaee0a852), [`884428a1b`](https://github.com/medusajs/medusa/commit/884428a1b573e499d7659aefed639bf797147428), [`d1c18a309`](https://github.com/medusajs/medusa/commit/d1c18a3090d71c68a98343fdbb53516f416504c5), [`882aa549b`](https://github.com/medusajs/medusa/commit/882aa549bdcc6f378934eab2a7c485df354f46aa)]:
|
||||
- @medusajs/utils@1.11.5
|
||||
- @medusajs/modules-sdk@1.12.8
|
||||
- @medusajs/types@1.11.12
|
||||
|
||||
## 0.3.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#6021](https://github.com/medusajs/medusa/pull/6021) [`a9b4214503`](https://github.com/medusajs/medusa/commit/a9b42145032ee88aa922a11fe03e777b140c68f4) Thanks [@adrien2p](https://github.com/adrien2p)! - chore(utils, product): Attempt to simplify module scripts export
|
||||
|
||||
- [#5763](https://github.com/medusajs/medusa/pull/5763) [`d85fee42e`](https://github.com/medusajs/medusa/commit/d85fee42ee7f661310584dfee5741d6c53b989bb) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Medusa App loading modules reference
|
||||
|
||||
- [#6127](https://github.com/medusajs/medusa/pull/6127) [`5e655dd59`](https://github.com/medusajs/medusa/commit/5e655dd59bda4ffface28db38021ba71cae6de10) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: Hide repository creation if they are not custom + add upsert support by default
|
||||
|
||||
- [#6035](https://github.com/medusajs/medusa/pull/6035) [`b6ac768698`](https://github.com/medusajs/medusa/commit/b6ac768698a3b49d0162cb49e628386f3352d034) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: Attempt to abstract the modules repository
|
||||
|
||||
- [#6087](https://github.com/medusajs/medusa/pull/6087) [`130c641e5c`](https://github.com/medusajs/medusa/commit/130c641e5c91cf831de64fb87aebbfdc4d23530d) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: Abstract module services
|
||||
|
||||
- Updated dependencies [[`68ddd866a5`](https://github.com/medusajs/medusa/commit/68ddd866a5ff9414e2db5b80d75acc5e81948540), [`72bc52231c`](https://github.com/medusajs/medusa/commit/72bc52231ca3a72fa6d197a248fe07a938ed0d85), [`99045848f`](https://github.com/medusajs/medusa/commit/99045848fd3e863359c7878d9bc05271ed083a0e), [`af7af7374`](https://github.com/medusajs/medusa/commit/af7af737455daa0f330840a9678e6339e519dfe6), [`fc6b1772a7`](https://github.com/medusajs/medusa/commit/fc6b1772a71582bb48602c5cac7b2297e9d267a9), [`a9b4214503`](https://github.com/medusajs/medusa/commit/a9b42145032ee88aa922a11fe03e777b140c68f4), [`d85fee42e`](https://github.com/medusajs/medusa/commit/d85fee42ee7f661310584dfee5741d6c53b989bb), [`5e655dd59`](https://github.com/medusajs/medusa/commit/5e655dd59bda4ffface28db38021ba71cae6de10), [`b132ff7669`](https://github.com/medusajs/medusa/commit/b132ff76693148b3a06373c168e8dd5e02970757), [`e28fa7fbdf`](https://github.com/medusajs/medusa/commit/e28fa7fbdf45c5b1fa19848db731132a0bf1757d), [`a12c28b7d5`](https://github.com/medusajs/medusa/commit/a12c28b7d5faed733bebbb4963dff50b9c8a33bc), [`b782d3bcb7`](https://github.com/medusajs/medusa/commit/b782d3bcb7e8088a962584b9a55200dd29c2161c), [`2b9f98895e`](https://github.com/medusajs/medusa/commit/2b9f98895eaca255e01278674b11cd7cb69b388f), [`7f7cb2a263`](https://github.com/medusajs/medusa/commit/7f7cb2a263c26baf540b05a40ab3732ffeb0c73c), [`302323916`](https://github.com/medusajs/medusa/commit/302323916b6d8eaf571cd59b5fc92a913af207de), [`da5cc4cf7`](https://github.com/medusajs/medusa/commit/da5cc4cf7f7f0ef40d409704a95b025ce95477f4), [`daecd82a7`](https://github.com/medusajs/medusa/commit/daecd82a7cdf7315599f464999690414c20d6748), [`ce81cade88`](https://github.com/medusajs/medusa/commit/ce81cade887659cefe9638e3c1c2807378191c62), [`fd78f5e24`](https://github.com/medusajs/medusa/commit/fd78f5e24263f5e158c3b7d11fbf0a4436e9c17a), [`192bc336cc`](https://github.com/medusajs/medusa/commit/192bc336cc2b6ec3820d94524c046dcd3c4ac7d9), [`06b33a9b4`](https://github.com/medusajs/medusa/commit/06b33a9b4525b77b1b14b35b973209700945654e), [`b6ac768698`](https://github.com/medusajs/medusa/commit/b6ac768698a3b49d0162cb49e628386f3352d034), [`130c641e5c`](https://github.com/medusajs/medusa/commit/130c641e5c91cf831de64fb87aebbfdc4d23530d), [`fade8ea7bf`](https://github.com/medusajs/medusa/commit/fade8ea7bf560343ecbde116d226ac44053cdb8e), [`8472460f53`](https://github.com/medusajs/medusa/commit/8472460f533322cc4535199aa768ac163021bc79), [`68d8daccd`](https://github.com/medusajs/medusa/commit/68d8daccd2a8508a13e211130e49017198b51fab)]:
|
||||
- @medusajs/types@1.11.11
|
||||
- @medusajs/utils@1.11.4
|
||||
- @medusajs/modules-sdk@1.12.7
|
||||
|
||||
## 0.3.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5985](https://github.com/medusajs/medusa/pull/5985) [`d16d10619`](https://github.com/medusajs/medusa/commit/d16d10619dfbd3966a4709753de3d8cc37c6f2eb) Thanks [@pKorsholm](https://github.com/pKorsholm)! - fix(medusa-test-utils, utils, link-modules, pricing, product): upgrade mikro-orm version
|
||||
|
||||
- Updated dependencies [[`6d1e3cc02`](https://github.com/medusajs/medusa/commit/6d1e3cc0285ef157fd6486060e8b32c00c01aa80), [`42cc8ae3f`](https://github.com/medusajs/medusa/commit/42cc8ae3f89ed7d642e51654d1a3cca011f13155), [`45996d58a2`](https://github.com/medusajs/medusa/commit/45996d58a2665d72335faad11bea958f8da74195), [`9cc787cac4`](https://github.com/medusajs/medusa/commit/9cc787cac4bf1c5d8edf1c4b548bb3205100e822), [`355075097`](https://github.com/medusajs/medusa/commit/3550750975a0c9359fd887929377733606ef03af), [`dc46ee118`](https://github.com/medusajs/medusa/commit/dc46ee1189c3eb719355da6a1d701c14a77e4578), [`925feea04`](https://github.com/medusajs/medusa/commit/925feea04a8222285175c33577548e50516069a7), [`3f6d79961`](https://github.com/medusajs/medusa/commit/3f6d79961dec1c5eb8950f8eacd94a5d87a4acde), [`fbee006e5`](https://github.com/medusajs/medusa/commit/fbee006e512ef2d56ffb23eeabad8b51b56be285), [`c41f3002f`](https://github.com/medusajs/medusa/commit/c41f3002f3118b1f195c5c822fe0f400091d115b), [`d16d10619`](https://github.com/medusajs/medusa/commit/d16d10619dfbd3966a4709753de3d8cc37c6f2eb), [`c1c470e6b`](https://github.com/medusajs/medusa/commit/c1c470e6b8646c5f0b4bca56a8e785f6c34e1fef), [`890e76a5c`](https://github.com/medusajs/medusa/commit/890e76a5c53039576c42ca4d46af6f6977cdebd1), [`fe007d01b`](https://github.com/medusajs/medusa/commit/fe007d01bd827f0e09ee545e48cef18913540c68), [`76332ca6c`](https://github.com/medusajs/medusa/commit/76332ca6c153a786acc07d3f06ff45c3b9346fd3)]:
|
||||
- @medusajs/modules-sdk@1.12.6
|
||||
- @medusajs/types@1.11.10
|
||||
- @medusajs/utils@1.11.3
|
||||
|
||||
## 0.3.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5755](https://github.com/medusajs/medusa/pull/5755) [`8f25ed8a1`](https://github.com/medusajs/medusa/commit/8f25ed8a10fe23e9342dc3d03545546b4ad4d6da) Thanks [@adrien2p](https://github.com/adrien2p)! - feat(link-modules, pricing, product, utils): Should be able to set some custom database config even in shared mode
|
||||
|
||||
- Updated dependencies [[`079f0da83`](https://github.com/medusajs/medusa/commit/079f0da83f482562bbb525807ee1a7e32993b4da), [`c4deeee48`](https://github.com/medusajs/medusa/commit/c4deeee481399f5371d773173e20dc149d502e20), [`8f25ed8a1`](https://github.com/medusajs/medusa/commit/8f25ed8a10fe23e9342dc3d03545546b4ad4d6da)]:
|
||||
- @medusajs/types@1.11.9
|
||||
- @medusajs/utils@1.11.2
|
||||
- @medusajs/modules-sdk@1.12.5
|
||||
|
||||
## 0.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5668](https://github.com/medusajs/medusa/pull/5668) [`a39ce125c`](https://github.com/medusajs/medusa/commit/a39ce125cc96f14732d5a6301313d2376484fa23) Thanks [@pKorsholm](https://github.com/pKorsholm)! - fix(workflows, product, types): Fix issues relating to update-variant workflow and options
|
||||
|
||||
- [#5536](https://github.com/medusajs/medusa/pull/5536) [`dc5750dd6`](https://github.com/medusajs/medusa/commit/dc5750dd665a91d35c0246ba83c7f90ec74907f4) Thanks [@pKorsholm](https://github.com/pKorsholm)! - feat(medusa,types,workflows,utils,product,pricing): PricingModule Integration of PriceLists into Core
|
||||
|
||||
- Updated dependencies [[`a39ce125c`](https://github.com/medusajs/medusa/commit/a39ce125cc96f14732d5a6301313d2376484fa23), [`18afe0b9a`](https://github.com/medusajs/medusa/commit/18afe0b9addb33ec2e3b285651b4eb1ef8065845), [`6025c702f`](https://github.com/medusajs/medusa/commit/6025c702f37d43e18af32bd716f33410d95efd19), [`de8f74867`](https://github.com/medusajs/medusa/commit/de8f748674bfd19b3dbadb9695d9080aa91940de), [`fc1ef29ed`](https://github.com/medusajs/medusa/commit/fc1ef29ed935e192f0943a2bf4b8fbb05ce6890d), [`0df1c7d42`](https://github.com/medusajs/medusa/commit/0df1c7d4273545bc717555611b9294a5c222e5ae), [`dc5750dd6`](https://github.com/medusajs/medusa/commit/dc5750dd665a91d35c0246ba83c7f90ec74907f4)]:
|
||||
- @medusajs/types@1.11.8
|
||||
- @medusajs/utils@1.11.1
|
||||
- @medusajs/modules-sdk@1.12.4
|
||||
|
||||
## 0.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5643](https://github.com/medusajs/medusa/pull/5643) [`c4722715c`](https://github.com/medusajs/medusa/commit/c4722715cd103e88ff8a3a3856d8138f211478ca) Thanks [@riqwan](https://github.com/riqwan)! - fix(product): when running migrations, prevent exploding on isolated case
|
||||
|
||||
- Updated dependencies [[`61aef4aaa`](https://github.com/medusajs/medusa/commit/61aef4aaa7295f178c61c9a891f03a0a48b15c81), [`1772e80ed`](https://github.com/medusajs/medusa/commit/1772e80ed1ecab27741be80204f5df92eaa3f2b4), [`cedab5833`](https://github.com/medusajs/medusa/commit/cedab583395275444001f0268e4b9ccab9b2b262)]:
|
||||
- @medusajs/types@1.11.7
|
||||
- @medusajs/utils@1.11.0
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5533](https://github.com/medusajs/medusa/pull/5533) [`f88d75b0a`](https://github.com/medusajs/medusa/commit/f88d75b0a7621ff9788a24775fa53d4c2987b52d) Thanks [@adrien2p](https://github.com/adrien2p)! - feat(product, pricing, utils): Transaction issues and reference issues + refactoring
|
||||
|
||||
- [`c39bf69a5`](https://github.com/medusajs/medusa/commit/c39bf69a5e5cae75d7fa12aa6022b10903557a32) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Add missing version bumps
|
||||
|
||||
- Updated dependencies [[`91615f9c4`](https://github.com/medusajs/medusa/commit/91615f9c459a2d8cb842561c5edb335680d30298), [`c39bf69a5`](https://github.com/medusajs/medusa/commit/c39bf69a5e5cae75d7fa12aa6022b10903557a32), [`154c9b43b`](https://github.com/medusajs/medusa/commit/154c9b43bde1fdff562aba9da8a79af2660b29b3)]:
|
||||
- @medusajs/types@1.11.6
|
||||
- @medusajs/modules-sdk@1.12.3
|
||||
- @medusajs/utils@1.10.5
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5468](https://github.com/medusajs/medusa/pull/5468) [`a45da9215`](https://github.com/medusajs/medusa/commit/a45da9215d2a7834c368037726aaa3961caadaf9) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(medusa, modules-sdk, modules): Module loading was missing the expected dependencies and remote query reference fix
|
||||
|
||||
- Updated dependencies [[`148f537b4`](https://github.com/medusajs/medusa/commit/148f537b47635e8b73ebaa27bbfbe58624bfe641), [`a45da9215`](https://github.com/medusajs/medusa/commit/a45da9215d2a7834c368037726aaa3961caadaf9)]:
|
||||
- @medusajs/types@1.11.5
|
||||
- @medusajs/modules-sdk@1.12.2
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5387](https://github.com/medusajs/medusa/pull/5387) [`453297f52`](https://github.com/medusajs/medusa/commit/453297f525bd9f3aaa95bf0b28ff6cd31e6696b4) Thanks [@riqwan](https://github.com/riqwan)! - chore(product,types): fix interfaces for product module
|
||||
|
||||
- Updated dependencies [[`378ca1b36`](https://github.com/medusajs/medusa/commit/378ca1b36e909a67e39c69ea5ca94ec58a345878), [`69cf7215f`](https://github.com/medusajs/medusa/commit/69cf7215f1f730ffb332129e65211470be1f88f1), [`453297f52`](https://github.com/medusajs/medusa/commit/453297f525bd9f3aaa95bf0b28ff6cd31e6696b4), [`b62af612c`](https://github.com/medusajs/medusa/commit/b62af612c7baa244075e546c949b89c4589bd2cf), [`e47461d95`](https://github.com/medusajs/medusa/commit/e47461d95caecf3a447ee9fa0b0950340b93f282), [`66413d094`](https://github.com/medusajs/medusa/commit/66413d094e916debbdb74b68800c96ca2c9302c9)]:
|
||||
- @medusajs/utils@1.10.4
|
||||
- @medusajs/types@1.11.4
|
||||
- @medusajs/modules-sdk@1.12.1
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5242](https://github.com/medusajs/medusa/pull/5242) [`130cbc1f4`](https://github.com/medusajs/medusa/commit/130cbc1f437af211b6d05f80128d90138abcd38d) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Modules exporting schema with entities and fields
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`eeceec791`](https://github.com/medusajs/medusa/commit/eeceec791c141996cf7fd06555afb6e738b52840), [`130cbc1f4`](https://github.com/medusajs/medusa/commit/130cbc1f437af211b6d05f80128d90138abcd38d), [`cb569c2df`](https://github.com/medusajs/medusa/commit/cb569c2dfe2d83e1ff72a49f2331450a83b73325), [`c5703a476`](https://github.com/medusajs/medusa/commit/c5703a4765a55da697885438cf3089d923669f21)]:
|
||||
- @medusajs/utils@1.10.3
|
||||
- @medusajs/modules-sdk@1.12.0
|
||||
- @medusajs/types@1.11.3
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4969](https://github.com/medusajs/medusa/pull/4969) [`30863fee5`](https://github.com/medusajs/medusa/commit/30863fee529ed035f161c749fda3cd64fa48efb1) Thanks [@adrien2p](https://github.com/adrien2p)! - feat: store List products remote query with product isolation
|
||||
|
||||
- Updated dependencies [[`d8649baca`](https://github.com/medusajs/medusa/commit/d8649bacaa2ed784b9e7b2b0e1f1194d3697bb92), [`4fa675ec2`](https://github.com/medusajs/medusa/commit/4fa675ec25b3d6fccd881c4f5a5b91f0e9e13e82), [`6273b4b16`](https://github.com/medusajs/medusa/commit/6273b4b160493463e1199e5db4e9cfa4cff6fbe4), [`30863fee5`](https://github.com/medusajs/medusa/commit/30863fee529ed035f161c749fda3cd64fa48efb1), [`3d68be2b6`](https://github.com/medusajs/medusa/commit/3d68be2b6b93ae928f5c955e102ebdf2c34fb364), [`a87d07655`](https://github.com/medusajs/medusa/commit/a87d07655bd8a1da8b90feb739daddd09295f724), [`edf90eecb`](https://github.com/medusajs/medusa/commit/edf90eecb487f6e031f2e2d0899de5ca2504cb12), [`107aaa371`](https://github.com/medusajs/medusa/commit/107aaa371c444843874d125bf8bd493ef89f5756), [`834da5c41`](https://github.com/medusajs/medusa/commit/834da5c41a7c043373f72239b6fdbf7815d9b4aa)]:
|
||||
- @medusajs/modules-sdk@1.11.0
|
||||
- @medusajs/types@1.11.1
|
||||
- @medusajs/utils@1.10.1
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [`09d97fd05`](https://github.com/medusajs/medusa/commit/09d97fd05ea093225fadbb1e1698d324e490895e) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore(changesets): Add changeset minor bumping @medusajs/product
|
||||
|
||||
- [#4695](https://github.com/medusajs/medusa/pull/4695) [`4d16acf5f`](https://github.com/medusajs/medusa/commit/4d16acf5f096b5656b645f510f9c971e7c2dc9ef) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - introduce @medusajs/link-modules
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4965](https://github.com/medusajs/medusa/pull/4965) [`66bd9a835`](https://github.com/medusajs/medusa/commit/66bd9a835c61b139af7051e5faf6c9de3c7134bb) Thanks [@riqwan](https://github.com/riqwan)! - feat(products,types,pricing): allow scoping products by collection_id, allow scoping pricing by currency_code
|
||||
|
||||
- [#4915](https://github.com/medusajs/medusa/pull/4915) [`87bade096`](https://github.com/medusajs/medusa/commit/87bade096e3d536f29ddc57dbc4c04e5d7a46e4b) Thanks [@riqwan](https://github.com/riqwan)! - fix(utils, product, pricing, link-modules): add missing dependencies for utils + fix migration path issue
|
||||
|
||||
- Updated dependencies [[`c3dba0694`](https://github.com/medusajs/medusa/commit/c3dba069488952945150117a30b1306a2e0bb3ce), [`460161a69`](https://github.com/medusajs/medusa/commit/460161a69f22cf6d561952e92e7d9b56912113e6), [`fcb6b4f51`](https://github.com/medusajs/medusa/commit/fcb6b4f510dba2757570625acb5da9476b7544fd), [`66bd9a835`](https://github.com/medusajs/medusa/commit/66bd9a835c61b139af7051e5faf6c9de3c7134bb), [`4d16acf5f`](https://github.com/medusajs/medusa/commit/4d16acf5f096b5656b645f510f9c971e7c2dc9ef), [`4d16acf5f`](https://github.com/medusajs/medusa/commit/4d16acf5f096b5656b645f510f9c971e7c2dc9ef), [`87bade096`](https://github.com/medusajs/medusa/commit/87bade096e3d536f29ddc57dbc4c04e5d7a46e4b), [`4d16acf5f`](https://github.com/medusajs/medusa/commit/4d16acf5f096b5656b645f510f9c971e7c2dc9ef), [`a4906d0ac`](https://github.com/medusajs/medusa/commit/a4906d0ac0af36b1382d3befe64281b404387bd7)]:
|
||||
- @medusajs/modules-sdk@1.10.0
|
||||
- @medusajs/types@1.11.0
|
||||
- @medusajs/utils@1.10.0
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4825](https://github.com/medusajs/medusa/pull/4825) [`c53fa6cd3`](https://github.com/medusajs/medusa/commit/c53fa6cd3be8b1af5625fc2db7871f9f69e761ad) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Fix mikro-orm connection loader
|
||||
|
||||
- Updated dependencies [[`c53fa6cd3`](https://github.com/medusajs/medusa/commit/c53fa6cd3be8b1af5625fc2db7871f9f69e761ad)]:
|
||||
- @medusajs/utils@1.9.7
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4626](https://github.com/medusajs/medusa/pull/4626) [`3f3a84262`](https://github.com/medusajs/medusa/commit/3f3a84262ce9cbd911923278a54e301fbe9a4634) Thanks [@adrien2p](https://github.com/adrien2p)! - [WIP] feat(types, product, utils, medusa): Include shared connection for modules
|
||||
|
||||
- [#4731](https://github.com/medusajs/medusa/pull/4731) [`f8d3d5f91`](https://github.com/medusajs/medusa/commit/f8d3d5f91ac6282958911fe2f1973ad81f459747) Thanks [@adrien2p](https://github.com/adrien2p)! - chore(product): remove decorator where it is not necessary and cleanup
|
||||
|
||||
- Updated dependencies [[`ac866ebb5`](https://github.com/medusajs/medusa/commit/ac866ebb5197ee694dda91824b501109012a3dd1), [`3f3a84262`](https://github.com/medusajs/medusa/commit/3f3a84262ce9cbd911923278a54e301fbe9a4634), [`30ce35b16`](https://github.com/medusajs/medusa/commit/30ce35b163afa25f4e1d8d1bd392f401a3b413df), [`281b0746c`](https://github.com/medusajs/medusa/commit/281b0746cfbe80b83c6a67d1ea120b47a0ea7121)]:
|
||||
- @medusajs/types@1.10.2
|
||||
- @medusajs/modules-sdk@1.9.2
|
||||
- @medusajs/utils@1.9.6
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4674](https://github.com/medusajs/medusa/pull/4674) [`4b80ba8a3`](https://github.com/medusajs/medusa/commit/4b80ba8a356806da3c92634e40e8946da25e35ee) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(product, integration-tests): Fix integration tests
|
||||
|
||||
- [#4654](https://github.com/medusajs/medusa/pull/4654) [`8af55aed8`](https://github.com/medusajs/medusa/commit/8af55aed87da7252c7c261175bc98331466a0da8) Thanks [@riqwan](https://github.com/riqwan)! - feat(product,types): added event bus events for products
|
||||
|
||||
- [#4631](https://github.com/medusajs/medusa/pull/4631) [`4073b7313`](https://github.com/medusajs/medusa/commit/4073b73130c874dc7d2240726224a01b7b19b1a1) Thanks [@adrien2p](https://github.com/adrien2p)! - feat(product): Move mikro orm utils to the utils package
|
||||
|
||||
- Updated dependencies [[`4b80ba8a3`](https://github.com/medusajs/medusa/commit/4b80ba8a356806da3c92634e40e8946da25e35ee), [`8af55aed8`](https://github.com/medusajs/medusa/commit/8af55aed87da7252c7c261175bc98331466a0da8), [`5c60aad17`](https://github.com/medusajs/medusa/commit/5c60aad177a99574ffff5ebdc02ce9dc86ef9af9), [`4073b7313`](https://github.com/medusajs/medusa/commit/4073b73130c874dc7d2240726224a01b7b19b1a1)]:
|
||||
- @medusajs/modules-sdk@1.9.1
|
||||
- @medusajs/types@1.10.1
|
||||
- @medusajs/utils@1.9.5
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4602](https://github.com/medusajs/medusa/pull/4602) [`585ebf245`](https://github.com/medusajs/medusa/commit/585ebf2454719ed36957ba798a189a45a5274c5a) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(product): Serialize typings
|
||||
|
||||
- [#4535](https://github.com/medusajs/medusa/pull/4535) [`131477faf`](https://github.com/medusajs/medusa/commit/131477faf0409c49d4aacf26ea591e33b2fa22fd) Thanks [@riqwan](https://github.com/riqwan)! - feat(product,types,utils): Add tags, types, categories, collection and options CRUD to product module services
|
||||
|
||||
- [#4590](https://github.com/medusajs/medusa/pull/4590) [`a0a041e5c`](https://github.com/medusajs/medusa/commit/a0a041e5c9b8aa5a418f500ba17fca2bea53f728) Thanks [@riqwan](https://github.com/riqwan)! - fix(medusa): correct the type of initializer for modules
|
||||
|
||||
- [#4504](https://github.com/medusajs/medusa/pull/4504) [`caea44ebf`](https://github.com/medusajs/medusa/commit/caea44ebfdf7393ace931ce2a9884105dadc4f8d) Thanks [@riqwan](https://github.com/riqwan)! - feat(product, types): added product module service update
|
||||
|
||||
- Updated dependencies [[`585ebf245`](https://github.com/medusajs/medusa/commit/585ebf2454719ed36957ba798a189a45a5274c5a), [`131477faf`](https://github.com/medusajs/medusa/commit/131477faf0409c49d4aacf26ea591e33b2fa22fd), [`379c83933`](https://github.com/medusajs/medusa/commit/379c83933ed12a4ec712e7f3c9b0252e4a4601dd), [`caea44ebf`](https://github.com/medusajs/medusa/commit/caea44ebfdf7393ace931ce2a9884105dadc4f8d), [`f174bb6fa`](https://github.com/medusajs/medusa/commit/f174bb6fa1b105b39065478a67b6be0b968f707a), [`f12299deb`](https://github.com/medusajs/medusa/commit/f12299deb10baadab1505cd4ac353dd5d1c8fa7c)]:
|
||||
- @medusajs/types@1.9.0
|
||||
- @medusajs/utils@1.9.3
|
||||
- @medusajs/modules-sdk@1.9.0
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4459](https://github.com/medusajs/medusa/pull/4459) [`befc2f1c8`](https://github.com/medusajs/medusa/commit/befc2f1c80b6aaeb3a5153f7fdeaa96cf832e46f) Thanks [@adrien2p](https://github.com/adrien2p)! - Feat(product): product module create - delete - soft delete - restore - create workflow
|
||||
|
||||
- [#4459](https://github.com/medusajs/medusa/pull/4459) [`befc2f1c8`](https://github.com/medusajs/medusa/commit/befc2f1c80b6aaeb3a5153f7fdeaa96cf832e46f) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: added collection methods for module and collection service
|
||||
|
||||
- Updated dependencies [[`d184d23c6`](https://github.com/medusajs/medusa/commit/d184d23c6384d5f8bf52827826b62c6bef37f884), [`befc2f1c8`](https://github.com/medusajs/medusa/commit/befc2f1c80b6aaeb3a5153f7fdeaa96cf832e46f)]:
|
||||
- @medusajs/types@1.8.11
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4420](https://github.com/medusajs/medusa/pull/4420) [`6f1fa244f`](https://github.com/medusajs/medusa/commit/6f1fa244fa47d4ecdaa7363483bd7da555dbbf32) Thanks [@adrien2p](https://github.com/adrien2p)! - chore(medusa-cli): Cleanup plugin setup + include Logger type update which is used across multiple packages
|
||||
|
||||
- Updated dependencies [[`499c3478c`](https://github.com/medusajs/medusa/commit/499c3478c910c8b922a15cc6f4d9fbad122a347f), [`9dcdc0041`](https://github.com/medusajs/medusa/commit/9dcdc0041a2b08cc0723343dd8d9127d9977b086), [`6f1fa244f`](https://github.com/medusajs/medusa/commit/6f1fa244fa47d4ecdaa7363483bd7da555dbbf32), [`9760d4a96`](https://github.com/medusajs/medusa/commit/9760d4a96c27f6f89a8c3f3b6e73b17547f97f2a)]:
|
||||
- @medusajs/types@1.8.10
|
||||
- @medusajs/utils@1.9.2
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4348](https://github.com/medusajs/medusa/pull/4348) [`dc120121c`](https://github.com/medusajs/medusa/commit/dc120121c12bd2722d97c65e242888bb552ef78a) Thanks [@riqwan](https://github.com/riqwan)! - fix(product, types): correct path for migration files + fix types on product module service interface
|
||||
|
||||
- Updated dependencies [[`dc120121c`](https://github.com/medusajs/medusa/commit/dc120121c12bd2722d97c65e242888bb552ef78a)]:
|
||||
- @medusajs/types@1.8.9
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [`12782778f`](https://github.com/medusajs/medusa/commit/12782778f93e6bb5b2b7e1b96f40af18af038095) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: @medusajs/product beta release
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#4296](https://github.com/medusajs/medusa/pull/4296) [`30d89044f`](https://github.com/medusajs/medusa/commit/30d89044f1f1b63b6b8c875cdcbb7747a1899360) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(product): The bin scripts should include a shebang and import in the body
|
||||
|
||||
- Updated dependencies [[`f98ba5bde`](https://github.com/medusajs/medusa/commit/f98ba5bde83ba785eead31b0c9eb9f135d664178), [`d76ba0cd2`](https://github.com/medusajs/medusa/commit/d76ba0cd29694c2e31f9f89992a9fbc14659c1ae), [`14c0f62f8`](https://github.com/medusajs/medusa/commit/14c0f62f84704a4c87beff3daaff60a52f5c88b8)]:
|
||||
- @medusajs/utils@1.9.1
|
||||
- @medusajs/types@1.8.8
|
||||
- @medusajs/modules-sdk@1.8.8
|
||||
196
packages/modules/product/README.md
Normal file
196
packages/modules/product/README.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Product Module
|
||||
|
||||
The Product Module gives you access Products, Variants, Categories, and more through a standalone package that can be installed and run in Next.js functions and other Node.js compatible runtimes.
|
||||
|
||||
[Product Module documentation](https://docs.medusajs.com/modules/product/serverless-module) | [Medusa Website](https://medusajs.com/) | [Medusa Repository](https://github.com/medusajs/medusa)
|
||||
|
||||
> The Product Module is currently in beta. The beta version comes with limited functionality, primarily centered around retrieving products. In the official version, the product module will be fully-fledged and on par with the product functionality in our core package.
|
||||
|
||||
---
|
||||
|
||||
## Installing and using it in Next.js
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Next.js project](https://nextjs.org/docs/pages/api-reference/create-next-app)
|
||||
|
||||
1\. Run the following command in your project
|
||||
|
||||
```bash
|
||||
npm install @medusajs/product
|
||||
```
|
||||
|
||||
2\. Add Database URL to your environment variables
|
||||
|
||||
```bash
|
||||
POSTGRES_URL=<DATABASE_URL>
|
||||
```
|
||||
|
||||
3\. Apply database migrations
|
||||
|
||||
> If you are using an existing Medusa database, you can skip this step. This step is only applicable when the module is used in isolation from a full Medusa setup
|
||||
|
||||
Before you can run migrations, add in your `package.json` the following scripts:
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
//...other scripts
|
||||
"product:migrations:run": "medusa-product-migrations-up",
|
||||
"product:seed": "medusa-product-seed ./seed-data.js"
|
||||
},
|
||||
```
|
||||
|
||||
The first command runs the migrations, and the second command allows you to optionally seed your database with demo products.
|
||||
|
||||
For the second command to work, you'll need to add the dummy seed data to the root of your Next.js project:
|
||||
|
||||
<details>
|
||||
<summary>Seed file</summary>
|
||||
|
||||
```js
|
||||
const productCategoriesData = [
|
||||
{
|
||||
id: "category-0",
|
||||
name: "category 0",
|
||||
parent_category_id: null,
|
||||
},
|
||||
{
|
||||
id: "category-1",
|
||||
name: "category 1",
|
||||
parent_category_id: "category-0",
|
||||
},
|
||||
{
|
||||
id: "category-1-a",
|
||||
name: "category 1 a",
|
||||
parent_category_id: "category-1",
|
||||
},
|
||||
{
|
||||
id: "category-1-b",
|
||||
name: "category 1 b",
|
||||
parent_category_id: "category-1",
|
||||
is_internal: true,
|
||||
},
|
||||
{
|
||||
id: "category-1-b-1",
|
||||
name: "category 1 b 1",
|
||||
parent_category_id: "category-1-b",
|
||||
},
|
||||
]
|
||||
|
||||
const productsData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "product 1",
|
||||
status: "published",
|
||||
descriptions: "Lorem ipsum dolor sit amet, consectetur.",
|
||||
tags: [
|
||||
{
|
||||
id: "tag-1",
|
||||
value: "France",
|
||||
},
|
||||
],
|
||||
categories: [
|
||||
{
|
||||
id: "category-0",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "product",
|
||||
status: "published",
|
||||
descriptions: "Lorem ipsum dolor sit amet, consectetur.",
|
||||
tags: [
|
||||
{
|
||||
id: "tag-2",
|
||||
value: "Germany",
|
||||
},
|
||||
],
|
||||
categories: [
|
||||
{
|
||||
id: "category-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const variantsData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "variant title",
|
||||
sku: "sku 1",
|
||||
product: { id: productsData[0].id },
|
||||
inventory_quantity: 10,
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "variant title",
|
||||
sku: "sku 2",
|
||||
product: { id: productsData[1].id },
|
||||
inventory_quantity: 10,
|
||||
},
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
productCategoriesData,
|
||||
productsData,
|
||||
variantsData,
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Then run the first and optionally the second command to migrate and seed the database:
|
||||
|
||||
```bash
|
||||
npm run product:migrations:run
|
||||
# optionally
|
||||
npm run product:seed
|
||||
```
|
||||
|
||||
4\. Adjust Next.js config
|
||||
|
||||
Next.js uses Webpack for compilation. Since quite a few of the dependencies used by the product module are not Webpack optimized, you have to add the product module as an external dependency.
|
||||
|
||||
To do that, add the serverComponentsExternalPackages option in `next.config.js`:
|
||||
|
||||
```js
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ["@medusajs/product"],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
```
|
||||
|
||||
5\. Create API Route
|
||||
|
||||
The product module is ready for use now! You can now use it to create API endpoints within your Next.js application.
|
||||
|
||||
For example, create the file `app/api/products/route.ts` with the following content:
|
||||
|
||||
```ts
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
import { initialize as initializeProductModule } from "@medusajs/product"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const productService = await initializeProductModule()
|
||||
|
||||
const data = await productService.list()
|
||||
|
||||
return NextResponse.json({ products: data })
|
||||
}
|
||||
```
|
||||
|
||||
6\. Test your Next.js application
|
||||
To test the endpoint you added, start your Next.js application with the following command:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, open in your browser the URL `http://localhost:3000/api/products`. If you seeded your database with demo products, or you’re using a Medusa database schema, you’ll receive the products in your database. Otherwise, the request will return an empty array.
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Context } from "@medusajs/types"
|
||||
import { DALUtils } from "@medusajs/utils"
|
||||
|
||||
class CustomRepository extends DALUtils.MikroOrmBaseRepository {
|
||||
constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
find = jest.fn().mockImplementation(async () => [])
|
||||
findAndCount = jest.fn().mockImplementation(async () => [])
|
||||
create = jest.fn()
|
||||
update = jest.fn()
|
||||
delete = jest.fn()
|
||||
softDelete = jest.fn()
|
||||
restore = jest.fn()
|
||||
|
||||
async transaction<TManager = unknown>(
|
||||
task: (transactionManager: TManager) => Promise<any>,
|
||||
options: {
|
||||
isolationLevel?: string
|
||||
enableNestedTransactions?: boolean
|
||||
transaction?: TManager
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
return super.transaction(task, options)
|
||||
}
|
||||
|
||||
getActiveManager<TManager = unknown>({
|
||||
transactionManager,
|
||||
manager,
|
||||
}: Context = {}): TManager {
|
||||
return super.getActiveManager({ transactionManager, manager })
|
||||
}
|
||||
|
||||
getFreshManager<TManager = unknown>(): TManager {
|
||||
return super.getFreshManager()
|
||||
}
|
||||
|
||||
async serialize<TOutput extends object | object[]>(
|
||||
data: any,
|
||||
options?: any
|
||||
): Promise<TOutput> {
|
||||
return super.serialize(data, options)
|
||||
}
|
||||
}
|
||||
|
||||
export class ProductRepository extends CustomRepository {}
|
||||
export class ProductTagRepository extends CustomRepository {}
|
||||
export class ProductCollectionRepository extends CustomRepository {}
|
||||
export class ProductVariantRepository extends CustomRepository {}
|
||||
export class ProductCategoryRepository extends CustomRepository {}
|
||||
@@ -0,0 +1,191 @@
|
||||
export const productCategoriesData = [
|
||||
{
|
||||
id: "category-0",
|
||||
name: "category 0",
|
||||
parent_category_id: null,
|
||||
},
|
||||
{
|
||||
id: "category-1",
|
||||
name: "category 1",
|
||||
parent_category_id: "category-0",
|
||||
},
|
||||
{
|
||||
id: "category-1-a",
|
||||
name: "category 1 a",
|
||||
parent_category_id: "category-1",
|
||||
},
|
||||
{
|
||||
id: "category-1-b",
|
||||
name: "category 1 b",
|
||||
parent_category_id: "category-1",
|
||||
is_internal: true,
|
||||
},
|
||||
{
|
||||
id: "category-1-b-1",
|
||||
name: "category 1 b 1",
|
||||
parent_category_id: "category-1-b",
|
||||
},
|
||||
]
|
||||
|
||||
export const productCategoriesRankData = [
|
||||
{
|
||||
id: "category-0-0",
|
||||
name: "category 0 0",
|
||||
parent_category_id: null,
|
||||
rank: 0,
|
||||
},
|
||||
{
|
||||
id: "category-0-1",
|
||||
name: "category 0 1",
|
||||
parent_category_id: null,
|
||||
rank: 1,
|
||||
},
|
||||
{
|
||||
id: "category-0-2",
|
||||
name: "category 0 2",
|
||||
parent_category_id: null,
|
||||
rank: 2,
|
||||
},
|
||||
{
|
||||
id: "category-0-0-0",
|
||||
name: "category 0 0-0",
|
||||
parent_category_id: "category-0-0",
|
||||
rank: 0,
|
||||
},
|
||||
{
|
||||
id: "category-0-0-1",
|
||||
name: "category 0 0-1",
|
||||
parent_category_id: "category-0-0",
|
||||
rank: 1,
|
||||
},
|
||||
{
|
||||
id: "category-0-0-2",
|
||||
name: "category 0 0-2",
|
||||
parent_category_id: "category-0-0",
|
||||
rank: 2,
|
||||
},
|
||||
]
|
||||
|
||||
export const eletronicsCategoriesData = eval(`[
|
||||
{
|
||||
id: "electronics",
|
||||
name: "Electronics",
|
||||
parent_category_id: null,
|
||||
},
|
||||
{
|
||||
id: "computers",
|
||||
name: "Computers & Accessories",
|
||||
parent_category_id: "electronics",
|
||||
},
|
||||
{
|
||||
id: "desktops",
|
||||
name: "Desktops",
|
||||
parent_category_id: "computers",
|
||||
},
|
||||
{
|
||||
id: "gaming-desktops",
|
||||
name: "Gaming Desktops",
|
||||
parent_category_id: "desktops",
|
||||
},
|
||||
{
|
||||
id: "office-desktops",
|
||||
name: "Office Desktops",
|
||||
parent_category_id: "desktops",
|
||||
},
|
||||
{
|
||||
id: "laptops",
|
||||
name: "Laptops",
|
||||
parent_category_id: "computers",
|
||||
},
|
||||
{
|
||||
id: "gaming-laptops",
|
||||
name: "Gaming Laptops",
|
||||
parent_category_id: "laptops",
|
||||
},
|
||||
{
|
||||
id: "budget-gaming",
|
||||
name: "Budget Gaming Laptops",
|
||||
parent_category_id: "gaming-laptops",
|
||||
},
|
||||
{
|
||||
id: "high-performance",
|
||||
name: "High Performance Gaming Laptops",
|
||||
parent_category_id: "gaming-laptops",
|
||||
},
|
||||
{
|
||||
id: "vr-ready",
|
||||
name: "VR-Ready High Performance Gaming Laptops",
|
||||
parent_category_id: "high-performance",
|
||||
},
|
||||
{
|
||||
id: "4k-gaming",
|
||||
name: "4K Gaming Laptops",
|
||||
parent_category_id: "high-performance",
|
||||
},
|
||||
{
|
||||
id: "ultrabooks",
|
||||
name: "Ultrabooks",
|
||||
parent_category_id: "laptops",
|
||||
},
|
||||
{
|
||||
id: "thin-light",
|
||||
name: "Thin & Light Ultrabooks",
|
||||
parent_category_id: "ultrabooks",
|
||||
},
|
||||
{
|
||||
id: "convertible-ultrabooks",
|
||||
name: "Convertible Ultrabooks",
|
||||
parent_category_id: "ultrabooks",
|
||||
},
|
||||
{
|
||||
id: "touchscreen-ultrabooks",
|
||||
name: "Touchscreen Ultrabooks",
|
||||
parent_category_id: "convertible-ultrabooks",
|
||||
},
|
||||
{
|
||||
id: "detachable-ultrabooks",
|
||||
name: "Detachable Ultrabooks",
|
||||
parent_category_id: "convertible-ultrabooks",
|
||||
},
|
||||
|
||||
{
|
||||
id: "mobile",
|
||||
name: "Mobile Phones & Accessories",
|
||||
parent_category_id: "electronics",
|
||||
},
|
||||
{
|
||||
id: "smartphones",
|
||||
name: "Smartphones",
|
||||
parent_category_id: "mobile",
|
||||
},
|
||||
{
|
||||
id: "android-phones",
|
||||
name: "Android Phones",
|
||||
parent_category_id: "smartphones",
|
||||
},
|
||||
{
|
||||
id: "flagship-phones",
|
||||
name: "Flagship Smartphones",
|
||||
parent_category_id: "android-phones",
|
||||
},
|
||||
{
|
||||
id: "budget-phones",
|
||||
name: "Budget Smartphones",
|
||||
parent_category_id: "android-phones",
|
||||
},
|
||||
{
|
||||
id: "iphones",
|
||||
name: "iPhones",
|
||||
parent_category_id: "smartphones",
|
||||
},
|
||||
{
|
||||
id: "pro-phones",
|
||||
name: "Pro Models",
|
||||
parent_category_id: "iphones",
|
||||
},
|
||||
{
|
||||
id: "mini-phones",
|
||||
name: "Mini Models",
|
||||
parent_category_id: "iphones",
|
||||
},
|
||||
]`)
|
||||
@@ -0,0 +1,31 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductCategory } from "@models"
|
||||
|
||||
export async function createProductCategories(
|
||||
manager: SqlEntityManager,
|
||||
categoriesData: any[]
|
||||
): Promise<ProductCategory[]> {
|
||||
const categories: ProductCategory[] = []
|
||||
|
||||
for (let categoryData of categoriesData) {
|
||||
let categoryDataClone = { ...categoryData }
|
||||
let parentCategory: ProductCategory | null = null
|
||||
const parentCategoryId = categoryDataClone.parent_category_id as string
|
||||
delete categoryDataClone.parent_category_id
|
||||
|
||||
if (parentCategoryId) {
|
||||
parentCategory = await manager.findOne(ProductCategory, parentCategoryId)
|
||||
}
|
||||
|
||||
const category = manager.create(ProductCategory, {
|
||||
...categoryDataClone,
|
||||
parent_category: parentCategory,
|
||||
})
|
||||
|
||||
categories.push(category)
|
||||
}
|
||||
|
||||
await manager.persistAndFlush(categories)
|
||||
|
||||
return categories
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const categoriesData = [
|
||||
{
|
||||
id: "category-0",
|
||||
name: "category 0",
|
||||
parent_category_id: null,
|
||||
},
|
||||
{
|
||||
id: "category-1",
|
||||
name: "category 1",
|
||||
parent_category_id: "category-0",
|
||||
},
|
||||
{
|
||||
id: "category-1-a",
|
||||
name: "category 1 a",
|
||||
parent_category_id: "category-1",
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { Image } from "@models"
|
||||
import faker from "faker"
|
||||
|
||||
export const buildProductOnlyData = ({
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
is_giftcard,
|
||||
discountable,
|
||||
thumbnail,
|
||||
images,
|
||||
status,
|
||||
}: {
|
||||
title?: string
|
||||
description?: string
|
||||
subtitle?: string
|
||||
is_giftcard?: boolean
|
||||
discountable?: boolean
|
||||
thumbnail?: string
|
||||
images?: { id?: string; url: string }[]
|
||||
status?: ProductTypes.ProductStatus
|
||||
} = {}) => {
|
||||
return {
|
||||
title: title ?? faker.commerce.productName(),
|
||||
description: description ?? faker.commerce.productName(),
|
||||
subtitle: subtitle ?? faker.commerce.productName(),
|
||||
is_giftcard: is_giftcard ?? false,
|
||||
discountable: discountable ?? true,
|
||||
thumbnail: thumbnail as string,
|
||||
status: status ?? ProductTypes.ProductStatus.PUBLISHED,
|
||||
images: (images ?? []) as Image[],
|
||||
}
|
||||
}
|
||||
|
||||
export const buildProductAndRelationsData = ({
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
is_giftcard,
|
||||
discountable,
|
||||
thumbnail,
|
||||
images,
|
||||
status,
|
||||
type_id,
|
||||
tags,
|
||||
options,
|
||||
variants,
|
||||
collection_id,
|
||||
}: Partial<ProductTypes.CreateProductDTO>) => {
|
||||
const defaultOptionTitle = "test-option"
|
||||
const defaultOptionValue = "test-value"
|
||||
|
||||
return {
|
||||
title: title ?? faker.commerce.productName(),
|
||||
description: description ?? faker.commerce.productName(),
|
||||
subtitle: subtitle ?? faker.commerce.productName(),
|
||||
is_giftcard: is_giftcard ?? false,
|
||||
discountable: discountable ?? true,
|
||||
thumbnail: thumbnail as string,
|
||||
status: status ?? ProductTypes.ProductStatus.PUBLISHED,
|
||||
images: (images ?? []) as Image[],
|
||||
type_id,
|
||||
tags: tags ?? [{ value: "tag-1" }],
|
||||
collection_id,
|
||||
options: options ?? [
|
||||
{
|
||||
title: defaultOptionTitle,
|
||||
values: [defaultOptionValue],
|
||||
},
|
||||
],
|
||||
variants: variants ?? [
|
||||
{
|
||||
title: faker.commerce.productName(),
|
||||
sku: faker.commerce.productName(),
|
||||
options: {
|
||||
[defaultOptionTitle]: defaultOptionValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
// TODO: add categories, must be created first
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./categories"
|
||||
export * from "./products"
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
|
||||
export const productsData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
tags: [
|
||||
{
|
||||
id: "tag-1",
|
||||
value: "France",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "product",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
tags: [
|
||||
{
|
||||
id: "tag-2",
|
||||
value: "Germany",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "test-3",
|
||||
title: "product 3",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
tags: [
|
||||
{
|
||||
id: "tag-3",
|
||||
value: "Netherlands",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const variantsData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "variant title",
|
||||
sku: "sku 1",
|
||||
product: { id: productsData[0].id },
|
||||
inventory_quantity: 10,
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "variant title",
|
||||
sku: "sku 2",
|
||||
product: { id: productsData[1].id },
|
||||
inventory_quantity: 10,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,146 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
Image,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
|
||||
import ProductOption from "../../../src/models/product-option"
|
||||
|
||||
export * from "./data/create-product"
|
||||
|
||||
export async function createProductAndTags(
|
||||
manager: SqlEntityManager,
|
||||
data: {
|
||||
id?: string
|
||||
title: string
|
||||
status: ProductTypes.ProductStatus
|
||||
tags?: { id: string; value: string }[]
|
||||
collection_id?: string
|
||||
}[]
|
||||
) {
|
||||
const products: any[] = data.map((productData) => {
|
||||
return manager.create(Product, productData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(products)
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
export async function createProductAndTypes(
|
||||
manager: SqlEntityManager,
|
||||
data: {
|
||||
id?: string
|
||||
title: string
|
||||
status: ProductTypes.ProductStatus
|
||||
type?: { id: string; value: string }
|
||||
}[]
|
||||
) {
|
||||
const products: any[] = data.map((productData) => {
|
||||
return manager.create(Product, productData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(products)
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
export async function createProductVariants(
|
||||
manager: SqlEntityManager,
|
||||
data: any[]
|
||||
) {
|
||||
const variants: any[] = data.map((variantsData) => {
|
||||
return manager.create(ProductVariant, variantsData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(variants)
|
||||
|
||||
return variants
|
||||
}
|
||||
|
||||
export async function createCollections(
|
||||
manager: SqlEntityManager,
|
||||
collectionData: {
|
||||
id?: string
|
||||
title: string
|
||||
handle?: string
|
||||
}[]
|
||||
) {
|
||||
const collections: any[] = collectionData.map((collectionData) => {
|
||||
return manager.create(ProductCollection, collectionData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(collections)
|
||||
|
||||
return collections
|
||||
}
|
||||
|
||||
export async function createTypes(
|
||||
manager: SqlEntityManager,
|
||||
typesData: {
|
||||
id?: string
|
||||
value: string
|
||||
}[]
|
||||
) {
|
||||
const types: any[] = typesData.map((typesData) => {
|
||||
return manager.create(ProductType, typesData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(types)
|
||||
|
||||
return types
|
||||
}
|
||||
|
||||
export async function createOptions(
|
||||
manager: SqlEntityManager,
|
||||
optionsData: {
|
||||
id?: string
|
||||
product: { id: string }
|
||||
title: string
|
||||
value?: string
|
||||
values?: {
|
||||
id?: string
|
||||
value: string
|
||||
variant?: { id: string } & any
|
||||
}[]
|
||||
variant?: { id: string } & any
|
||||
}[]
|
||||
) {
|
||||
const options: any[] = optionsData.map((option) => {
|
||||
return manager.create(ProductOption, option)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(options)
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
export async function createImages(
|
||||
manager: SqlEntityManager,
|
||||
imagesData: string[]
|
||||
) {
|
||||
const images: any[] = imagesData.map((img) => {
|
||||
return manager.create(Image, { url: img })
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(images)
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
export async function assignCategoriesToProduct(
|
||||
manager: SqlEntityManager,
|
||||
product: Product,
|
||||
categories: ProductCategory[]
|
||||
) {
|
||||
product.categories.add(categories)
|
||||
|
||||
await manager.persistAndFlush(product)
|
||||
|
||||
return product
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import faker from "faker"
|
||||
|
||||
export const buildProductVariantOnlyData = ({
|
||||
title,
|
||||
sku,
|
||||
barcode,
|
||||
ean,
|
||||
upc,
|
||||
allow_backorder,
|
||||
inventory_quantity,
|
||||
manage_inventory,
|
||||
hs_code,
|
||||
origin_country,
|
||||
mid_code,
|
||||
material,
|
||||
weight,
|
||||
length,
|
||||
height,
|
||||
width,
|
||||
options,
|
||||
metadata,
|
||||
}: Partial<ProductTypes.CreateProductVariantOnlyDTO>) => {
|
||||
return {
|
||||
title: title ?? faker.commerce.productName(),
|
||||
sku: sku ?? faker.commerce.productName(),
|
||||
barcode,
|
||||
ean,
|
||||
upc,
|
||||
allow_backorder,
|
||||
inventory_quantity,
|
||||
manage_inventory,
|
||||
hs_code,
|
||||
origin_country,
|
||||
mid_code,
|
||||
material,
|
||||
weight,
|
||||
length,
|
||||
height,
|
||||
width,
|
||||
options,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./data/create-variant"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,370 @@
|
||||
import { ProductCollectionService } from "@services"
|
||||
|
||||
import { createCollections } from "../../../__fixtures__/product"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
medusaApp,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("Product collection Service", () => {
|
||||
let service: ProductCollectionService
|
||||
|
||||
beforeEach(() => {
|
||||
service = medusaApp.modules["productService"].productCollectionService_
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
const data = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
},
|
||||
{
|
||||
id: "test-3",
|
||||
title: "col 3 extra",
|
||||
},
|
||||
{
|
||||
id: "test-4",
|
||||
title: "col 4 extra",
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(async () => {
|
||||
await createCollections(MikroOrmWrapper.forkManager(), data)
|
||||
})
|
||||
|
||||
it("list product collections", async () => {
|
||||
const productCollectionResults = await service.list()
|
||||
|
||||
expect(productCollectionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-3",
|
||||
title: "col 3 extra",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-4",
|
||||
title: "col 4 extra",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("list product collections by id", async () => {
|
||||
const productCollectionResults = await service.list({
|
||||
id: data![0].id,
|
||||
})
|
||||
|
||||
expect(productCollectionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("list product collections by title matching string", async () => {
|
||||
const productCollectionResults = await service.list({
|
||||
title: "col 3 extra",
|
||||
})
|
||||
|
||||
expect(productCollectionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-3",
|
||||
title: "col 3 extra",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCount", () => {
|
||||
const data = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
},
|
||||
{
|
||||
id: "test-3",
|
||||
title: "col 3 extra",
|
||||
},
|
||||
{
|
||||
id: "test-4",
|
||||
title: "col 4 extra",
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(async () => {
|
||||
await createCollections(MikroOrmWrapper.forkManager(), data)
|
||||
})
|
||||
|
||||
it("should return all collections and count", async () => {
|
||||
const [productCollectionResults, count] = await service.listAndCount()
|
||||
const serialized = JSON.parse(
|
||||
JSON.stringify(productCollectionResults)
|
||||
)
|
||||
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-3",
|
||||
title: "col 3 extra",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-4",
|
||||
title: "col 4 extra",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return count and collections based on filter data", async () => {
|
||||
const [productCollectionResults, count] = await service.listAndCount({
|
||||
id: data![0].id,
|
||||
})
|
||||
const serialized = JSON.parse(
|
||||
JSON.stringify(productCollectionResults)
|
||||
)
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return count and collections based on config data", async () => {
|
||||
const [productCollectionResults, count] = await service.listAndCount(
|
||||
{},
|
||||
{
|
||||
relations: ["products"],
|
||||
select: ["title"],
|
||||
take: 1,
|
||||
skip: 1,
|
||||
}
|
||||
)
|
||||
const serialized = JSON.parse(
|
||||
JSON.stringify(productCollectionResults)
|
||||
)
|
||||
|
||||
expect(count).toEqual(4)
|
||||
expect(serialized).toEqual([
|
||||
{
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
handle: "col-2",
|
||||
products: [],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const collectionData = {
|
||||
id: "collection-1",
|
||||
title: "collection 1",
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await createCollections(MikroOrmWrapper.forkManager(), [
|
||||
collectionData,
|
||||
])
|
||||
})
|
||||
|
||||
it("should return collection for the given id", async () => {
|
||||
const productCollectionResults = await service.retrieve(
|
||||
collectionData.id
|
||||
)
|
||||
|
||||
expect(productCollectionResults).toEqual(
|
||||
expect.objectContaining({
|
||||
id: collectionData.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when collection with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductCollection with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when an id is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"productCollection - id must be defined"
|
||||
)
|
||||
})
|
||||
|
||||
it("should return collection based on config select param", async () => {
|
||||
const productCollectionResults = await service.retrieve(
|
||||
collectionData.id,
|
||||
{
|
||||
select: ["id", "title"],
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(
|
||||
JSON.stringify(productCollectionResults)
|
||||
)
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id: collectionData.id,
|
||||
title: collectionData.title,
|
||||
handle: "collection-1",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return collection based on config relation param", async () => {
|
||||
const productCollectionResults = await service.retrieve(
|
||||
collectionData.id,
|
||||
{
|
||||
select: ["id", "title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(
|
||||
JSON.stringify(productCollectionResults)
|
||||
)
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id: collectionData.id,
|
||||
title: collectionData.title,
|
||||
handle: "collection-1",
|
||||
products: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
const collectionId = "collection-1"
|
||||
const collectionData = {
|
||||
id: collectionId,
|
||||
title: "collection 1",
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await createCollections(MikroOrmWrapper.forkManager(), [
|
||||
collectionData,
|
||||
])
|
||||
})
|
||||
|
||||
it("should delete the product collection given an ID successfully", async () => {
|
||||
await service.delete([collectionId])
|
||||
|
||||
const collections = await service.list({
|
||||
id: collectionId,
|
||||
})
|
||||
|
||||
expect(collections).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const collectionId = "collection-1"
|
||||
const collectionData = {
|
||||
id: collectionId,
|
||||
title: "collection 1",
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await createCollections(MikroOrmWrapper.forkManager(), [
|
||||
collectionData,
|
||||
])
|
||||
})
|
||||
|
||||
it("should update the value of the collection successfully", async () => {
|
||||
await service.update([
|
||||
{
|
||||
id: collectionId,
|
||||
title: "New Collection",
|
||||
},
|
||||
])
|
||||
|
||||
const productCollection = await service.retrieve(collectionId)
|
||||
|
||||
expect(productCollection.title).toEqual("New Collection")
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.update([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
title: "New Collection",
|
||||
},
|
||||
])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'ProductCollection with id "does-not-exist" not found'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a collection successfully", async () => {
|
||||
await service.create([
|
||||
{
|
||||
title: "New Collection",
|
||||
},
|
||||
])
|
||||
|
||||
const [productCollection] = await service.list({
|
||||
title: "New Collection",
|
||||
})
|
||||
|
||||
expect(productCollection.title).toEqual("New Collection")
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,614 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||
import { Product, ProductCategory } from "@models"
|
||||
import {
|
||||
MockEventBusService,
|
||||
SuiteOptions,
|
||||
moduleIntegrationTestRunner,
|
||||
} from "medusa-test-utils"
|
||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||
import { productCategoriesRankData } from "../../../__fixtures__/product-category/data"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
injectedDependencies: {
|
||||
eventBusModuleService: new MockEventBusService(),
|
||||
},
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductModuleService product categories", () => {
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
let productCategoryOne: ProductCategory
|
||||
let productCategoryTwo: ProductCategory
|
||||
let productCategories: ProductCategory[]
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
productTwo = testManager.create(Product, {
|
||||
id: "product-2",
|
||||
title: "product 2",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
const productCategoriesData = [
|
||||
{
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
products: [productOne],
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
name: "category",
|
||||
products: [productTwo],
|
||||
},
|
||||
]
|
||||
|
||||
productCategories = await createProductCategories(
|
||||
testManager,
|
||||
productCategoriesData
|
||||
)
|
||||
|
||||
productCategoryOne = productCategories[0]
|
||||
productCategoryTwo = productCategories[1]
|
||||
|
||||
await testManager.persistAndFlush([
|
||||
productCategoryOne,
|
||||
productCategoryTwo,
|
||||
])
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("listCategories", () => {
|
||||
it("should return categories queried by ID", async () => {
|
||||
const results = await service.listCategories({
|
||||
id: productCategoryOne.id,
|
||||
})
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return categories based on the options and filter parameter", async () => {
|
||||
let results = await service.listCategories(
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listCategories({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for categories", async () => {
|
||||
const results = await service.listCategories(
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "name", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCountCategories", () => {
|
||||
it("should return categories and count queried by ID", async () => {
|
||||
const results = await service.listAndCountCategories({
|
||||
id: productCategoryOne.id,
|
||||
})
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return categories and count based on the options and filter parameter", async () => {
|
||||
let results = await service.listAndCountCategories(
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listAndCountCategories({}, { take: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
|
||||
results = await service.listAndCountCategories(
|
||||
{},
|
||||
{ take: 1, skip: 1 }
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for categories", async () => {
|
||||
const results = await service.listAndCountCategories(
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "name", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveCategory", () => {
|
||||
it("should return the requested category", async () => {
|
||||
const result = await service.retrieveCategory(productCategoryOne.id, {
|
||||
select: ["id", "name"],
|
||||
})
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const result = await service.retrieveCategory(productCategoryOne.id, {
|
||||
select: ["id", "name", "products.title"],
|
||||
relations: ["products"],
|
||||
})
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a category with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveCategory("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductCategory with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createCategory", () => {
|
||||
it("should create a category successfully", async () => {
|
||||
await service.createCategory({
|
||||
name: "New Category",
|
||||
parent_category_id: productCategoryOne.id,
|
||||
})
|
||||
|
||||
const [productCategory] = await service.listCategories(
|
||||
{
|
||||
name: "New Category",
|
||||
},
|
||||
{
|
||||
select: ["name", "rank"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategory).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "New Category",
|
||||
rank: "0",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should emit events through event bus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
|
||||
const category = await service.createCategory({
|
||||
name: "New Category",
|
||||
parent_category_id: productCategoryOne.id,
|
||||
})
|
||||
|
||||
expect(eventBusSpy).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusSpy).toHaveBeenCalledWith("product-category.created", {
|
||||
id: category.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should append rank from an existing category depending on parent", async () => {
|
||||
await service.createCategory({
|
||||
name: "New Category",
|
||||
parent_category_id: productCategoryOne.id,
|
||||
rank: 0,
|
||||
})
|
||||
|
||||
await service.createCategory({
|
||||
name: "New Category 2",
|
||||
parent_category_id: productCategoryOne.id,
|
||||
})
|
||||
|
||||
const [productCategoryNew] = await service.listCategories(
|
||||
{
|
||||
name: "New Category 2",
|
||||
},
|
||||
{
|
||||
select: ["name", "rank"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategoryNew).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "New Category 2",
|
||||
rank: "1",
|
||||
})
|
||||
)
|
||||
|
||||
await service.createCategory({
|
||||
name: "New Category 2.1",
|
||||
parent_category_id: productCategoryNew.id,
|
||||
})
|
||||
|
||||
const [productCategoryWithParent] = await service.listCategories(
|
||||
{
|
||||
name: "New Category 2.1",
|
||||
},
|
||||
{
|
||||
select: ["name", "rank", "parent_category_id"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategoryWithParent).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "New Category 2.1",
|
||||
parent_category_id: productCategoryNew.id,
|
||||
rank: "0",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateCategory", () => {
|
||||
let productCategoryZero
|
||||
let productCategoryOne
|
||||
let productCategoryTwo
|
||||
let productCategoryZeroZero
|
||||
let productCategoryZeroOne
|
||||
let productCategoryZeroTwo
|
||||
let categories
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
categories = await createProductCategories(
|
||||
testManager,
|
||||
productCategoriesRankData
|
||||
)
|
||||
|
||||
productCategoryZero = categories[0]
|
||||
productCategoryOne = categories[1]
|
||||
productCategoryTwo = categories[2]
|
||||
productCategoryZeroZero = categories[3]
|
||||
productCategoryZeroOne = categories[4]
|
||||
productCategoryZeroTwo = categories[5]
|
||||
})
|
||||
|
||||
it("should emit events through event bus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
await service.updateCategory(productCategoryZero.id, {
|
||||
name: "New Category",
|
||||
})
|
||||
|
||||
expect(eventBusSpy).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusSpy).toHaveBeenCalledWith("product-category.updated", {
|
||||
id: productCategoryZero.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should update the name of the category successfully", async () => {
|
||||
await service.updateCategory(productCategoryZero.id, {
|
||||
name: "New Category",
|
||||
})
|
||||
|
||||
const productCategory = await service.retrieveCategory(
|
||||
productCategoryZero.id,
|
||||
{
|
||||
select: ["name"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategory.name).toEqual("New Category")
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.updateCategory("does-not-exist", {
|
||||
name: "New Category",
|
||||
})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
`ProductCategory not found ({ id: 'does-not-exist' })`
|
||||
)
|
||||
})
|
||||
|
||||
it("should reorder rank successfully in the same parent", async () => {
|
||||
await service.updateCategory(productCategoryTwo.id, {
|
||||
rank: 0,
|
||||
})
|
||||
|
||||
const productCategories = await service.listCategories(
|
||||
{
|
||||
parent_category_id: null,
|
||||
},
|
||||
{
|
||||
select: ["name", "rank"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategories).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
rank: "0",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryZero.id,
|
||||
rank: "1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
rank: "2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should reorder rank successfully when changing parent", async () => {
|
||||
await service.updateCategory(productCategoryTwo.id, {
|
||||
rank: 0,
|
||||
parent_category_id: productCategoryZero.id,
|
||||
})
|
||||
|
||||
const productCategories = await service.listCategories(
|
||||
{
|
||||
parent_category_id: productCategoryZero.id,
|
||||
},
|
||||
{
|
||||
select: ["name", "rank"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategories).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
rank: "0",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryZeroZero.id,
|
||||
rank: "1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryZeroOne.id,
|
||||
rank: "2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryZeroTwo.id,
|
||||
rank: "3",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should reorder rank successfully when changing parent and in first position", async () => {
|
||||
await service.updateCategory(productCategoryTwo.id, {
|
||||
rank: 0,
|
||||
parent_category_id: productCategoryZero.id,
|
||||
})
|
||||
|
||||
const productCategories = await service.listCategories(
|
||||
{
|
||||
parent_category_id: productCategoryZero.id,
|
||||
},
|
||||
{
|
||||
select: ["name", "rank"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategories).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
rank: "0",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryZeroZero.id,
|
||||
rank: "1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryZeroOne.id,
|
||||
rank: "2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryZeroTwo.id,
|
||||
rank: "3",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("deleteCategory", () => {
|
||||
let productCategoryZero
|
||||
let productCategoryOne
|
||||
let productCategoryTwo
|
||||
let categories
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
categories = await createProductCategories(
|
||||
testManager,
|
||||
productCategoriesRankData
|
||||
)
|
||||
|
||||
productCategoryZero = categories[0]
|
||||
productCategoryOne = categories[1]
|
||||
productCategoryTwo = categories[2]
|
||||
})
|
||||
|
||||
it("should emit events through event bus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
await service.deleteCategory(productCategoryOne.id)
|
||||
|
||||
expect(eventBusSpy).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusSpy).toHaveBeenCalledWith("product-category.deleted", {
|
||||
id: productCategoryOne.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.deleteCategory("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
`ProductCategory not found ({ id: 'does-not-exist' })`
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when it has children", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.deleteCategory(productCategoryZero.id)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
`Deleting ProductCategory (category-0-0) with category children is not allowed`
|
||||
)
|
||||
})
|
||||
|
||||
it("should reorder siblings rank successfully on deleting", async () => {
|
||||
await service.deleteCategory(productCategoryOne.id)
|
||||
|
||||
const productCategories = await service.listCategories(
|
||||
{
|
||||
parent_category_id: null,
|
||||
},
|
||||
{
|
||||
select: ["id", "rank"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategories).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: productCategoryZero.id,
|
||||
rank: "0",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
rank: "1",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,440 @@
|
||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||
import { Product, ProductCollection } from "@models"
|
||||
import { MockEventBusService } from "medusa-test-utils"
|
||||
import { createCollections } from "../../../__fixtures__/product"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
injectedDependencies: {
|
||||
eventBusModuleService: new MockEventBusService(),
|
||||
},
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductModuleService product collections", () => {
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
let productCollectionOne: ProductCollection
|
||||
let productCollectionTwo: ProductCollection
|
||||
let productCollections: ProductCollection[]
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
productTwo = testManager.create(Product, {
|
||||
id: "product-2",
|
||||
title: "product 2",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
const productCollectionsData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
products: [productOne],
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "collection",
|
||||
products: [productTwo],
|
||||
},
|
||||
]
|
||||
|
||||
productCollections = await createCollections(
|
||||
testManager,
|
||||
productCollectionsData
|
||||
)
|
||||
|
||||
productCollectionOne = productCollections[0]
|
||||
productCollectionTwo = productCollections[1]
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("listCollections", () => {
|
||||
it("should return collections queried by ID", async () => {
|
||||
const results = await service.listCollections({
|
||||
id: productCollectionOne.id,
|
||||
})
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return collections based on the options and filter parameter", async () => {
|
||||
let results = await service.listCollections(
|
||||
{
|
||||
id: productCollectionOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listCollections({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for collections", async () => {
|
||||
const results = await service.listCollections(
|
||||
{
|
||||
id: productCollectionOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCountCollections", () => {
|
||||
it("should return collections and count queried by ID", async () => {
|
||||
const results = await service.listAndCountCollections({
|
||||
id: productCollectionOne.id,
|
||||
})
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return collections and count based on the options and filter parameter", async () => {
|
||||
let results = await service.listAndCountCollections(
|
||||
{
|
||||
id: productCollectionOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listAndCountCollections({}, { take: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
|
||||
results = await service.listAndCountCollections(
|
||||
{},
|
||||
{ take: 1, skip: 1 }
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for collections", async () => {
|
||||
const results = await service.listAndCountCollections(
|
||||
{
|
||||
id: productCollectionOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveCollection", () => {
|
||||
it("should return the requested collection", async () => {
|
||||
const result = await service.retrieveCollection(
|
||||
productCollectionOne.id
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const result = await service.retrieveCollection(
|
||||
productCollectionOne.id,
|
||||
{
|
||||
select: ["id", "title", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a collection with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveCollection("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductCollection with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("deleteCollections", () => {
|
||||
const collectionId = "test-1"
|
||||
|
||||
it("should delete the product collection given an ID successfully", async () => {
|
||||
await service.deleteCollections([collectionId])
|
||||
|
||||
const collections = await service.listCollections({
|
||||
id: collectionId,
|
||||
})
|
||||
|
||||
expect(collections).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should emit events through event bus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
await service.deleteCollections([collectionId])
|
||||
|
||||
expect(eventBusSpy).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
eventName: "product-collection.deleted",
|
||||
data: { id: collectionId },
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateCollections", () => {
|
||||
const collectionId = "test-1"
|
||||
|
||||
it("should emit events through event bus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
|
||||
await service.upsertCollections([
|
||||
{
|
||||
id: collectionId,
|
||||
title: "New Collection",
|
||||
},
|
||||
])
|
||||
|
||||
expect(eventBusSpy).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
eventName: "product-collection.updated",
|
||||
data: { id: collectionId },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should update the value of the collection successfully", async () => {
|
||||
await service.upsertCollections([
|
||||
{
|
||||
id: collectionId,
|
||||
title: "New Collection",
|
||||
},
|
||||
])
|
||||
|
||||
const productCollection = await service.retrieveCollection(
|
||||
collectionId
|
||||
)
|
||||
|
||||
expect(productCollection.title).toEqual("New Collection")
|
||||
})
|
||||
|
||||
it("should add products to a collection successfully", async () => {
|
||||
await service.upsertCollections([
|
||||
{
|
||||
id: collectionId,
|
||||
product_ids: [productOne.id, productTwo.id],
|
||||
},
|
||||
])
|
||||
|
||||
const productCollection = await service.retrieveCollection(
|
||||
collectionId,
|
||||
{
|
||||
select: ["products.id"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCollection.products).toHaveLength(2)
|
||||
expect(productCollection).toEqual(
|
||||
expect.objectContaining({
|
||||
products: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.updateCollections("does-not-exist", {
|
||||
title: "New Collection",
|
||||
})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductCollection with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createCollections", () => {
|
||||
it("should create a collection successfully", async () => {
|
||||
const res = await service.createCollections([
|
||||
{
|
||||
title: "New Collection",
|
||||
},
|
||||
])
|
||||
|
||||
const [productCollection] = await service.listCollections({
|
||||
title: "New Collection",
|
||||
})
|
||||
|
||||
expect(productCollection.title).toEqual("New Collection")
|
||||
})
|
||||
|
||||
it("should create collection with products successfully", async () => {
|
||||
await service.createCollections([
|
||||
{
|
||||
title: "New Collection with products",
|
||||
handle: "new-collection-with-products",
|
||||
product_ids: [productOne.id, productTwo.id],
|
||||
},
|
||||
])
|
||||
|
||||
const [productCollection] = await service.listCollections(
|
||||
{
|
||||
handle: "new-collection-with-products",
|
||||
},
|
||||
{
|
||||
select: ["title", "handle", "products.id"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCollection).toEqual(
|
||||
expect.objectContaining({
|
||||
title: "New Collection with products",
|
||||
handle: "new-collection-with-products",
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should emit events through event bus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
|
||||
const collections = await service.createCollections([
|
||||
{
|
||||
title: "New Collection",
|
||||
},
|
||||
])
|
||||
|
||||
expect(eventBusSpy).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
eventName: "product-collection.created",
|
||||
data: { id: collections[0].id },
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,312 @@
|
||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||
import { Product, ProductOption } from "@models"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductModuleService product options", () => {
|
||||
let optionOne: ProductOption
|
||||
let optionTwo: ProductOption
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
productTwo = testManager.create(Product, {
|
||||
id: "product-2",
|
||||
title: "product 2",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
optionOne = testManager.create(ProductOption, {
|
||||
id: "option-1",
|
||||
title: "option 1",
|
||||
product: productOne,
|
||||
})
|
||||
|
||||
optionTwo = testManager.create(ProductOption, {
|
||||
id: "option-2",
|
||||
title: "option 1",
|
||||
product: productTwo,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([optionOne, optionTwo])
|
||||
})
|
||||
|
||||
describe("listOptions", () => {
|
||||
it("should return options and count queried by ID", async () => {
|
||||
const options = await service.listOptions({
|
||||
id: optionOne.id,
|
||||
})
|
||||
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({
|
||||
id: optionOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return options and count based on the options and filter parameter", async () => {
|
||||
let options = await service.listOptions(
|
||||
{
|
||||
id: optionOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({
|
||||
id: optionOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
options = await service.listOptions({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({
|
||||
id: optionTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for options", async () => {
|
||||
const options = await service.listOptions(
|
||||
{
|
||||
id: optionOne.id,
|
||||
},
|
||||
{
|
||||
select: ["title", "product.id"],
|
||||
relations: ["product"],
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(options).toEqual([
|
||||
{
|
||||
id: optionOne.id,
|
||||
title: optionOne.title,
|
||||
product_id: productOne.id,
|
||||
product: {
|
||||
id: productOne.id,
|
||||
type_id: null,
|
||||
collection_id: null,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCountOptions", () => {
|
||||
it("should return options and count queried by ID", async () => {
|
||||
const [options, count] = await service.listAndCountOptions({
|
||||
id: optionOne.id,
|
||||
})
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({
|
||||
id: optionOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return options and count based on the options and filter parameter", async () => {
|
||||
let [options, count] = await service.listAndCountOptions(
|
||||
{
|
||||
id: optionOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({
|
||||
id: optionOne.id,
|
||||
}),
|
||||
])
|
||||
;[options, count] = await service.listAndCountOptions({}, { take: 1 })
|
||||
|
||||
expect(count).toEqual(2)
|
||||
;[options, count] = await service.listAndCountOptions(
|
||||
{},
|
||||
{ take: 1, skip: 1 }
|
||||
)
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({
|
||||
id: optionTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for options", async () => {
|
||||
const [options, count] = await service.listAndCountOptions(
|
||||
{
|
||||
id: optionOne.id,
|
||||
},
|
||||
{
|
||||
select: ["title", "product.id"],
|
||||
relations: ["product"],
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(options).toEqual([
|
||||
{
|
||||
id: optionOne.id,
|
||||
title: optionOne.title,
|
||||
product_id: productOne.id,
|
||||
product: {
|
||||
id: productOne.id,
|
||||
type_id: null,
|
||||
collection_id: null,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveOption", () => {
|
||||
it("should return the requested option", async () => {
|
||||
const option = await service.retrieveOption(optionOne.id)
|
||||
|
||||
expect(option).toEqual(
|
||||
expect.objectContaining({
|
||||
id: optionOne.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const option = await service.retrieveOption(optionOne.id, {
|
||||
select: ["id", "product.title"],
|
||||
relations: ["product"],
|
||||
})
|
||||
|
||||
expect(option).toEqual(
|
||||
expect.objectContaining({
|
||||
id: optionOne.id,
|
||||
product: {
|
||||
id: "product-1",
|
||||
handle: "product-1",
|
||||
title: "product 1",
|
||||
type_id: null,
|
||||
collection_id: null,
|
||||
},
|
||||
product_id: "product-1",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a option with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveOption("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
`ProductOption with id: does-not-exist was not found`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("deleteOptions", () => {
|
||||
const optionId = "option-1"
|
||||
|
||||
it("should delete the product option given an ID successfully", async () => {
|
||||
await service.deleteOptions([optionId])
|
||||
|
||||
const options = await service.listOptions({
|
||||
id: optionId,
|
||||
})
|
||||
|
||||
expect(options).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateOptions", () => {
|
||||
const optionId = "option-1"
|
||||
|
||||
it("should update the title of the option successfully", async () => {
|
||||
await service.upsertOptions([
|
||||
{
|
||||
id: optionId,
|
||||
title: "new test",
|
||||
},
|
||||
])
|
||||
|
||||
const productOption = await service.retrieveOption(optionId)
|
||||
|
||||
expect(productOption.title).toEqual("new test")
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.updateOptions("does-not-exist", {})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
`ProductOption with id: does-not-exist was not found`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createOptions", () => {
|
||||
it("should create a option successfully", async () => {
|
||||
const res = await service.createOptions([
|
||||
{
|
||||
title: "test",
|
||||
values: [],
|
||||
product_id: productOne.id,
|
||||
},
|
||||
])
|
||||
|
||||
const [productOption] = await service.listOptions(
|
||||
{
|
||||
title: "test",
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "product.id"],
|
||||
relations: ["product"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productOption).toEqual(
|
||||
expect.objectContaining({
|
||||
title: "test",
|
||||
product: expect.objectContaining({
|
||||
id: productOne.id,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,302 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||
import { Product, ProductTag } from "@models"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductModuleService product tags", () => {
|
||||
let tagOne: ProductTag
|
||||
let tagTwo: ProductTag
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
productTwo = testManager.create(Product, {
|
||||
id: "product-2",
|
||||
title: "product 2",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
tagOne = testManager.create(ProductTag, {
|
||||
id: "tag-1",
|
||||
value: "tag 1",
|
||||
products: [productOne],
|
||||
})
|
||||
|
||||
tagTwo = testManager.create(ProductTag, {
|
||||
id: "tag-2",
|
||||
value: "tag",
|
||||
products: [productTwo],
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([tagOne, tagTwo])
|
||||
})
|
||||
|
||||
describe("listTags", () => {
|
||||
it("should return tags and count queried by ID", async () => {
|
||||
const tags = await service.listTags({
|
||||
id: tagOne.id,
|
||||
})
|
||||
|
||||
expect(tags).toEqual([
|
||||
expect.objectContaining({
|
||||
id: tagOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return tags and count based on the options and filter parameter", async () => {
|
||||
let tags = await service.listTags(
|
||||
{
|
||||
id: tagOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(tags).toEqual([
|
||||
expect.objectContaining({
|
||||
id: tagOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
tags = await service.listTags({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(tags).toEqual([
|
||||
expect.objectContaining({
|
||||
id: tagTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for tags", async () => {
|
||||
const tags = await service.listTags(
|
||||
{
|
||||
id: tagOne.id,
|
||||
},
|
||||
{
|
||||
select: ["value", "products.id"],
|
||||
relations: ["products"],
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(tags).toEqual([
|
||||
{
|
||||
id: tagOne.id,
|
||||
value: tagOne.value,
|
||||
products: [
|
||||
{
|
||||
collection_id: null,
|
||||
type_id: null,
|
||||
id: productOne.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCountTags", () => {
|
||||
it("should return tags and count queried by ID", async () => {
|
||||
const [tags, count] = await service.listAndCountTags({
|
||||
id: tagOne.id,
|
||||
})
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(tags).toEqual([
|
||||
expect.objectContaining({
|
||||
id: tagOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return tags and count based on the options and filter parameter", async () => {
|
||||
let [tags, count] = await service.listAndCountTags(
|
||||
{
|
||||
id: tagOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(tags).toEqual([
|
||||
expect.objectContaining({
|
||||
id: tagOne.id,
|
||||
}),
|
||||
])
|
||||
;[tags, count] = await service.listAndCountTags({}, { take: 1 })
|
||||
|
||||
expect(count).toEqual(2)
|
||||
;[tags, count] = await service.listAndCountTags(
|
||||
{},
|
||||
{ take: 1, skip: 1 }
|
||||
)
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(tags).toEqual([
|
||||
expect.objectContaining({
|
||||
id: tagTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for tags", async () => {
|
||||
const [tags, count] = await service.listAndCountTags(
|
||||
{
|
||||
id: tagOne.id,
|
||||
},
|
||||
{
|
||||
select: ["value", "products.id"],
|
||||
relations: ["products"],
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(tags).toEqual([
|
||||
{
|
||||
id: tagOne.id,
|
||||
value: tagOne.value,
|
||||
products: [
|
||||
{
|
||||
collection_id: null,
|
||||
type_id: null,
|
||||
id: productOne.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveTag", () => {
|
||||
it("should return the requested tag", async () => {
|
||||
const tag = await service.retrieveTag(tagOne.id)
|
||||
|
||||
expect(tag).toEqual(
|
||||
expect.objectContaining({
|
||||
id: tagOne.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const tag = await service.retrieveTag(tagOne.id, {
|
||||
select: ["id", "value", "products.title"],
|
||||
relations: ["products"],
|
||||
})
|
||||
|
||||
expect(tag).toEqual(
|
||||
expect.objectContaining({
|
||||
id: tagOne.id,
|
||||
value: tagOne.value,
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
title: "product 1",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a tag with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveTag("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductTag with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("deleteTags", () => {
|
||||
const tagId = "tag-1"
|
||||
|
||||
it("should delete the product tag given an ID successfully", async () => {
|
||||
await service.deleteTags([tagId])
|
||||
|
||||
const tags = await service.listTags({
|
||||
id: tagId,
|
||||
})
|
||||
|
||||
expect(tags).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateTags", () => {
|
||||
const tagId = "tag-1"
|
||||
|
||||
it("should update the value of the tag successfully", async () => {
|
||||
await service.updateTags([
|
||||
{
|
||||
id: tagId,
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
|
||||
const productTag = await service.retrieveTag(tagId)
|
||||
|
||||
expect(productTag.value).toEqual("UK")
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.updateTags([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'ProductTag with id "does-not-exist" not found'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createTags", () => {
|
||||
it("should create a tag successfully", async () => {
|
||||
const res = await service.createTags([
|
||||
{
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
|
||||
const productTag = await service.listTags({
|
||||
value: "UK",
|
||||
})
|
||||
|
||||
expect(productTag[0]?.value).toEqual("UK")
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,257 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ProductType } from "@models"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductModuleService product types", () => {
|
||||
let typeOne: ProductType
|
||||
let typeTwo: ProductType
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
typeOne = testManager.create(ProductType, {
|
||||
id: "type-1",
|
||||
value: "type 1",
|
||||
})
|
||||
|
||||
typeTwo = testManager.create(ProductType, {
|
||||
id: "type-2",
|
||||
value: "type",
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([typeOne, typeTwo])
|
||||
})
|
||||
|
||||
describe("listTypes", () => {
|
||||
it("should return types and count queried by ID", async () => {
|
||||
const types = await service.listTypes({
|
||||
id: typeOne.id,
|
||||
})
|
||||
|
||||
expect(types).toEqual([
|
||||
expect.objectContaining({
|
||||
id: typeOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return types and count based on the options and filter parameter", async () => {
|
||||
let types = await service.listTypes(
|
||||
{
|
||||
id: typeOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(types).toEqual([
|
||||
expect.objectContaining({
|
||||
id: typeOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
types = await service.listTypes({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(types).toEqual([
|
||||
expect.objectContaining({
|
||||
id: typeTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields for types", async () => {
|
||||
const types = await service.listTypes(
|
||||
{
|
||||
id: typeOne.id,
|
||||
},
|
||||
{
|
||||
select: ["value"],
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(types).toEqual([
|
||||
{
|
||||
id: typeOne.id,
|
||||
value: typeOne.value,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCountTypes", () => {
|
||||
it("should return types and count queried by ID", async () => {
|
||||
const [types, count] = await service.listAndCountTypes({
|
||||
id: typeOne.id,
|
||||
})
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(types).toEqual([
|
||||
expect.objectContaining({
|
||||
id: typeOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return types and count based on the options and filter parameter", async () => {
|
||||
let [types, count] = await service.listAndCountTypes(
|
||||
{
|
||||
id: typeOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(types).toEqual([
|
||||
expect.objectContaining({
|
||||
id: typeOne.id,
|
||||
}),
|
||||
])
|
||||
;[types, count] = await service.listAndCountTypes({}, { take: 1 })
|
||||
|
||||
expect(count).toEqual(2)
|
||||
;[types, count] = await service.listAndCountTypes(
|
||||
{},
|
||||
{ take: 1, skip: 1 }
|
||||
)
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(types).toEqual([
|
||||
expect.objectContaining({
|
||||
id: typeTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields for types", async () => {
|
||||
const [types, count] = await service.listAndCountTypes(
|
||||
{
|
||||
id: typeOne.id,
|
||||
},
|
||||
{
|
||||
select: ["value"],
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(types).toEqual([
|
||||
{
|
||||
id: typeOne.id,
|
||||
value: typeOne.value,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveType", () => {
|
||||
it("should return the requested type", async () => {
|
||||
const type = await service.retrieveType(typeOne.id)
|
||||
|
||||
expect(type).toEqual(
|
||||
expect.objectContaining({
|
||||
id: typeOne.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const type = await service.retrieveType(typeOne.id, {
|
||||
select: ["id", "value"],
|
||||
})
|
||||
|
||||
expect(type).toEqual({
|
||||
id: typeOne.id,
|
||||
value: typeOne.value,
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw an error when a type with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveType("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductType with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("deleteTypes", () => {
|
||||
const typeId = "type-1"
|
||||
|
||||
it("should delete the product type given an ID successfully", async () => {
|
||||
await service.deleteTypes([typeId])
|
||||
|
||||
const types = await service.listTypes({
|
||||
id: typeId,
|
||||
})
|
||||
|
||||
expect(types).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateTypes", () => {
|
||||
const typeId = "type-1"
|
||||
|
||||
it("should update the value of the type successfully", async () => {
|
||||
await service.updateTypes(typeId, {
|
||||
value: "UK",
|
||||
})
|
||||
|
||||
const productType = await service.retrieveType(typeId)
|
||||
|
||||
expect(productType.value).toEqual("UK")
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.updateTypes("does-not-exist", {
|
||||
value: "UK",
|
||||
})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductType with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createTypes", () => {
|
||||
it("should create a type successfully", async () => {
|
||||
const res = await service.createTypes([
|
||||
{
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
|
||||
const productType = await service.listTypes({
|
||||
value: "UK",
|
||||
})
|
||||
|
||||
expect(productType[0]?.value).toEqual("UK")
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,273 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||
import { Product, ProductVariant } from "@models"
|
||||
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductModuleService product variants", () => {
|
||||
let variantOne: ProductVariant
|
||||
let variantTwo: ProductVariant
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
|
||||
beforeEach(async () => {
|
||||
productOne = await service.create({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
options: [
|
||||
{
|
||||
title: "size",
|
||||
values: ["large", "small"],
|
||||
},
|
||||
{
|
||||
title: "color",
|
||||
values: ["red", "blue"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
productTwo = await service.create({
|
||||
id: "product-2",
|
||||
title: "product 2",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
variantOne = await service.createVariants({
|
||||
id: "test-1",
|
||||
title: "variant 1",
|
||||
inventory_quantity: 10,
|
||||
product_id: productOne.id,
|
||||
options: { size: "large" },
|
||||
})
|
||||
|
||||
variantTwo = await service.createVariants({
|
||||
id: "test-2",
|
||||
title: "variant",
|
||||
inventory_quantity: 10,
|
||||
product_id: productTwo.id,
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCountVariants", () => {
|
||||
it("should return variants and count queried by ID", async () => {
|
||||
const results = await service.listAndCountVariants({
|
||||
id: variantOne.id,
|
||||
})
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: variantOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return variants and count based on the options and filter parameter", async () => {
|
||||
let results = await service.listAndCountVariants(
|
||||
{
|
||||
id: variantOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: variantOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listAndCountVariants({}, { take: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
|
||||
results = await service.listAndCountVariants({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: variantTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for variants", async () => {
|
||||
const results = await service.listAndCountVariants(
|
||||
{
|
||||
id: variantOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "product.title"] as any,
|
||||
relations: ["product"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "variant 1",
|
||||
// TODO: investigate why this is returning more than the expected results
|
||||
product: expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveVariant", () => {
|
||||
it("should return the requested variant", async () => {
|
||||
const result = await service.retrieveVariant(variantOne.id)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "variant 1",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const result = await service.retrieveVariant(variantOne.id, {
|
||||
select: ["id", "title", "product.title"] as any,
|
||||
relations: ["product"],
|
||||
})
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "variant 1",
|
||||
product: expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a variant with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveVariant("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductVariant with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateVariants", () => {
|
||||
it("should update the title of the variant successfully", async () => {
|
||||
await service.upsertVariants([
|
||||
{
|
||||
id: variantOne.id,
|
||||
title: "new test",
|
||||
},
|
||||
])
|
||||
|
||||
const productVariant = await service.retrieveVariant(variantOne.id)
|
||||
expect(productVariant.title).toEqual("new test")
|
||||
})
|
||||
|
||||
it("should upsert the options of a variant successfully", async () => {
|
||||
await service.upsertVariants([
|
||||
{
|
||||
id: variantOne.id,
|
||||
options: { size: "small" },
|
||||
},
|
||||
])
|
||||
|
||||
const productVariant = await service.retrieveVariant(variantOne.id, {
|
||||
relations: ["options"],
|
||||
})
|
||||
expect(productVariant.options).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "small",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should do a partial update on the options of a variant successfully", async () => {
|
||||
await service.updateVariants(variantOne.id, {
|
||||
options: { size: "small", color: "red" },
|
||||
})
|
||||
|
||||
const productVariant = await service.retrieveVariant(variantOne.id, {
|
||||
relations: ["options"],
|
||||
})
|
||||
|
||||
expect(productVariant.options).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "small",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: "red",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.updateVariants("does-not-exist", {})
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
`Cannot update non-existing variants with ids: does-not-exist`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("softDelete variant", () => {
|
||||
it("should soft delete a variant and its relations", async () => {
|
||||
const beforeDeletedVariants = await service.listVariants(
|
||||
{ id: variantOne.id },
|
||||
{
|
||||
relations: ["options"],
|
||||
}
|
||||
)
|
||||
|
||||
await service.softDeleteVariants([variantOne.id])
|
||||
const deletedVariants = await service.listVariants(
|
||||
{ id: variantOne.id },
|
||||
{
|
||||
relations: ["options"],
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deletedVariants).toHaveLength(1)
|
||||
expect(deletedVariants[0].deleted_at).not.toBeNull()
|
||||
|
||||
for (const variantOption of deletedVariants[0].options) {
|
||||
expect(variantOption?.deleted_at).toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,869 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||
import { kebabCase } from "@medusajs/utils"
|
||||
import {
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
|
||||
import {
|
||||
MockEventBusService,
|
||||
moduleIntegrationTestRunner,
|
||||
SuiteOptions,
|
||||
} from "medusa-test-utils"
|
||||
import { createCollections, createTypes } from "../../../__fixtures__/product"
|
||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||
import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product"
|
||||
import { UpdateProductInput } from "@types"
|
||||
|
||||
jest.setTimeout(300000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
injectedDependencies: {
|
||||
eventBusModuleService: new MockEventBusService(),
|
||||
},
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductModuleService products", function () {
|
||||
let productCollectionOne: ProductCollection
|
||||
let productCollectionTwo: ProductCollection
|
||||
|
||||
const productCollectionsData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
},
|
||||
]
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("update", function () {
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
let productCategoryOne: ProductCategory
|
||||
let productCategoryTwo: ProductCategory
|
||||
let productTypeOne: ProductType
|
||||
let productTypeTwo: ProductType
|
||||
let images = [{ url: "image-1" }]
|
||||
|
||||
const productCategoriesData = [
|
||||
{
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
name: "category 2",
|
||||
},
|
||||
]
|
||||
|
||||
const productTypesData = [
|
||||
{
|
||||
id: "type-1",
|
||||
value: "type 1",
|
||||
},
|
||||
{
|
||||
id: "type-2",
|
||||
value: "type 2",
|
||||
},
|
||||
]
|
||||
|
||||
const tagsData = [
|
||||
{
|
||||
id: "tag-1",
|
||||
value: "tag 1",
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = MikroOrmWrapper.forkManager()
|
||||
|
||||
const collections = await createCollections(
|
||||
testManager,
|
||||
productCollectionsData
|
||||
)
|
||||
|
||||
productCollectionOne = collections[0]
|
||||
productCollectionTwo = collections[1]
|
||||
|
||||
const types = await createTypes(testManager, productTypesData)
|
||||
|
||||
productTypeOne = types[0]
|
||||
productTypeTwo = types[1]
|
||||
|
||||
const categories = await createProductCategories(
|
||||
testManager,
|
||||
productCategoriesData
|
||||
)
|
||||
|
||||
productCategoryOne = categories[0]
|
||||
productCategoryTwo = categories[1]
|
||||
|
||||
productOne = service.create({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
variants: [
|
||||
{
|
||||
id: "variant-1",
|
||||
title: "variant 1",
|
||||
inventory_quantity: 10,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
productTwo = service.create({
|
||||
id: "product-2",
|
||||
title: "product 2",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
categories: [{ id: productCategoryOne.id }],
|
||||
collection_id: productCollectionOne.id,
|
||||
tags: tagsData,
|
||||
options: [
|
||||
{
|
||||
title: "size",
|
||||
values: ["large", "small"],
|
||||
},
|
||||
{
|
||||
title: "color",
|
||||
values: ["red", "blue"],
|
||||
},
|
||||
],
|
||||
variants: [
|
||||
{
|
||||
id: "variant-2",
|
||||
title: "variant 2",
|
||||
inventory_quantity: 10,
|
||||
},
|
||||
{
|
||||
id: "variant-3",
|
||||
title: "variant 3",
|
||||
inventory_quantity: 10,
|
||||
options: {
|
||||
size: "small",
|
||||
color: "red",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const res = await Promise.all([productOne, productTwo])
|
||||
productOne = res[0]
|
||||
productTwo = res[1]
|
||||
})
|
||||
|
||||
it("should update a product and upsert relations that are not created yet", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const variantTitle = data.variants[0].title
|
||||
|
||||
const productBefore = (await service.retrieve(productOne.id, {
|
||||
relations: [
|
||||
"images",
|
||||
"variants",
|
||||
"options",
|
||||
"options.values",
|
||||
"variants.options",
|
||||
"tags",
|
||||
"type",
|
||||
],
|
||||
})) as unknown as UpdateProductInput
|
||||
|
||||
productBefore.title = "updated title"
|
||||
productBefore.variants = [
|
||||
...productBefore.variants!,
|
||||
...data.variants,
|
||||
]
|
||||
productBefore.options = data.options
|
||||
productBefore.images = data.images
|
||||
productBefore.thumbnail = data.thumbnail
|
||||
productBefore.tags = data.tags
|
||||
const updatedProducts = await service.upsert([productBefore])
|
||||
expect(updatedProducts).toHaveLength(1)
|
||||
|
||||
const product = await service.retrieve(productBefore.id, {
|
||||
relations: [
|
||||
"images",
|
||||
"variants",
|
||||
"options",
|
||||
"options.values",
|
||||
"variants.options",
|
||||
"tags",
|
||||
"type",
|
||||
],
|
||||
})
|
||||
|
||||
const createdVariant = product.variants.find(
|
||||
(v) => v.title === variantTitle
|
||||
)!
|
||||
|
||||
expect(product.images).toHaveLength(1)
|
||||
expect(createdVariant?.options).toHaveLength(1)
|
||||
expect(product.tags).toHaveLength(1)
|
||||
expect(product.variants).toHaveLength(2)
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "updated title",
|
||||
description: productBefore.description,
|
||||
subtitle: productBefore.subtitle,
|
||||
is_giftcard: productBefore.is_giftcard,
|
||||
discountable: productBefore.discountable,
|
||||
thumbnail: images[0].url,
|
||||
status: productBefore.status,
|
||||
images: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
url: images[0].url,
|
||||
}),
|
||||
]),
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: productBefore.options?.[0].title,
|
||||
values: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.options[0].values[0],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
tags: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: productBefore.tags?.[0].value,
|
||||
}),
|
||||
]),
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: createdVariant.title,
|
||||
sku: createdVariant.sku,
|
||||
allow_backorder: false,
|
||||
manage_inventory: true,
|
||||
inventory_quantity: 100,
|
||||
variant_rank: 0,
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.options[0].values[0],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should emit events through event bus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const updateData = {
|
||||
...data,
|
||||
options: data.options,
|
||||
id: productOne.id,
|
||||
title: "updated title",
|
||||
}
|
||||
|
||||
await service.upsert([updateData])
|
||||
|
||||
expect(eventBusSpy).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
eventName: "product.updated",
|
||||
data: { id: productOne.id },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should add relationships to a product", async () => {
|
||||
const updateData = {
|
||||
id: productOne.id,
|
||||
categories: [
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
],
|
||||
collection_id: productCollectionOne.id,
|
||||
type_id: productTypeOne.id,
|
||||
}
|
||||
|
||||
await service.upsert([updateData])
|
||||
|
||||
const product = await service.retrieve(updateData.id, {
|
||||
relations: ["categories", "collection", "type"],
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
categories: [
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
],
|
||||
collection: expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
type: expect.objectContaining({
|
||||
id: productTypeOne.id,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should upsert a product type when type object is passed", async () => {
|
||||
let updateData = {
|
||||
id: productTwo.id,
|
||||
type_id: productTypeOne.id,
|
||||
}
|
||||
|
||||
await service.upsert([updateData])
|
||||
|
||||
let product = await service.retrieve(updateData.id, {
|
||||
relations: ["type"],
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
type: expect.objectContaining({
|
||||
id: productTypeOne.id,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should replace relationships of a product", async () => {
|
||||
const newTagData = {
|
||||
id: "tag-2",
|
||||
value: "tag 2",
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
id: productTwo.id,
|
||||
categories: [
|
||||
{
|
||||
id: productCategoryTwo.id,
|
||||
},
|
||||
],
|
||||
collection_id: productCollectionTwo.id,
|
||||
type_id: productTypeTwo.id,
|
||||
tags: [newTagData],
|
||||
}
|
||||
|
||||
await service.upsert([updateData])
|
||||
|
||||
const product = await service.retrieve(updateData.id, {
|
||||
relations: ["categories", "collection", "tags", "type"],
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
categories: [
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
}),
|
||||
],
|
||||
collection: expect.objectContaining({
|
||||
id: productCollectionTwo.id,
|
||||
}),
|
||||
tags: [
|
||||
expect.objectContaining({
|
||||
id: newTagData.id,
|
||||
value: newTagData.value,
|
||||
}),
|
||||
],
|
||||
type: expect.objectContaining({
|
||||
id: productTypeTwo.id,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should remove relationships of a product", async () => {
|
||||
const updateData = {
|
||||
id: productTwo.id,
|
||||
categories: [],
|
||||
collection_id: null,
|
||||
type_id: null,
|
||||
tags: [],
|
||||
}
|
||||
|
||||
await service.upsert([updateData])
|
||||
|
||||
const product = await service.retrieve(updateData.id, {
|
||||
relations: ["categories", "collection", "tags"],
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
categories: [],
|
||||
tags: [],
|
||||
collection: null,
|
||||
type: null,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when product ID does not exist", async () => {
|
||||
let error
|
||||
try {
|
||||
await service.update("does-not-exist", { title: "test" })
|
||||
} catch (e) {
|
||||
error = e.message
|
||||
}
|
||||
|
||||
expect(error).toEqual(`Product with id: does-not-exist was not found`)
|
||||
})
|
||||
|
||||
it("should update, create and delete variants", async () => {
|
||||
const updateData = {
|
||||
id: productTwo.id,
|
||||
// Note: VariantThree is already assigned to productTwo, that should be deleted
|
||||
variants: [
|
||||
{
|
||||
id: productTwo.variants[0].id,
|
||||
title: "updated-variant",
|
||||
},
|
||||
{
|
||||
title: "created-variant",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await service.upsert([updateData])
|
||||
|
||||
const product = await service.retrieve(updateData.id, {
|
||||
relations: ["variants"],
|
||||
})
|
||||
|
||||
expect(product.variants).toHaveLength(2)
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: productTwo.variants[0].id,
|
||||
title: "updated-variant",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "created-variant",
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should do a partial update on the options of a variant successfully", async () => {
|
||||
await service.update(productTwo.id, {
|
||||
variants: [
|
||||
{
|
||||
id: "variant-3",
|
||||
options: { size: "small", color: "blue" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const fetchedProduct = await service.retrieve(productTwo.id, {
|
||||
relations: ["variants", "variants.options"],
|
||||
})
|
||||
|
||||
expect(fetchedProduct.variants[0].options).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "small",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: "blue",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should createa variant with id that was passed if it does not exist", async () => {
|
||||
const updateData = {
|
||||
id: productTwo.id,
|
||||
// Note: VariantThree is already assigned to productTwo, that should be deleted
|
||||
variants: [
|
||||
{
|
||||
id: "passed-id",
|
||||
title: "updated-variant",
|
||||
},
|
||||
{
|
||||
title: "created-variant",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await service.upsert([updateData])
|
||||
const retrieved = await service.retrieve(updateData.id, {
|
||||
relations: ["variants"],
|
||||
})
|
||||
|
||||
expect(retrieved.variants).toHaveLength(2)
|
||||
expect(retrieved.variants).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "passed-id",
|
||||
title: "updated-variant",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "created-variant",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", function () {
|
||||
let images = [{ url: "image-1" }]
|
||||
it("should create a product", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const productsCreated = await service.create([data])
|
||||
|
||||
const products = await service.list(
|
||||
{ id: productsCreated[0].id },
|
||||
{
|
||||
relations: [
|
||||
"images",
|
||||
"categories",
|
||||
"variants",
|
||||
"variants.options",
|
||||
"options",
|
||||
"options.values",
|
||||
"tags",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
expect(products).toHaveLength(1)
|
||||
expect(products[0].images).toHaveLength(1)
|
||||
expect(products[0].options).toHaveLength(1)
|
||||
expect(products[0].tags).toHaveLength(1)
|
||||
expect(products[0].categories).toHaveLength(0)
|
||||
expect(products[0].variants).toHaveLength(1)
|
||||
|
||||
expect(products[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.title,
|
||||
handle: kebabCase(data.title),
|
||||
description: data.description,
|
||||
subtitle: data.subtitle,
|
||||
is_giftcard: data.is_giftcard,
|
||||
discountable: data.discountable,
|
||||
thumbnail: images[0].url,
|
||||
status: data.status,
|
||||
images: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
url: images[0].url,
|
||||
}),
|
||||
]),
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.options[0].title,
|
||||
values: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.options[0].values[0],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
tags: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.tags[0].value,
|
||||
}),
|
||||
]),
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.variants[0].title,
|
||||
sku: data.variants[0].sku,
|
||||
allow_backorder: false,
|
||||
manage_inventory: true,
|
||||
inventory_quantity: 100,
|
||||
variant_rank: 0,
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.options[0].values[0],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should emit events through eventBus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
expect(eventBusSpy).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
eventName: "product.created",
|
||||
data: { id: products[0].id },
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("softDelete", function () {
|
||||
let images = [{ url: "image-1" }]
|
||||
it("should soft delete a product and its cascaded relations", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
|
||||
await service.softDelete([products[0].id])
|
||||
|
||||
const deletedProducts = await service.list(
|
||||
{ id: products[0].id },
|
||||
{
|
||||
relations: [
|
||||
"variants",
|
||||
"variants.options",
|
||||
"options",
|
||||
"options.values",
|
||||
],
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deletedProducts).toHaveLength(1)
|
||||
expect(deletedProducts[0].deleted_at).not.toBeNull()
|
||||
|
||||
for (const option of deletedProducts[0].options) {
|
||||
expect(option.deleted_at).not.toBeNull()
|
||||
}
|
||||
|
||||
const productOptionsValues = deletedProducts[0].options
|
||||
.map((o) => o.values)
|
||||
.flat()
|
||||
|
||||
for (const optionValue of productOptionsValues) {
|
||||
expect(optionValue.deleted_at).not.toBeNull()
|
||||
}
|
||||
|
||||
for (const variant of deletedProducts[0].variants) {
|
||||
expect(variant.deleted_at).not.toBeNull()
|
||||
}
|
||||
|
||||
const variantsOptions = deletedProducts[0].options
|
||||
.map((o) => o.values)
|
||||
.flat()
|
||||
|
||||
for (const option of variantsOptions) {
|
||||
expect(option.deleted_at).not.toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it("should retrieve soft-deleted products if filtered on deleted_at", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
|
||||
await service.softDelete([products[0].id])
|
||||
|
||||
const softDeleted = await service.list({
|
||||
deleted_at: { $gt: "01-01-2022" },
|
||||
})
|
||||
|
||||
expect(softDeleted).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should emit events through eventBus", async () => {
|
||||
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
|
||||
await service.softDelete([products[0].id])
|
||||
|
||||
expect(eventBusSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
eventName: "product.created",
|
||||
data: { id: products[0].id },
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("restore", function () {
|
||||
let images = [{ url: "image-1" }]
|
||||
|
||||
it("should restore a soft deleted product and its cascaded relations", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
|
||||
let retrievedProducts = await service.list({ id: products[0].id })
|
||||
|
||||
expect(retrievedProducts).toHaveLength(1)
|
||||
expect(retrievedProducts[0].deleted_at).toBeNull()
|
||||
|
||||
await service.softDelete([products[0].id])
|
||||
|
||||
retrievedProducts = await service.list(
|
||||
{ id: products[0].id },
|
||||
{
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(retrievedProducts).toHaveLength(1)
|
||||
expect(retrievedProducts[0].deleted_at).not.toBeNull()
|
||||
|
||||
await service.restore([products[0].id])
|
||||
|
||||
const deletedProducts = await service.list(
|
||||
{ id: products[0].id },
|
||||
{
|
||||
relations: [
|
||||
"variants",
|
||||
"variants.options",
|
||||
"options",
|
||||
"options.values",
|
||||
],
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deletedProducts).toHaveLength(1)
|
||||
expect(deletedProducts[0].deleted_at).toBeNull()
|
||||
|
||||
for (const option of deletedProducts[0].options) {
|
||||
expect(option.deleted_at).toBeNull()
|
||||
}
|
||||
|
||||
const productOptionsValues = deletedProducts[0].options
|
||||
.map((o) => o.values)
|
||||
.flat()
|
||||
|
||||
for (const optionValue of productOptionsValues) {
|
||||
expect(optionValue.deleted_at).toBeNull()
|
||||
}
|
||||
|
||||
for (const variant of deletedProducts[0].variants) {
|
||||
expect(variant.deleted_at).toBeNull()
|
||||
}
|
||||
|
||||
const variantsOptions = deletedProducts[0].options
|
||||
.map((o) => o.values)
|
||||
.flat()
|
||||
|
||||
for (const option of variantsOptions) {
|
||||
expect(option.deleted_at).toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("list", function () {
|
||||
beforeEach(async () => {
|
||||
const collections = await createCollections(
|
||||
MikroOrmWrapper.forkManager(),
|
||||
productCollectionsData
|
||||
)
|
||||
|
||||
productCollectionOne = collections[0]
|
||||
productCollectionTwo = collections[1]
|
||||
|
||||
const productOneData = buildProductAndRelationsData({
|
||||
collection_id: productCollectionOne.id,
|
||||
})
|
||||
|
||||
const productTwoData = buildProductAndRelationsData({
|
||||
collection_id: productCollectionTwo.id,
|
||||
tags: [],
|
||||
})
|
||||
|
||||
await service.create([productOneData, productTwoData])
|
||||
})
|
||||
|
||||
it("should return a list of products scoped by collection id", async () => {
|
||||
const productsWithCollectionOne = await service.list(
|
||||
{ collection_id: productCollectionOne.id },
|
||||
{
|
||||
relations: ["collection"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productsWithCollectionOne).toHaveLength(1)
|
||||
|
||||
expect([
|
||||
expect.objectContaining({
|
||||
collection: expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return empty array when querying for a collection that doesnt exist", async () => {
|
||||
const products = await service.list(
|
||||
{
|
||||
categories: { id: ["collection-doesnt-exist-id"] },
|
||||
},
|
||||
{
|
||||
select: ["title", "collection.title"],
|
||||
relations: ["collection"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(products).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,371 @@
|
||||
import { ProductOptionService } from "@services"
|
||||
import { Product } from "@models"
|
||||
|
||||
import { createOptions } from "../../../__fixtures__/product"
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
medusaApp,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductOption Service", () => {
|
||||
let service: ProductOptionService
|
||||
|
||||
beforeEach(() => {
|
||||
service = medusaApp.modules["productService"].productOptionService_
|
||||
})
|
||||
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
|
||||
const productOneData = {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
}
|
||||
|
||||
const productTwoData = {
|
||||
id: "product-2",
|
||||
title: "product 2",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = MikroOrmWrapper.forkManager()
|
||||
productOne = testManager.create(Product, productOneData)
|
||||
productTwo = testManager.create(Product, productTwoData)
|
||||
|
||||
await createOptions(testManager, [
|
||||
{
|
||||
id: "option-1",
|
||||
title: "Option 1",
|
||||
product: productOne,
|
||||
},
|
||||
{
|
||||
id: "option-2",
|
||||
title: "Option 2",
|
||||
product: productOne,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
it("list product option", async () => {
|
||||
const optionResults = await service.list()
|
||||
|
||||
expect(optionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "option-1",
|
||||
title: "Option 1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "option-2",
|
||||
title: "Option 2",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("list product option by id", async () => {
|
||||
const optionResults = await service.list({ id: "option-2" })
|
||||
|
||||
expect(optionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "option-2",
|
||||
title: "Option 2",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("list product option by title matching string", async () => {
|
||||
const optionResults = await service.list({ title: "Option 1" })
|
||||
|
||||
expect(optionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "option-1",
|
||||
title: "Option 1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCount", () => {
|
||||
it("should return product option and count", async () => {
|
||||
const [optionResults, count] = await service.listAndCount()
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(optionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "option-1",
|
||||
title: "Option 1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "option-2",
|
||||
title: "Option 2",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return product option and count when filtered", async () => {
|
||||
const [optionResults, count] = await service.listAndCount({
|
||||
id: "option-2",
|
||||
})
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(optionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "option-2",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return product option and count when using skip and take", async () => {
|
||||
const [optionResults, count] = await service.listAndCount(
|
||||
{},
|
||||
{ skip: 1, take: 1 }
|
||||
)
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(optionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "option-2",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return requested fields", async () => {
|
||||
const [optionResults, count] = await service.listAndCount(
|
||||
{},
|
||||
{
|
||||
take: 1,
|
||||
select: ["title"],
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(optionResults))
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "option-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const optionId = "option-1"
|
||||
const optionValue = "Option 1"
|
||||
|
||||
it("should return option for the given id", async () => {
|
||||
const option = await service.retrieve(optionId)
|
||||
|
||||
expect(option).toEqual(
|
||||
expect.objectContaining({
|
||||
id: optionId,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when option with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductOption with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when an id is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("productOption - id must be defined")
|
||||
})
|
||||
|
||||
it("should return option based on config select param", async () => {
|
||||
const option = await service.retrieve(optionId, {
|
||||
select: ["id", "title"],
|
||||
})
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(option))
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id: optionId,
|
||||
title: optionValue,
|
||||
product_id: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
const optionId = "option-1"
|
||||
|
||||
it("should delete the product option given an ID successfully", async () => {
|
||||
await service.delete([optionId])
|
||||
|
||||
const options = await service.list({
|
||||
id: optionId,
|
||||
})
|
||||
|
||||
expect(options).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const optionId = "option-1"
|
||||
|
||||
it("should update the title of the option successfully", async () => {
|
||||
await service.update([
|
||||
{
|
||||
id: optionId,
|
||||
title: "UK",
|
||||
},
|
||||
])
|
||||
|
||||
const productOption = await service.retrieve(optionId)
|
||||
|
||||
expect(productOption.title).toEqual("UK")
|
||||
})
|
||||
|
||||
it("should update the relationship of the option successfully", async () => {
|
||||
await service.update([
|
||||
{
|
||||
id: optionId,
|
||||
product_id: productTwo.id,
|
||||
},
|
||||
])
|
||||
|
||||
const productOption = await service.retrieve(optionId, {
|
||||
relations: ["product"],
|
||||
})
|
||||
|
||||
expect(productOption).toEqual(
|
||||
expect.objectContaining({
|
||||
id: optionId,
|
||||
product: expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.update([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
title: "UK",
|
||||
},
|
||||
])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'ProductOption with id "does-not-exist" not found'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a option successfully", async () => {
|
||||
await service.create([
|
||||
{
|
||||
title: "UK",
|
||||
product: productOne,
|
||||
},
|
||||
])
|
||||
|
||||
const [productOption] = await service.list(
|
||||
{
|
||||
title: "UK",
|
||||
},
|
||||
{
|
||||
relations: ["product"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productOption).toEqual(
|
||||
expect.objectContaining({
|
||||
title: "UK",
|
||||
product: expect.objectContaining({
|
||||
id: productOne.id,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("upsert", function () {
|
||||
it("should create an option and update another option successfully", async () => {
|
||||
const productOption = (
|
||||
await service.create([
|
||||
{
|
||||
title: "UK",
|
||||
product: productOne,
|
||||
},
|
||||
])
|
||||
)[0]
|
||||
|
||||
const optionToUpdate = {
|
||||
id: productOption.id,
|
||||
title: "US",
|
||||
}
|
||||
|
||||
const newOption = {
|
||||
title: "US2",
|
||||
product_id: productOne.id,
|
||||
}
|
||||
|
||||
await service.upsert([optionToUpdate, newOption])
|
||||
|
||||
const productOptions = await service.list(
|
||||
{
|
||||
q: "US%",
|
||||
},
|
||||
{
|
||||
relations: ["product"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(JSON.parse(JSON.stringify(productOptions))).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "US",
|
||||
product: expect.objectContaining({
|
||||
id: productOne.id,
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: newOption.title,
|
||||
product: expect.objectContaining({
|
||||
id: productOne.id,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,338 @@
|
||||
import { Product } from "@models"
|
||||
import { ProductTagService } from "@services"
|
||||
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { createProductAndTags } from "../../../__fixtures__/product"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
medusaApp,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductTag Service", () => {
|
||||
let data!: Product[]
|
||||
let service: ProductTagService
|
||||
|
||||
beforeEach(() => {
|
||||
service = medusaApp.modules["productService"].productTagService_
|
||||
})
|
||||
|
||||
const productsData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
tags: [
|
||||
{
|
||||
id: "tag-1",
|
||||
value: "France",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "product",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
tags: [
|
||||
{
|
||||
id: "tag-2",
|
||||
value: "Germany",
|
||||
},
|
||||
{
|
||||
id: "tag-3",
|
||||
value: "United States",
|
||||
},
|
||||
{
|
||||
id: "tag-4",
|
||||
value: "United Kingdom",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(async () => {
|
||||
data = await createProductAndTags(
|
||||
MikroOrmWrapper.forkManager(),
|
||||
productsData
|
||||
)
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
it("list product tags", async () => {
|
||||
const tagsResults = await service.list()
|
||||
|
||||
expect(tagsResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "tag-1",
|
||||
value: "France",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "tag-2",
|
||||
value: "Germany",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "tag-3",
|
||||
value: "United States",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "tag-4",
|
||||
value: "United Kingdom",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("list product tags by id", async () => {
|
||||
const tagsResults = await service.list({ id: data[0].tags![0].id })
|
||||
|
||||
expect(tagsResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "tag-1",
|
||||
value: "France",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("list product tags by value matching string", async () => {
|
||||
const tagsResults = await service.list({ q: "united kingdom" })
|
||||
|
||||
expect(tagsResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "tag-4",
|
||||
value: "United Kingdom",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCount", () => {
|
||||
it("should return product tags and count", async () => {
|
||||
const [tagsResults, count] = await service.listAndCount()
|
||||
|
||||
expect(count).toEqual(4)
|
||||
expect(tagsResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "tag-1",
|
||||
value: "France",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "tag-2",
|
||||
value: "Germany",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "tag-3",
|
||||
value: "United States",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "tag-4",
|
||||
value: "United Kingdom",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return product tags and count when filtered", async () => {
|
||||
const [tagsResults, count] = await service.listAndCount({
|
||||
id: data[0].tags![0].id,
|
||||
})
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(tagsResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "tag-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return product tags and count when using skip and take", async () => {
|
||||
const [tagsResults, count] = await service.listAndCount(
|
||||
{},
|
||||
{ skip: 1, take: 2 }
|
||||
)
|
||||
|
||||
expect(count).toEqual(4)
|
||||
expect(tagsResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "tag-2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "tag-3",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return requested fields and relations", async () => {
|
||||
const [tagsResults, count] = await service.listAndCount(
|
||||
{},
|
||||
{
|
||||
take: 1,
|
||||
select: ["value", "products.id"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(tagsResults))
|
||||
|
||||
expect(count).toEqual(4)
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "tag-1",
|
||||
products: [
|
||||
{
|
||||
id: "test-1",
|
||||
collection_id: null,
|
||||
type_id: null,
|
||||
},
|
||||
],
|
||||
value: "France",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const tagId = "tag-1"
|
||||
const tagValue = "France"
|
||||
const productId = "test-1"
|
||||
|
||||
it("should return tag for the given id", async () => {
|
||||
const tag = await service.retrieve(tagId)
|
||||
|
||||
expect(tag).toEqual(
|
||||
expect.objectContaining({
|
||||
id: tagId,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when tag with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductTag with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when an id is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("productTag - id must be defined")
|
||||
})
|
||||
|
||||
it("should return tag based on config select param", async () => {
|
||||
const tag = await service.retrieve(tagId, {
|
||||
select: ["id", "value"],
|
||||
})
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(tag))
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id: tagId,
|
||||
value: tagValue,
|
||||
})
|
||||
})
|
||||
|
||||
it("should return tag based on config relation param", async () => {
|
||||
const tag = await service.retrieve(tagId, {
|
||||
select: ["id", "value", "products.id"],
|
||||
relations: ["products"],
|
||||
})
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(tag))
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id: tagId,
|
||||
value: tagValue,
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: productId,
|
||||
}),
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
const tagId = "tag-1"
|
||||
|
||||
it("should delete the product tag given an ID successfully", async () => {
|
||||
await service.delete([tagId])
|
||||
|
||||
const tags = await service.list({
|
||||
id: tagId,
|
||||
})
|
||||
|
||||
expect(tags).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const tagId = "tag-1"
|
||||
|
||||
it("should update the value of the tag successfully", async () => {
|
||||
await service.update([
|
||||
{
|
||||
id: tagId,
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
|
||||
const productTag = await service.retrieve(tagId)
|
||||
|
||||
expect(productTag.value).toEqual("UK")
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.update([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'ProductTag with id "does-not-exist" not found'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a tag successfully", async () => {
|
||||
await service.create([
|
||||
{
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
|
||||
const [productTag] = await service.list({
|
||||
value: "UK",
|
||||
})
|
||||
|
||||
expect(productTag.value).toEqual("UK")
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,277 @@
|
||||
import { ProductTypeService } from "@services"
|
||||
import { Product } from "@models"
|
||||
|
||||
import { createProductAndTypes } from "../../../__fixtures__/product"
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
medusaApp,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe("ProductType Service", () => {
|
||||
let data!: Product[]
|
||||
let service: ProductTypeService
|
||||
|
||||
beforeEach(() => {
|
||||
service = medusaApp.modules["productService"].productTypeService_
|
||||
})
|
||||
|
||||
const productsData = [
|
||||
{
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
type: {
|
||||
id: "type-1",
|
||||
value: "Type 1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "product-2",
|
||||
title: "product",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
type: {
|
||||
id: "type-2",
|
||||
value: "Type 2",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(async () => {
|
||||
data = await createProductAndTypes(
|
||||
MikroOrmWrapper.forkManager(),
|
||||
productsData
|
||||
)
|
||||
})
|
||||
describe("list", () => {
|
||||
it("list product type", async () => {
|
||||
const typeResults = await service.list()
|
||||
|
||||
expect(typeResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "type-1",
|
||||
value: "Type 1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "type-2",
|
||||
value: "Type 2",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("list product type by id", async () => {
|
||||
const typeResults = await service.list({ id: data[0].type.id })
|
||||
|
||||
expect(typeResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "type-1",
|
||||
value: "Type 1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("list product type by value matching string", async () => {
|
||||
const typeResults = await service.list({ value: "Type 1" })
|
||||
|
||||
expect(typeResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "type-1",
|
||||
value: "Type 1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCount", () => {
|
||||
it("should return product type and count", async () => {
|
||||
const [typeResults, count] = await service.listAndCount()
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(typeResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "type-1",
|
||||
value: "Type 1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "type-2",
|
||||
value: "Type 2",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return product type and count when filtered", async () => {
|
||||
const [typeResults, count] = await service.listAndCount({
|
||||
id: data[0].type.id,
|
||||
})
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(typeResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "type-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return product type and count when using skip and take", async () => {
|
||||
const [typeResults, count] = await service.listAndCount(
|
||||
{},
|
||||
{ skip: 1, take: 1 }
|
||||
)
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(typeResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "type-2",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return requested fields", async () => {
|
||||
const [typeResults, count] = await service.listAndCount(
|
||||
{},
|
||||
{
|
||||
take: 1,
|
||||
select: ["value"],
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(typeResults))
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "type-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const typeId = "type-1"
|
||||
const typeValue = "Type 1"
|
||||
|
||||
it("should return type for the given id", async () => {
|
||||
const type = await service.retrieve(typeId)
|
||||
|
||||
expect(type).toEqual(
|
||||
expect.objectContaining({
|
||||
id: typeId,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when type with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductType with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when an id is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("productType - id must be defined")
|
||||
})
|
||||
|
||||
it("should return type based on config select param", async () => {
|
||||
const type = await service.retrieve(typeId, {
|
||||
select: ["id", "value"],
|
||||
})
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(type))
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id: typeId,
|
||||
value: typeValue,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
const typeId = "type-1"
|
||||
|
||||
it("should delete the product type given an ID successfully", async () => {
|
||||
await service.delete([typeId])
|
||||
|
||||
const types = await service.list({
|
||||
id: typeId,
|
||||
})
|
||||
|
||||
expect(types).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const typeId = "type-1"
|
||||
|
||||
it("should update the value of the type successfully", async () => {
|
||||
await service.update([
|
||||
{
|
||||
id: typeId,
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
|
||||
const productType = await service.retrieve(typeId)
|
||||
|
||||
expect(productType.value).toEqual("UK")
|
||||
})
|
||||
|
||||
it("should throw an error when an id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.update([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'ProductType with id "does-not-exist" not found'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a type successfully", async () => {
|
||||
await service.create([
|
||||
{
|
||||
value: "UK",
|
||||
},
|
||||
])
|
||||
|
||||
const [productType] = await service.list({
|
||||
value: "UK",
|
||||
})
|
||||
|
||||
expect(productType.value).toEqual("UK")
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,328 @@
|
||||
import { ProductOption } from "@medusajs/client-types"
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { Collection } from "@mikro-orm/core"
|
||||
import { Product, ProductTag, ProductVariant } from "@models"
|
||||
import { ProductVariantService } from "@services"
|
||||
import {
|
||||
createOptions,
|
||||
createProductAndTags,
|
||||
createProductVariants,
|
||||
} from "../../../__fixtures__/product"
|
||||
import { productsData, variantsData } from "../../../__fixtures__/product/data"
|
||||
import { buildProductVariantOnlyData } from "../../../__fixtures__/variant/data/create-variant"
|
||||
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
medusaApp,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
describe.skip("ProductVariant Service", () => {
|
||||
let variantOne: ProductVariant
|
||||
let variantTwo: ProductVariant
|
||||
let productOne: Product
|
||||
const productVariantTestOne = "test-1"
|
||||
let service: ProductVariantService
|
||||
|
||||
beforeEach(() => {
|
||||
service = medusaApp.modules["productService"].productVariantService_
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
variantOne = testManager.create(ProductVariant, {
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
inventory_quantity: 10,
|
||||
product: productOne,
|
||||
})
|
||||
|
||||
variantTwo = testManager.create(ProductVariant, {
|
||||
id: "test-2",
|
||||
title: "variant",
|
||||
inventory_quantity: 10,
|
||||
product: productOne,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([variantOne, variantTwo])
|
||||
})
|
||||
|
||||
it("selecting by properties, scopes out the results", async () => {
|
||||
const results = await service.list({
|
||||
id: variantOne.id,
|
||||
})
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: variantOne.id,
|
||||
title: "variant 1",
|
||||
inventory_quantity: "10",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("passing a limit, scopes the result to the limit", async () => {
|
||||
const results = await service.list(
|
||||
{},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: variantOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("passing populate, scopes the results of the response", async () => {
|
||||
const results = await service.list(
|
||||
{
|
||||
id: productVariantTestOne,
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "product.title"] as any,
|
||||
relations: ["product"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
product: expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
tags: expect.any(Collection<ProductTag>),
|
||||
variants: expect.any(Collection<ProductVariant>),
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
expect(JSON.parse(JSON.stringify(results))).toEqual([
|
||||
{
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
product: {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("relation: options", () => {
|
||||
let products: Product[]
|
||||
let variants: ProductVariant[]
|
||||
let options: ProductOption[]
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
products = (await createProductAndTags(
|
||||
testManager,
|
||||
productsData
|
||||
)) as Product[]
|
||||
variants = (await createProductVariants(
|
||||
testManager,
|
||||
variantsData
|
||||
)) as ProductVariant[]
|
||||
|
||||
options = await createOptions(testManager, [
|
||||
{
|
||||
id: "test-option-1",
|
||||
title: "size",
|
||||
product: products[0],
|
||||
values: [
|
||||
{
|
||||
id: "value-1",
|
||||
value: "XS",
|
||||
variant: products[0].variants[0],
|
||||
},
|
||||
{
|
||||
id: "value-1",
|
||||
value: "XL",
|
||||
variant: products[0].variants[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "test-option-2",
|
||||
title: "color",
|
||||
product: products[0],
|
||||
value: "blue",
|
||||
variant: products[0].variants[0],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("filter by options relation", async () => {
|
||||
const variants = await service.list(
|
||||
{ options: { id: ["value-1"] } },
|
||||
{ relations: ["options"] }
|
||||
)
|
||||
|
||||
expect(JSON.parse(JSON.stringify(variants))).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productVariantTestOne,
|
||||
title: "variant title",
|
||||
sku: "sku 1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", function () {
|
||||
let products: Product[]
|
||||
let productOptions!: ProductOption[]
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
products = (await createProductAndTags(
|
||||
testManager,
|
||||
productsData
|
||||
)) as Product[]
|
||||
|
||||
productOptions = await createOptions(testManager, [
|
||||
{
|
||||
id: "test-option-1",
|
||||
title: "size",
|
||||
product: products[0],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should create a variant", async () => {
|
||||
const data = buildProductVariantOnlyData({
|
||||
options: [
|
||||
{
|
||||
option: productOptions[0],
|
||||
value: "XS",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const variants = await service.create(products[0].id, [data])
|
||||
|
||||
expect(variants).toHaveLength(1)
|
||||
expect(variants[0].options).toHaveLength(1)
|
||||
|
||||
expect(JSON.parse(JSON.stringify(variants[0]))).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.title,
|
||||
sku: data.sku,
|
||||
inventory_quantity: 100,
|
||||
allow_backorder: false,
|
||||
manage_inventory: true,
|
||||
variant_rank: 0,
|
||||
product: expect.objectContaining({
|
||||
id: products[0].id,
|
||||
}),
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.options![0].value,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
beforeEach(async () => {
|
||||
const testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
variantOne = testManager.create(ProductVariant, {
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
inventory_quantity: 10,
|
||||
product: productOne,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([variantOne])
|
||||
})
|
||||
|
||||
it("should return the requested variant", async () => {
|
||||
const result = await service.retrieve(variantOne.id)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const result = await service.retrieve(variantOne.id, {
|
||||
select: ["id", "title", "product.title"] as any,
|
||||
relations: ["product"],
|
||||
})
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
product_id: "product-1",
|
||||
product: expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a variant with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"ProductVariant with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when an id is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("productVariant - id must be defined")
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,693 @@
|
||||
import { Image, Product, ProductCategory, ProductCollection } from "@models"
|
||||
import {
|
||||
assignCategoriesToProduct,
|
||||
buildProductOnlyData,
|
||||
createCollections,
|
||||
createImages,
|
||||
createProductAndTags,
|
||||
createProductVariants,
|
||||
} from "../../../__fixtures__/product"
|
||||
|
||||
import {
|
||||
categoriesData,
|
||||
productsData,
|
||||
variantsData,
|
||||
} from "../../../__fixtures__/product/data"
|
||||
import { ProductService } from "@services"
|
||||
import {
|
||||
IProductModuleService,
|
||||
ProductDTO,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import { kebabCase } from "@medusajs/utils"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { ProductTag } from "../../../../src/models"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.PRODUCT,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
medusaApp,
|
||||
}: SuiteOptions<IProductModuleService>) => {
|
||||
let service: ProductService
|
||||
|
||||
beforeEach(() => {
|
||||
service = medusaApp.modules["productService"].productService_
|
||||
})
|
||||
|
||||
describe("Product Service", () => {
|
||||
let testManager: SqlEntityManager
|
||||
let products!: Product[]
|
||||
let productOne: Product
|
||||
let categories!: ProductCategory[]
|
||||
|
||||
describe("retrieve", () => {
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([productOne])
|
||||
})
|
||||
|
||||
it("should throw an error when an id is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("product - id must be defined")
|
||||
})
|
||||
|
||||
it("should throw an error when product with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"Product with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should return a product when product with an id exists", async () => {
|
||||
const result = await service.retrieve(productOne.id)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should create a product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
|
||||
expect(products).toHaveLength(1)
|
||||
expect(JSON.parse(JSON.stringify(products[0]))).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.title,
|
||||
handle: kebabCase(data.title),
|
||||
description: data.description,
|
||||
subtitle: data.subtitle,
|
||||
is_giftcard: data.is_giftcard,
|
||||
discountable: data.discountable,
|
||||
thumbnail: images[0].url,
|
||||
status: data.status,
|
||||
images: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: images[0].id,
|
||||
url: images[0].url,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
images = await createImages(testManager, ["image-1", "image-2"])
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([productOne])
|
||||
})
|
||||
|
||||
it("should update a product and its allowed relations", async () => {
|
||||
const updateData = [
|
||||
{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
images: images,
|
||||
thumbnail: images[0].url,
|
||||
},
|
||||
]
|
||||
|
||||
const products = await service.update(updateData)
|
||||
|
||||
expect(products.length).toEqual(1)
|
||||
|
||||
let result = await service.retrieve(productOne.id, {
|
||||
relations: ["images", "thumbnail"],
|
||||
})
|
||||
let serialized = JSON.parse(JSON.stringify(result))
|
||||
|
||||
expect(serialized).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
thumbnail: images[0].url,
|
||||
images: [
|
||||
expect.objectContaining({
|
||||
url: images[0].url,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: images[1].url,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when id is not present", async () => {
|
||||
let error
|
||||
const updateData = [
|
||||
{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
},
|
||||
{
|
||||
id: undefined as unknown as string,
|
||||
title: "update test 2",
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
await service.update(updateData)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(`Product with id "" not found`)
|
||||
|
||||
let result = await service.retrieve(productOne.id)
|
||||
|
||||
expect(result.title).not.toBe("update test 1")
|
||||
})
|
||||
|
||||
it("should throw an error when product with id does not exist", async () => {
|
||||
let error
|
||||
const updateData = [
|
||||
{
|
||||
id: "does-not-exist",
|
||||
title: "update test 1",
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
await service.update(updateData)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
`Product with id "does-not-exist" not found`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
it("should list all product that match the free text search", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
title: "test product",
|
||||
})
|
||||
const data2 = buildProductOnlyData({
|
||||
title: "space X",
|
||||
})
|
||||
|
||||
const products = await service.create([data, data2])
|
||||
|
||||
const result = await service.list({
|
||||
q: "test",
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].title).toEqual("test product")
|
||||
|
||||
const result2 = await service.list({
|
||||
q: "space",
|
||||
})
|
||||
|
||||
expect(result2).toHaveLength(1)
|
||||
expect(result2[0].title).toEqual("space X")
|
||||
})
|
||||
|
||||
describe("soft deleted", function () {
|
||||
let product
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
const products = await createProductAndTags(
|
||||
testManager,
|
||||
productsData
|
||||
)
|
||||
|
||||
product = products[1]
|
||||
await service.softDelete([products[0].id])
|
||||
})
|
||||
|
||||
it("should list all products that are not deleted", async () => {
|
||||
const products = await service.list()
|
||||
|
||||
expect(products).toHaveLength(2)
|
||||
expect(products[0].id).toEqual(product.id)
|
||||
})
|
||||
|
||||
it("should list all products including the deleted", async () => {
|
||||
const products = await service.list({}, { withDeleted: true })
|
||||
|
||||
expect(products).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe("relation: tags", () => {
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
products = await createProductAndTags(testManager, productsData)
|
||||
})
|
||||
|
||||
it("should filter by id and including relations", async () => {
|
||||
const productsResult = await service.list(
|
||||
{
|
||||
id: products[0].id,
|
||||
},
|
||||
{
|
||||
relations: ["tags"],
|
||||
}
|
||||
)
|
||||
|
||||
productsResult.forEach((product, index) => {
|
||||
const tags = product.tags.toArray()
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productsData[index].id,
|
||||
title: productsData[index].title,
|
||||
})
|
||||
)
|
||||
|
||||
tags.forEach((tag, tagIndex) => {
|
||||
expect(tag).toEqual(
|
||||
expect.objectContaining({
|
||||
...productsData[index].tags[tagIndex],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should filter by id and without relations", async () => {
|
||||
const productsResult = await service.list({
|
||||
id: products[0].id,
|
||||
})
|
||||
|
||||
productsResult.forEach((product, index) => {
|
||||
const tags = product.tags.getItems(false)
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productsData[index].id,
|
||||
title: productsData[index].title,
|
||||
})
|
||||
)
|
||||
|
||||
expect(tags.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("relation: categories", () => {
|
||||
let workingProduct: Product
|
||||
let workingCategory: ProductCategory
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
products = await createProductAndTags(testManager, productsData)
|
||||
workingProduct = products.find((p) => p.id === "test-1") as Product
|
||||
categories = await createProductCategories(
|
||||
testManager,
|
||||
categoriesData
|
||||
)
|
||||
workingCategory = (await testManager.findOne(
|
||||
ProductCategory,
|
||||
"category-1"
|
||||
)) as ProductCategory
|
||||
|
||||
workingProduct = await assignCategoriesToProduct(
|
||||
testManager,
|
||||
workingProduct,
|
||||
categories
|
||||
)
|
||||
})
|
||||
|
||||
it("should filter by categories relation and scope fields", async () => {
|
||||
const products = await service.list(
|
||||
{
|
||||
id: workingProduct.id,
|
||||
categories: { id: [workingCategory.id] },
|
||||
},
|
||||
{
|
||||
select: [
|
||||
"title",
|
||||
"categories.name",
|
||||
"categories.handle",
|
||||
"categories.mpath",
|
||||
] as (keyof ProductDTO)[],
|
||||
relations: ["categories"],
|
||||
}
|
||||
)
|
||||
|
||||
const product = products.find(
|
||||
(p) => p.id === workingProduct.id
|
||||
) as unknown as Product
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: workingProduct.id,
|
||||
title: workingProduct.title,
|
||||
})
|
||||
)
|
||||
|
||||
expect(product.categories.toArray()).toEqual([
|
||||
{
|
||||
id: "category-0",
|
||||
name: "category 0",
|
||||
handle: "category-0",
|
||||
mpath: "category-0.",
|
||||
parent_category_id: null,
|
||||
},
|
||||
{
|
||||
id: "category-1",
|
||||
name: "category 1",
|
||||
handle: "category-1",
|
||||
mpath: "category-0.category-1.",
|
||||
parent_category_id: null,
|
||||
},
|
||||
{
|
||||
id: "category-1-a",
|
||||
name: "category 1 a",
|
||||
handle: "category-1-a",
|
||||
mpath: "category-0.category-1.category-1-a.",
|
||||
parent_category_id: null,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should returns empty array when querying for a category that doesnt exist", async () => {
|
||||
const products = await service.list(
|
||||
{
|
||||
id: workingProduct.id,
|
||||
categories: { id: ["category-doesnt-exist-id"] },
|
||||
},
|
||||
{
|
||||
select: [
|
||||
"title",
|
||||
"categories.name",
|
||||
"categories.handle",
|
||||
] as (keyof ProductDTO)[],
|
||||
relations: ["categories"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(products).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("relation: collections", () => {
|
||||
let workingProduct: Product
|
||||
let workingProductTwo: Product
|
||||
let workingCollection: ProductCollection
|
||||
let workingCollectionTwo: ProductCollection
|
||||
const collectionData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
await createCollections(testManager, collectionData)
|
||||
workingCollection = (await testManager.findOne(
|
||||
ProductCollection,
|
||||
"test-1"
|
||||
)) as ProductCollection
|
||||
workingCollectionTwo = (await testManager.findOne(
|
||||
ProductCollection,
|
||||
"test-2"
|
||||
)) as ProductCollection
|
||||
|
||||
products = await createProductAndTags(testManager, [
|
||||
{
|
||||
...productsData[0],
|
||||
collection_id: workingCollection.id,
|
||||
},
|
||||
{
|
||||
...productsData[1],
|
||||
collection_id: workingCollectionTwo.id,
|
||||
},
|
||||
{
|
||||
...productsData[2],
|
||||
},
|
||||
])
|
||||
|
||||
workingProduct = products.find((p) => p.id === "test-1") as Product
|
||||
workingProductTwo = products.find(
|
||||
(p) => p.id === "test-2"
|
||||
) as Product
|
||||
})
|
||||
|
||||
it("should filter by collection relation and scope fields", async () => {
|
||||
const products = await service.list(
|
||||
{
|
||||
id: workingProduct.id,
|
||||
collection_id: workingCollection.id,
|
||||
},
|
||||
{
|
||||
select: ["title", "collection.title"],
|
||||
relations: ["collection"],
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(products))
|
||||
|
||||
expect(serialized.length).toEqual(1)
|
||||
expect(serialized).toEqual([
|
||||
{
|
||||
id: workingProduct.id,
|
||||
title: workingProduct.title,
|
||||
handle: "product-1",
|
||||
collection_id: workingCollection.id,
|
||||
type_id: null,
|
||||
collection: {
|
||||
handle: "col-1",
|
||||
id: workingCollection.id,
|
||||
title: workingCollection.title,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should filter by collection when multiple collection ids are passed", async () => {
|
||||
const products = await service.list(
|
||||
{
|
||||
collection_id: [workingCollection.id, workingCollectionTwo.id],
|
||||
},
|
||||
{
|
||||
select: ["title", "collection.title"],
|
||||
relations: ["collection"],
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(products))
|
||||
|
||||
expect(serialized.length).toEqual(2)
|
||||
expect(serialized).toEqual([
|
||||
{
|
||||
id: workingProduct.id,
|
||||
title: workingProduct.title,
|
||||
handle: "product-1",
|
||||
type_id: null,
|
||||
collection_id: workingCollection.id,
|
||||
collection: {
|
||||
handle: "col-1",
|
||||
id: workingCollection.id,
|
||||
title: workingCollection.title,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: workingProductTwo.id,
|
||||
title: workingProductTwo.title,
|
||||
handle: "product",
|
||||
type_id: null,
|
||||
collection_id: workingCollectionTwo.id,
|
||||
collection: {
|
||||
handle: "col-2",
|
||||
id: workingCollectionTwo.id,
|
||||
title: workingCollectionTwo.title,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should returns empty array when querying for a collection that doesnt exist", async () => {
|
||||
const products = await service.list(
|
||||
{
|
||||
id: workingProduct.id,
|
||||
collection_id: "collection-doesnt-exist-id",
|
||||
},
|
||||
{
|
||||
select: ["title", "collection.title"] as (keyof ProductDTO)[],
|
||||
relations: ["collection"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(products).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("relation: variants", () => {
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
products = await createProductAndTags(testManager, productsData)
|
||||
await createProductVariants(testManager, variantsData)
|
||||
})
|
||||
|
||||
it("should filter by id and including relations", async () => {
|
||||
const productsResult = await service.list(
|
||||
{
|
||||
id: products[0].id,
|
||||
},
|
||||
{
|
||||
relations: ["variants"],
|
||||
}
|
||||
)
|
||||
|
||||
productsResult.forEach((product, index) => {
|
||||
const variants = product.variants.toArray()
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productsData[index].id,
|
||||
title: productsData[index].title,
|
||||
})
|
||||
)
|
||||
|
||||
variants.forEach((variant, variantIndex) => {
|
||||
const expectedVariant = variantsData.filter(
|
||||
(d) => d.product.id === product.id
|
||||
)[variantIndex]
|
||||
|
||||
const variantProduct = variant.product
|
||||
|
||||
expect(variant).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expectedVariant.id,
|
||||
sku: expectedVariant.sku,
|
||||
title: expectedVariant.title,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("softDelete", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should soft delete a product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
await service.softDelete(products.map((p) => p.id))
|
||||
const deleteProducts = await service.list(
|
||||
{ id: products.map((p) => p.id) },
|
||||
{
|
||||
relations: [
|
||||
"variants",
|
||||
"variants.options",
|
||||
"options",
|
||||
"options.values",
|
||||
],
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deleteProducts).toHaveLength(1)
|
||||
expect(deleteProducts[0].deleted_at).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("restore", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should restore a soft deleted product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
const product = products[0]
|
||||
await service.softDelete([product.id])
|
||||
const [restoreProducts] = await service.restore([product.id])
|
||||
|
||||
expect(restoreProducts).toHaveLength(1)
|
||||
expect(restoreProducts[0].deleted_at).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
20
packages/modules/product/jest.config.js
Normal file
20
packages/modules/product/jest.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
"^@models": "<rootDir>/src/models",
|
||||
"^@services": "<rootDir>/src/services",
|
||||
"^@repositories": "<rootDir>/src/repositories",
|
||||
"^@types": "<rootDir>/src/types",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: "tsconfig.spec.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
modulePathIgnorePatterns: ["dist/"],
|
||||
}
|
||||
12
packages/modules/product/mikro-orm.config.dev.ts
Normal file
12
packages/modules/product/mikro-orm.config.dev.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { TSMigrationGenerator } from "@mikro-orm/migrations"
|
||||
import * as entities from "./src/models"
|
||||
|
||||
module.exports = {
|
||||
entities: Object.values(entities),
|
||||
schema: "public",
|
||||
clientUrl: "postgres://postgres@localhost/medusa-products",
|
||||
type: "postgresql",
|
||||
migrations: {
|
||||
generator: TSMigrationGenerator,
|
||||
},
|
||||
}
|
||||
64
packages/modules/product/package.json
Normal file
64
packages/modules/product/package.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@medusajs/product",
|
||||
"version": "0.3.12",
|
||||
"description": "Medusa Product module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"bin": {
|
||||
"medusa-product-seed": "dist/scripts/bin/run-seed.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/product"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"author": "Medusa",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"watch": "tsc --build --watch",
|
||||
"watch:test": "tsc --build tsconfig.spec.json --watch",
|
||||
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
|
||||
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
|
||||
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts",
|
||||
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts",
|
||||
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate",
|
||||
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial",
|
||||
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",
|
||||
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up",
|
||||
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mikro-orm/cli": "5.9.7",
|
||||
"cross-env": "^5.2.1",
|
||||
"faker": "^6.6.6",
|
||||
"jest": "^29.6.3",
|
||||
"medusa-test-utils": "^1.1.44",
|
||||
"pg-god": "^1.0.12",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-alias": "^1.8.6",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/modules-sdk": "^1.12.11",
|
||||
"@medusajs/types": "^1.11.16",
|
||||
"@medusajs/utils": "^1.11.9",
|
||||
"@mikro-orm/core": "5.9.7",
|
||||
"@mikro-orm/migrations": "5.9.7",
|
||||
"@mikro-orm/postgresql": "5.9.7",
|
||||
"awilix": "^8.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"knex": "2.4.2",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
}
|
||||
14
packages/modules/product/src/index.ts
Normal file
14
packages/modules/product/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
moduleDefinition,
|
||||
revertMigration,
|
||||
runMigrations,
|
||||
} from "./module-definition"
|
||||
|
||||
export default moduleDefinition
|
||||
export { revertMigration, runMigrations }
|
||||
|
||||
export * from "./initialize"
|
||||
// TODO: remove export from models and services
|
||||
export * from "./models"
|
||||
export * from "./services"
|
||||
export * from "./types"
|
||||
34
packages/modules/product/src/initialize/index.ts
Normal file
34
packages/modules/product/src/initialize/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
MedusaModule,
|
||||
MODULE_PACKAGE_NAMES,
|
||||
Modules,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
|
||||
|
||||
import { InitializeModuleInjectableDependencies } from "@types"
|
||||
import { moduleDefinition } from "../module-definition"
|
||||
|
||||
export const initialize = async (
|
||||
options?:
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration
|
||||
| InternalModuleDeclaration,
|
||||
injectedDependencies?: InitializeModuleInjectableDependencies
|
||||
): Promise<IProductModuleService> => {
|
||||
const serviceKey = Modules.PRODUCT
|
||||
|
||||
const loaded = await MedusaModule.bootstrap<IProductModuleService>({
|
||||
moduleKey: serviceKey,
|
||||
defaultPath: MODULE_PACKAGE_NAMES[Modules.PRODUCT],
|
||||
declaration: options as
|
||||
| InternalModuleDeclaration
|
||||
| ExternalModuleDeclaration,
|
||||
injectedDependencies,
|
||||
moduleExports: moduleDefinition,
|
||||
})
|
||||
|
||||
return loaded[serviceKey]
|
||||
}
|
||||
99
packages/modules/product/src/joiner-config.ts
Normal file
99
packages/modules/product/src/joiner-config.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
import {
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductOption,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
import ProductImage from "./models/product-image"
|
||||
|
||||
export const LinkableKeys = {
|
||||
product_id: Product.name,
|
||||
product_handle: Product.name,
|
||||
variant_id: ProductVariant.name,
|
||||
variant_sku: ProductVariant.name,
|
||||
product_option_id: ProductOption.name,
|
||||
product_type_id: ProductType.name,
|
||||
product_category_id: ProductCategory.name,
|
||||
product_collection_id: ProductCollection.name,
|
||||
product_tag_id: ProductTag.name,
|
||||
product_image_id: ProductImage.name,
|
||||
}
|
||||
|
||||
const entityLinkableKeysMap: MapToConfig = {}
|
||||
Object.entries(LinkableKeys).forEach(([key, value]) => {
|
||||
entityLinkableKeysMap[value] ??= []
|
||||
entityLinkableKeysMap[value].push({
|
||||
mapTo: key,
|
||||
valueFrom: key.split("_").pop()!,
|
||||
})
|
||||
})
|
||||
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
|
||||
|
||||
export const joinerConfig: ModuleJoinerConfig = {
|
||||
serviceName: Modules.PRODUCT,
|
||||
primaryKeys: ["id", "handle"],
|
||||
linkableKeys: LinkableKeys,
|
||||
alias: [
|
||||
{
|
||||
name: ["product", "products"],
|
||||
args: {
|
||||
entity: "Product",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_variant", "product_variants", "variant", "variants"],
|
||||
args: {
|
||||
entity: "ProductVariant",
|
||||
methodSuffix: "Variants",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_option", "product_options"],
|
||||
args: {
|
||||
entity: "ProductOption",
|
||||
methodSuffix: "Options",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_type", "product_types"],
|
||||
args: {
|
||||
entity: "ProductType",
|
||||
methodSuffix: "Types",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_image", "product_images"],
|
||||
args: {
|
||||
entity: "ProductImage",
|
||||
methodSuffix: "Images",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_tag", "product_tags"],
|
||||
args: {
|
||||
entity: "ProductTag",
|
||||
methodSuffix: "Tags",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_collection", "product_collections"],
|
||||
args: {
|
||||
entity: "ProductCollection",
|
||||
methodSuffix: "Collections",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_category", "product_categories"],
|
||||
args: {
|
||||
entity: "ProductCategory",
|
||||
methodSuffix: "Categories",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
30
packages/modules/product/src/loaders/connection.ts
Normal file
30
packages/modules/product/src/loaders/connection.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { InternalModuleDeclaration, LoaderOptions } from "@medusajs/modules-sdk"
|
||||
import { ModulesSdkTypes } from "@medusajs/types"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import * as ProductModels from "../models"
|
||||
|
||||
export default async (
|
||||
{
|
||||
options,
|
||||
container,
|
||||
logger,
|
||||
}: LoaderOptions<
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void> => {
|
||||
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
|
||||
const pathToMigrations = __dirname + "/../migrations"
|
||||
|
||||
await ModulesSdkUtils.mikroOrmConnectionLoader({
|
||||
moduleName: "product",
|
||||
entities,
|
||||
container,
|
||||
options,
|
||||
moduleDeclaration,
|
||||
logger,
|
||||
pathToMigrations,
|
||||
})
|
||||
}
|
||||
10
packages/modules/product/src/loaders/container.ts
Normal file
10
packages/modules/product/src/loaders/container.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import * as ModuleModels from "@models"
|
||||
import * as ModuleRepositories from "@repositories"
|
||||
import * as ModuleServices from "@services"
|
||||
|
||||
export default ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: ModuleModels,
|
||||
moduleRepositories: ModuleRepositories,
|
||||
moduleServices: ModuleServices,
|
||||
})
|
||||
2
packages/modules/product/src/loaders/index.ts
Normal file
2
packages/modules/product/src/loaders/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./connection"
|
||||
export * from "./container"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class InitialSetup20240315083440 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
const productTables = await this.execute(
|
||||
"select * from information_schema.tables where table_name = 'product' and table_schema = 'public'"
|
||||
)
|
||||
|
||||
if (productTables.length > 0) {
|
||||
// This is so we can still run the api tests, remove completely once that is not needed
|
||||
this.addSql(
|
||||
`alter table "product_option_value" alter column "variant_id" drop not null;`
|
||||
)
|
||||
this.addSql(
|
||||
`alter table "product_variant" alter column "inventory_quantity" drop not null;`
|
||||
)
|
||||
this.addSql(
|
||||
`alter table "product_category" add column "deleted_at" timestamptz null;`
|
||||
)
|
||||
}
|
||||
|
||||
/* --- ENTITY TABLES AND INDICES --- */
|
||||
this.addSql('create table if not exists "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));');
|
||||
this.addSql('create unique index if not exists "IDX_product_handle_unique" on "product" (handle) where deleted_at is null;')
|
||||
this.addSql('create index if not exists "IDX_product_type_id" on "product" ("type_id") where deleted_at is null;');
|
||||
this.addSql('create index if not exists "IDX_product_collection_id" on "product" ("collection_id") where deleted_at is null;');
|
||||
this.addSql('create index if not exists "IDX_product_deleted_at" on "product" ("deleted_at");');
|
||||
|
||||
this.addSql('create table if not exists "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null default 100, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null default 0, "product_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_pkey" primary key ("id"));');
|
||||
this.addSql('create unique index if not exists "IDX_product_variant_ean_unique" on "product_variant" (ean) where deleted_at is null;')
|
||||
this.addSql('create unique index if not exists "IDX_product_variant_upc_unique" on "product_variant" (upc) where deleted_at is null;')
|
||||
this.addSql('create unique index if not exists "IDX_product_variant_sku_unique" on "product_variant" (sku) where deleted_at is null;')
|
||||
this.addSql('create unique index if not exists "IDX_product_variant_barcode_unique" on "product_variant" (barcode) where deleted_at is null;')
|
||||
this.addSql('create index if not exists "IDX_product_variant_product_id" on "product_variant" ("product_id") where deleted_at is null;');
|
||||
this.addSql('create index if not exists "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");');
|
||||
|
||||
this.addSql('create table if not exists "product_option" ("id" text not null, "title" text not null, "product_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));');
|
||||
this.addSql('create unique index if not exists "IDX_option_product_id_title_unique" on "product_option" (product_id, title) where deleted_at is null;')
|
||||
this.addSql('create index if not exists "IDX_product_option_deleted_at" on "product_option" ("deleted_at");');
|
||||
|
||||
this.addSql('create table if not exists "product_option_value" ("id" text not null, "value" text not null, "option_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));');
|
||||
this.addSql('create unique index if not exists "IDX_option_value_option_id_unique" on "product_option_value" (option_id, value) where deleted_at is null;')
|
||||
this.addSql('create index if not exists "IDX_product_option_value_deleted_at" on "product_option_value" ("deleted_at");');
|
||||
|
||||
this.addSql('create table if not exists "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));');
|
||||
this.addSql('create index if not exists "IDX_product_image_url" on "image" ("url") where deleted_at is null;');
|
||||
this.addSql('create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");');
|
||||
|
||||
this.addSql('create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));');
|
||||
|
||||
this.addSql('create unique index if not exists "IDX_tag_value_unique" on "product_tag" (value) where deleted_at is null;')
|
||||
this.addSql('create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");');
|
||||
|
||||
this.addSql('create table if not exists "product_type" ("id" text not null, "value" text not null, "metadata" json null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));');
|
||||
this.addSql('create unique index if not exists "IDX_type_value_unique" on "product_type" (value) where deleted_at is null;')
|
||||
this.addSql('create index if not exists "IDX_product_type_deleted_at" on "product_type" ("deleted_at");');
|
||||
|
||||
this.addSql('create table if not exists "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));');
|
||||
this.addSql('create unique index if not exists "IDX_collection_handle_unique" on "product_collection" (handle) where deleted_at is null;')
|
||||
this.addSql('create index if not exists "IDX_product_collection_deleted_at" on "product_collection" ("deleted_at");');
|
||||
|
||||
this.addSql('create table if not exists "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_category_pkey" primary key ("id"));');
|
||||
this.addSql('create unique index if not exists "IDX_category_handle_unique" on "product_category" (handle) where deleted_at is null;')
|
||||
this.addSql('create index if not exists "IDX_product_category_path" on "product_category" ("mpath") where deleted_at is null;');
|
||||
this.addSql('create index if not exists "IDX_product_category_deleted_at" on "product_collection" ("deleted_at");');
|
||||
// TODO: Batch updating composite unique index in MikroORM is faulty. Should be added back when issue has been resolved.
|
||||
this.addSql(`drop index if exists "UniqProductCategoryParentIdRank";`)
|
||||
|
||||
/* --- PIVOT TABLES --- */
|
||||
this.addSql('create table if not exists "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));');
|
||||
this.addSql('create table if not exists "product_images" ("product_id" text not null, "image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "image_id"));');
|
||||
this.addSql('create table if not exists "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));');
|
||||
this.addSql('create table if not exists "product_variant_option" ("variant_id" text not null, "option_value_id" text not null, constraint "product_variant_option_pkey" primary key ("variant_id", "option_value_id"));');
|
||||
|
||||
/* --- FOREIGN KEYS AND CONSTRAINTS --- */
|
||||
this.addSql('alter table if exists "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;');
|
||||
this.addSql('alter table if exists "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;');
|
||||
|
||||
this.addSql('alter table if exists "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table if exists "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table if exists "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_option_value_id_foreign" foreign key ("option_value_id") references "product_option_value" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table if exists "product_images" add constraint "product_images_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('alter table if exists "product_images" add constraint "product_images_image_id_foreign" foreign key ("image_id") references "image" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table if exists "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('alter table if exists "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table if exists "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('alter table if exists "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table if exists "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete cascade;');
|
||||
}
|
||||
}
|
||||
9
packages/modules/product/src/models/index.ts
Normal file
9
packages/modules/product/src/models/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default as Product } from "./product"
|
||||
export { default as ProductCategory } from "./product-category"
|
||||
export { default as ProductCollection } from "./product-collection"
|
||||
export { default as ProductTag } from "./product-tag"
|
||||
export { default as ProductType } from "./product-type"
|
||||
export { default as ProductVariant } from "./product-variant"
|
||||
export { default as ProductOption } from "./product-option"
|
||||
export { default as ProductOptionValue } from "./product-option-value"
|
||||
export { default as Image } from "./product-image"
|
||||
148
packages/modules/product/src/models/product-category.ts
Normal file
148
packages/modules/product/src/models/product-category.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
kebabCase,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
EventArgs,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import Product from "./product"
|
||||
|
||||
const categoryHandleIndexName = "IDX_category_handle_unique"
|
||||
const categoryHandleIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: categoryHandleIndexName,
|
||||
tableName: "product_category",
|
||||
columns: ["handle"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const categoryMpathIndexName = "IDX_product_category_path"
|
||||
const categoryMpathIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: categoryMpathIndexName,
|
||||
tableName: "product_category",
|
||||
columns: ["mpath"],
|
||||
unique: false,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
categoryMpathIndexStatement.MikroORMIndex()
|
||||
categoryHandleIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_category" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductCategory {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
name?: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", default: "", nullable: false })
|
||||
description?: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
handle?: string
|
||||
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
mpath?: string
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
is_active?: boolean
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
is_internal?: boolean
|
||||
|
||||
@Property({ columnType: "numeric", nullable: false, default: 0 })
|
||||
rank?: number
|
||||
|
||||
@ManyToOne(() => ProductCategory, {
|
||||
columnType: "text",
|
||||
fieldName: "parent_category_id",
|
||||
nullable: true,
|
||||
mapToPk: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
parent_category_id?: string | null
|
||||
|
||||
@ManyToOne(() => ProductCategory, { nullable: true, persist: false })
|
||||
parent_category?: ProductCategory
|
||||
|
||||
@OneToMany({
|
||||
entity: () => ProductCategory,
|
||||
mappedBy: (productCategory) => productCategory.parent_category,
|
||||
})
|
||||
category_children = new Collection<ProductCategory>(this)
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at?: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at?: Date
|
||||
|
||||
@Index({ name: "IDX_product_category_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.categories)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@OnInit()
|
||||
async onInit() {
|
||||
this.id = generateEntityId(this.id, "pcat")
|
||||
this.parent_category_id ??= this.parent_category?.id ?? null
|
||||
}
|
||||
|
||||
@BeforeCreate()
|
||||
async onCreate(args: EventArgs<ProductCategory>) {
|
||||
this.id = generateEntityId(this.id, "pcat")
|
||||
this.parent_category_id ??= this.parent_category?.id ?? null
|
||||
|
||||
if (!this.handle && this.name) {
|
||||
this.handle = kebabCase(this.name)
|
||||
}
|
||||
|
||||
const { em } = args
|
||||
|
||||
let parentCategory: ProductCategory | null = null
|
||||
|
||||
if (this.parent_category_id) {
|
||||
parentCategory = await em.findOne(
|
||||
ProductCategory,
|
||||
this.parent_category_id
|
||||
)
|
||||
}
|
||||
|
||||
if (parentCategory) {
|
||||
this.mpath = `${parentCategory?.mpath}${this.id}.`
|
||||
} else {
|
||||
this.mpath = `${this.id}.`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductCategory
|
||||
81
packages/modules/product/src/models/product-collection.ts
Normal file
81
packages/modules/product/src/models/product-collection.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
kebabCase,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
|
||||
const collectionHandleIndexName = "IDX_collection_handle_unique"
|
||||
const collectionHandleIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: collectionHandleIndexName,
|
||||
tableName: "product_collection",
|
||||
columns: ["handle"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
collectionHandleIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_collection" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductCollection {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
handle?: string
|
||||
|
||||
@OneToMany(() => Product, (product) => product.collection)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_collection_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "pcol")
|
||||
|
||||
if (!this.handle && this.title) {
|
||||
this.handle = kebabCase(this.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductCollection
|
||||
75
packages/modules/product/src/models/product-image.ts
Normal file
75
packages/modules/product/src/models/product-image.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
|
||||
const imageUrlIndexName = "IDX_product_image_url"
|
||||
const imageUrlIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: imageUrlIndexName,
|
||||
tableName: "image",
|
||||
columns: ["url"],
|
||||
unique: false,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
imageUrlIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "image" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductImage {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
url: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_image_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.images)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@OnInit()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "img")
|
||||
}
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "img")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductImage
|
||||
87
packages/modules/product/src/models/product-option-value.ts
Normal file
87
packages/modules/product/src/models/product-option-value.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { ProductOption, ProductVariant } from "./index"
|
||||
|
||||
const optionValueOptionIdIndexName = "IDX_option_value_option_id_unique"
|
||||
const optionValueOptionIdIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: optionValueOptionIdIndexName,
|
||||
tableName: "product_option_value",
|
||||
columns: ["option_id", "value"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
optionValueOptionIdIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_option_value" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductOptionValue {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@ManyToOne(() => ProductOption, {
|
||||
columnType: "text",
|
||||
fieldName: "option_id",
|
||||
mapToPk: true,
|
||||
nullable: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
option_id: string | null
|
||||
|
||||
@ManyToOne(() => ProductOption, {
|
||||
nullable: true,
|
||||
persist: false,
|
||||
})
|
||||
option: ProductOption | null
|
||||
|
||||
@ManyToMany(() => ProductVariant, (variant) => variant.options)
|
||||
variants = new Collection<ProductVariant>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_option_value_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "optval")
|
||||
this.option_id ??= this.option?.id ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductOptionValue
|
||||
93
packages/modules/product/src/models/product-option.ts
Normal file
93
packages/modules/product/src/models/product-option.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { Product } from "./index"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
|
||||
const optionProductIdTitleIndexName = "IDX_option_product_id_title_unique"
|
||||
const optionProductIdTitleIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: optionProductIdTitleIndexName,
|
||||
tableName: "product_option",
|
||||
columns: ["product_id", "title"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
optionProductIdTitleIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_option" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductOption {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
columnType: "text",
|
||||
fieldName: "product_id",
|
||||
mapToPk: true,
|
||||
nullable: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
product_id: string | null
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
persist: false,
|
||||
nullable: true,
|
||||
})
|
||||
product: Product | null
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (value) => value.option, {
|
||||
cascade: [Cascade.PERSIST, "soft-remove" as any],
|
||||
})
|
||||
values = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_option_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "opt")
|
||||
this.product_id ??= this.product?.id ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductOption
|
||||
73
packages/modules/product/src/models/product-tag.ts
Normal file
73
packages/modules/product/src/models/product-tag.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
|
||||
const tagValueIndexName = "IDX_tag_value_unique"
|
||||
const tagValueIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: tagValueIndexName,
|
||||
tableName: "product_tag",
|
||||
columns: ["value"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
tagValueIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_tag" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductTag {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_tag_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.tags)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "ptag")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductTag
|
||||
67
packages/modules/product/src/models/product-type.ts
Normal file
67
packages/modules/product/src/models/product-type.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
const typeValueIndexName = "IDX_type_value_unique"
|
||||
const typeValueIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: typeValueIndexName,
|
||||
tableName: "product_type",
|
||||
columns: ["value"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
typeValueIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_type" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductType {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ columnType: "json", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_type_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "ptyp")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductType
|
||||
203
packages/modules/product/src/models/product-variant.ts
Normal file
203
packages/modules/product/src/models/product-variant.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
optionalNumericSerializer,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { Product, ProductOptionValue } from "@models"
|
||||
|
||||
const variantSkuIndexName = "IDX_product_variant_sku_unique"
|
||||
const variantSkuIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: variantSkuIndexName,
|
||||
tableName: "product_variant",
|
||||
columns: ["sku"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const variantBarcodeIndexName = "IDX_product_variant_barcode_unique"
|
||||
const variantBarcodeIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: variantBarcodeIndexName,
|
||||
tableName: "product_variant",
|
||||
columns: ["barcode"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const variantEanIndexName = "IDX_product_variant_ean_unique"
|
||||
const variantEanIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: variantEanIndexName,
|
||||
tableName: "product_variant",
|
||||
columns: ["ean"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const variantUpcIndexName = "IDX_product_variant_upc_unique"
|
||||
const variantUpcIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: variantUpcIndexName,
|
||||
tableName: "product_variant",
|
||||
columns: ["upc"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const variantProductIdIndexName = "IDX_product_variant_product_id"
|
||||
const variantProductIdIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: variantProductIdIndexName,
|
||||
tableName: "product_variant",
|
||||
columns: ["product_id"],
|
||||
unique: false,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
variantProductIdIndexStatement.MikroORMIndex()
|
||||
variantSkuIndexStatement.MikroORMIndex()
|
||||
variantBarcodeIndexStatement.MikroORMIndex()
|
||||
variantEanIndexStatement.MikroORMIndex()
|
||||
variantUpcIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_variant" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductVariant {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
sku?: string | null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
barcode?: string | null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
ean?: string | null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
upc?: string | null
|
||||
|
||||
// TODO: replace with BigNumber
|
||||
// Note: Upon serialization, this turns to a string. This is on purpose, because you would loose
|
||||
// precision if you cast numeric to JS number, as JS number is a float.
|
||||
// Ref: https://github.com/mikro-orm/mikro-orm/issues/2295
|
||||
@Property({
|
||||
columnType: "numeric",
|
||||
default: 100,
|
||||
serializer: optionalNumericSerializer,
|
||||
})
|
||||
inventory_quantity?: number = 100
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
allow_backorder?: boolean = false
|
||||
|
||||
@Property({ columnType: "boolean", default: true })
|
||||
manage_inventory?: boolean = true
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
hs_code?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
origin_country?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
mid_code?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
material?: string | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
weight?: number | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
length?: number | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
height?: number | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
width?: number | null
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
// TODO: replace with BigNumber, or in this case a normal int should work
|
||||
@Property({
|
||||
columnType: "numeric",
|
||||
nullable: true,
|
||||
default: 0,
|
||||
serializer: optionalNumericSerializer,
|
||||
})
|
||||
variant_rank?: number | null
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
onDelete: "cascade",
|
||||
fieldName: "product_id",
|
||||
mapToPk: true,
|
||||
})
|
||||
product_id: string | null
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
persist: false,
|
||||
nullable: true,
|
||||
})
|
||||
product: Product | null
|
||||
|
||||
@ManyToMany(() => ProductOptionValue, "variants", {
|
||||
owner: true,
|
||||
pivotTable: "product_variant_option",
|
||||
joinColumn: "variant_id",
|
||||
inverseJoinColumn: "option_value_id",
|
||||
})
|
||||
options = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_variant_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "variant")
|
||||
this.product_id ??= this.product?.id ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductVariant
|
||||
224
packages/modules/product/src/models/product.ts
Normal file
224
packages/modules/product/src/models/product.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Enum,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
kebabCase,
|
||||
ProductUtils,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import ProductCategory from "./product-category"
|
||||
import ProductCollection from "./product-collection"
|
||||
import ProductImage from "./product-image"
|
||||
import ProductOption from "./product-option"
|
||||
import ProductTag from "./product-tag"
|
||||
import ProductType from "./product-type"
|
||||
import ProductVariant from "./product-variant"
|
||||
|
||||
const productHandleIndexName = "IDX_product_handle_unique"
|
||||
const productHandleIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: productHandleIndexName,
|
||||
tableName: "product",
|
||||
columns: ["handle"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const productTypeIndexName = "IDX_product_type_id"
|
||||
const productTypeIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: productTypeIndexName,
|
||||
tableName: "product",
|
||||
columns: ["type_id"],
|
||||
unique: false,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
const productCollectionIndexName = "IDX_product_collection_id"
|
||||
const productCollectionIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: productCollectionIndexName,
|
||||
tableName: "product",
|
||||
columns: ["collection_id"],
|
||||
unique: false,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
productTypeIndexStatement.MikroORMIndex()
|
||||
productCollectionIndexStatement.MikroORMIndex()
|
||||
productHandleIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class Product {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
handle?: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
subtitle?: string | null
|
||||
|
||||
@Searchable()
|
||||
@Property({
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
})
|
||||
description?: string | null
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
is_giftcard!: boolean
|
||||
|
||||
@Enum(() => ProductUtils.ProductStatus)
|
||||
@Property({ default: ProductUtils.ProductStatus.DRAFT })
|
||||
status!: ProductUtils.ProductStatus
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
thumbnail?: string | null
|
||||
|
||||
@OneToMany(() => ProductOption, (o) => o.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
options = new Collection<ProductOption>(this)
|
||||
|
||||
@Searchable()
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
variants = new Collection<ProductVariant>(this)
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
weight?: number | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
length?: number | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
height?: number | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
width?: number | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
origin_country?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
hs_code?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
mid_code?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
material?: string | null
|
||||
|
||||
@Searchable()
|
||||
@ManyToOne(() => ProductCollection, {
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
fieldName: "collection_id",
|
||||
mapToPk: true,
|
||||
onDelete: "set null",
|
||||
})
|
||||
collection_id: string | null
|
||||
|
||||
@ManyToOne(() => ProductCollection, {
|
||||
nullable: true,
|
||||
persist: false,
|
||||
})
|
||||
collection: ProductCollection | null
|
||||
|
||||
@ManyToOne(() => ProductType, {
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
fieldName: "type_id",
|
||||
mapToPk: true,
|
||||
onDelete: "set null",
|
||||
})
|
||||
type_id: string | null
|
||||
|
||||
@ManyToOne(() => ProductType, {
|
||||
nullable: true,
|
||||
persist: false,
|
||||
})
|
||||
type: ProductType | null
|
||||
|
||||
@ManyToMany(() => ProductTag, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_tags",
|
||||
index: "IDX_product_tag_id",
|
||||
})
|
||||
tags = new Collection<ProductTag>(this)
|
||||
|
||||
@ManyToMany(() => ProductImage, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_images",
|
||||
joinColumn: "product_id",
|
||||
inverseJoinColumn: "image_id",
|
||||
})
|
||||
images = new Collection<ProductImage>(this)
|
||||
|
||||
@ManyToMany(() => ProductCategory, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_category_product",
|
||||
})
|
||||
categories = new Collection<ProductCategory>(this)
|
||||
|
||||
@Property({ columnType: "boolean", default: true })
|
||||
discountable: boolean
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
external_id?: string | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "prod")
|
||||
this.type_id ??= this.type?.id ?? null
|
||||
this.collection_id ??= this.collection?.id ?? null
|
||||
|
||||
if (!this.handle && this.title) {
|
||||
this.handle = kebabCase(this.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Product
|
||||
31
packages/modules/product/src/module-definition.ts
Normal file
31
packages/modules/product/src/module-definition.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ModuleExports } from "@medusajs/types"
|
||||
import { ProductModuleService } from "@services"
|
||||
import loadConnection from "./loaders/connection"
|
||||
import loadContainer from "./loaders/container"
|
||||
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import * as ProductModels from "@models"
|
||||
|
||||
const migrationScriptOptions = {
|
||||
moduleName: Modules.PRODUCT,
|
||||
models: ProductModels,
|
||||
pathToMigrations: __dirname + "/migrations",
|
||||
}
|
||||
|
||||
export const runMigrations = ModulesSdkUtils.buildMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
const service = ProductModuleService
|
||||
const loaders = [loadContainer, loadConnection] as any
|
||||
|
||||
export const moduleDefinition: ModuleExports = {
|
||||
service,
|
||||
loaders,
|
||||
runMigrations,
|
||||
revertMigration,
|
||||
}
|
||||
3
packages/modules/product/src/repositories/index.ts
Normal file
3
packages/modules/product/src/repositories/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
|
||||
export { ProductRepository } from "./product"
|
||||
export { ProductCategoryRepository } from "./product-category"
|
||||
471
packages/modules/product/src/repositories/product-category.ts
Normal file
471
packages/modules/product/src/repositories/product-category.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
ProductCategoryTransformOptions,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import { DALUtils, MedusaError, isDefined } from "@medusajs/utils"
|
||||
import {
|
||||
LoadStrategy,
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
} from "@mikro-orm/core"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductCategory } from "@models"
|
||||
|
||||
export type ReorderConditions = {
|
||||
targetCategoryId: string
|
||||
originalParentId: string | null
|
||||
targetParentId: string | null | undefined
|
||||
originalRank: number
|
||||
targetRank: number | undefined
|
||||
shouldChangeParent: boolean
|
||||
shouldChangeRank: boolean
|
||||
shouldIncrementRank: boolean
|
||||
shouldDeleteElement: boolean
|
||||
}
|
||||
|
||||
export const tempReorderRank = 99999
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<ProductCategory> {
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
transformOptions: ProductCategoryTransformOptions = {},
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
const { includeDescendantsTree, includeAncestorsTree } = transformOptions
|
||||
findOptions_.options ??= {}
|
||||
const fields = (findOptions_.options.fields ??= [])
|
||||
|
||||
// Ref: Building descendants
|
||||
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
|
||||
if (includeDescendantsTree || includeAncestorsTree) {
|
||||
fields.indexOf("mpath") === -1 && fields.push("mpath")
|
||||
fields.indexOf("parent_category_id") === -1 &&
|
||||
fields.push("parent_category_id")
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
const productCategories = await manager.find(
|
||||
ProductCategory,
|
||||
findOptions_.where as MikroFilterQuery<ProductCategory>,
|
||||
findOptions_.options as MikroOptions<ProductCategory>
|
||||
)
|
||||
|
||||
if (!includeDescendantsTree && !includeAncestorsTree) {
|
||||
return productCategories
|
||||
}
|
||||
|
||||
return this.buildProductCategoriesWithTree(
|
||||
{
|
||||
descendants: includeDescendantsTree,
|
||||
ancestors: includeAncestorsTree,
|
||||
},
|
||||
productCategories,
|
||||
findOptions_
|
||||
)
|
||||
}
|
||||
|
||||
async buildProductCategoriesWithTree(
|
||||
include: {
|
||||
descendants?: boolean
|
||||
ancestors?: boolean
|
||||
},
|
||||
productCategories: ProductCategory[],
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const hasPopulateParentCategory = (
|
||||
findOptions.options?.populate ?? ([] as any)
|
||||
).find((pop) => pop.field === "parent_category")
|
||||
|
||||
include.ancestors = include.ancestors || hasPopulateParentCategory
|
||||
|
||||
const mpaths: any[] = []
|
||||
const parentMpaths = new Set()
|
||||
for (const cat of productCategories) {
|
||||
if (include.descendants) {
|
||||
mpaths.push({ mpath: { $like: `${cat.mpath}%` } })
|
||||
}
|
||||
|
||||
if (include.ancestors) {
|
||||
let parent = ""
|
||||
cat.mpath?.split(".").forEach((mpath) => {
|
||||
if (mpath === "") {
|
||||
return
|
||||
}
|
||||
parentMpaths.add(parent + mpath + ".")
|
||||
parent += mpath + "."
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mpaths.push({ mpath: Array.from(parentMpaths) })
|
||||
|
||||
const whereOptions = {
|
||||
...findOptions.where,
|
||||
$or: mpaths,
|
||||
}
|
||||
|
||||
if ("parent_category_id" in whereOptions) {
|
||||
delete whereOptions.parent_category_id
|
||||
}
|
||||
|
||||
if ("id" in whereOptions) {
|
||||
delete whereOptions.id
|
||||
}
|
||||
|
||||
let allCategories = await manager.find(
|
||||
ProductCategory,
|
||||
whereOptions as MikroFilterQuery<ProductCategory>,
|
||||
findOptions.options as MikroOptions<ProductCategory>
|
||||
)
|
||||
|
||||
allCategories = JSON.parse(JSON.stringify(allCategories))
|
||||
|
||||
const categoriesById = new Map(allCategories.map((cat) => [cat.id, cat]))
|
||||
|
||||
allCategories.forEach((cat: any) => {
|
||||
if (cat.parent_category_id) {
|
||||
cat.parent_category = categoriesById.get(cat.parent_category_id)
|
||||
}
|
||||
})
|
||||
|
||||
const populateChildren = (category, level = 0) => {
|
||||
const categories = allCategories.filter(
|
||||
(child) => child.parent_category_id === category.id
|
||||
)
|
||||
|
||||
if (include.descendants) {
|
||||
category.category_children = categories.map((child) => {
|
||||
return populateChildren(categoriesById.get(child.id), level + 1)
|
||||
})
|
||||
}
|
||||
|
||||
if (level === 0) {
|
||||
return category
|
||||
}
|
||||
|
||||
if (include.ancestors) {
|
||||
delete category.category_children
|
||||
}
|
||||
if (include.descendants) {
|
||||
delete category.parent_category
|
||||
}
|
||||
|
||||
return category
|
||||
}
|
||||
|
||||
const populatedProductCategories = productCategories.map((cat) => {
|
||||
const fullCategory = categoriesById.get(cat.id)
|
||||
return populateChildren(fullCategory)
|
||||
})
|
||||
|
||||
return populatedProductCategories
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
transformOptions: ProductCategoryTransformOptions = {},
|
||||
context: Context = {}
|
||||
): Promise<[ProductCategory[], number]> {
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
const { includeDescendantsTree, includeAncestorsTree } = transformOptions
|
||||
findOptions_.options ??= {}
|
||||
const fields = (findOptions_.options.fields ??= [])
|
||||
|
||||
// Ref: Building descendants
|
||||
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
|
||||
if (includeDescendantsTree) {
|
||||
fields.indexOf("mpath") === -1 && fields.push("mpath")
|
||||
fields.indexOf("parent_category_id") === -1 &&
|
||||
fields.push("parent_category_id")
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
const [productCategories, count] = await manager.findAndCount(
|
||||
ProductCategory,
|
||||
findOptions_.where as MikroFilterQuery<ProductCategory>,
|
||||
findOptions_.options as MikroOptions<ProductCategory>
|
||||
)
|
||||
if (!includeDescendantsTree) {
|
||||
return [productCategories, count]
|
||||
}
|
||||
|
||||
if (!includeDescendantsTree && !includeAncestorsTree) {
|
||||
return [productCategories, count]
|
||||
}
|
||||
|
||||
return [
|
||||
await this.buildProductCategoriesWithTree(
|
||||
{
|
||||
descendants: includeDescendantsTree,
|
||||
ancestors: includeAncestorsTree,
|
||||
},
|
||||
productCategories,
|
||||
findOptions_
|
||||
),
|
||||
count,
|
||||
]
|
||||
}
|
||||
|
||||
async delete(id: string, context: Context = {}): Promise<void> {
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
const productCategory = await manager.findOneOrFail(
|
||||
ProductCategory,
|
||||
{ id },
|
||||
{
|
||||
populate: ["category_children"],
|
||||
}
|
||||
)
|
||||
|
||||
if (productCategory.category_children.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Deleting ProductCategory (${id}) with category children is not allowed`
|
||||
)
|
||||
}
|
||||
|
||||
const conditions = this.fetchReorderConditions(
|
||||
productCategory,
|
||||
{
|
||||
parent_category_id: productCategory.parent_category_id,
|
||||
rank: productCategory.rank,
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
await this.performReordering(manager, conditions)
|
||||
await manager.nativeDelete(ProductCategory, { id: id }, {})
|
||||
}
|
||||
|
||||
async create(
|
||||
data: ProductTypes.CreateProductCategoryDTO,
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory> {
|
||||
const categoryData = { ...data }
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
const siblings = await manager.find(ProductCategory, {
|
||||
parent_category_id: categoryData?.parent_category_id || null,
|
||||
})
|
||||
|
||||
if (!isDefined(categoryData.rank)) {
|
||||
categoryData.rank = siblings.length
|
||||
}
|
||||
|
||||
const productCategory = manager.create(ProductCategory, categoryData)
|
||||
|
||||
manager.persist(productCategory)
|
||||
|
||||
return productCategory
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: ProductTypes.UpdateProductCategoryDTO,
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory> {
|
||||
const categoryData = { ...data }
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
const productCategory = await manager.findOneOrFail(ProductCategory, { id })
|
||||
|
||||
const conditions = this.fetchReorderConditions(
|
||||
productCategory,
|
||||
categoryData
|
||||
)
|
||||
|
||||
if (conditions.shouldChangeRank || conditions.shouldChangeParent) {
|
||||
categoryData.rank = tempReorderRank
|
||||
}
|
||||
|
||||
// await this.transformParentIdToEntity(categoryData)
|
||||
|
||||
for (const key in categoryData) {
|
||||
if (isDefined(categoryData[key])) {
|
||||
productCategory[key] = categoryData[key]
|
||||
}
|
||||
}
|
||||
|
||||
manager.assign(productCategory, categoryData)
|
||||
manager.persist(productCategory)
|
||||
|
||||
await this.performReordering(manager, conditions)
|
||||
|
||||
return productCategory
|
||||
}
|
||||
|
||||
protected fetchReorderConditions(
|
||||
productCategory: ProductCategory,
|
||||
data: ProductTypes.UpdateProductCategoryDTO,
|
||||
shouldDeleteElement = false
|
||||
): ReorderConditions {
|
||||
const originalParentId = productCategory.parent_category_id || null
|
||||
const targetParentId = data.parent_category_id
|
||||
const originalRank = productCategory.rank || 0
|
||||
const targetRank = data.rank
|
||||
const shouldChangeParent =
|
||||
targetParentId !== undefined && targetParentId !== originalParentId
|
||||
const shouldChangeRank =
|
||||
shouldChangeParent ||
|
||||
(isDefined(targetRank) && originalRank !== targetRank)
|
||||
|
||||
return {
|
||||
targetCategoryId: productCategory.id,
|
||||
originalParentId,
|
||||
targetParentId,
|
||||
originalRank,
|
||||
targetRank,
|
||||
shouldChangeParent,
|
||||
shouldChangeRank,
|
||||
shouldIncrementRank: false,
|
||||
shouldDeleteElement,
|
||||
}
|
||||
}
|
||||
|
||||
protected async performReordering(
|
||||
manager: SqlEntityManager,
|
||||
conditions: ReorderConditions
|
||||
): Promise<void> {
|
||||
const { shouldChangeParent, shouldChangeRank, shouldDeleteElement } =
|
||||
conditions
|
||||
|
||||
if (!(shouldChangeParent || shouldChangeRank || shouldDeleteElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we change parent, we need to shift the siblings to eliminate the
|
||||
// rank occupied by the targetCategory in the original parent.
|
||||
shouldChangeParent &&
|
||||
(await this.shiftSiblings(manager, {
|
||||
...conditions,
|
||||
targetRank: conditions.originalRank,
|
||||
targetParentId: conditions.originalParentId,
|
||||
}))
|
||||
|
||||
// If we change parent, we need to shift the siblings of the new parent
|
||||
// to create a rank that the targetCategory will occupy.
|
||||
shouldChangeParent &&
|
||||
shouldChangeRank &&
|
||||
(await this.shiftSiblings(manager, {
|
||||
...conditions,
|
||||
shouldIncrementRank: true,
|
||||
}))
|
||||
|
||||
// If we only change rank, we need to shift the siblings
|
||||
// to create a rank that the targetCategory will occupy.
|
||||
;((!shouldChangeParent && shouldChangeRank) || shouldDeleteElement) &&
|
||||
(await this.shiftSiblings(manager, {
|
||||
...conditions,
|
||||
targetParentId: conditions.originalParentId,
|
||||
}))
|
||||
}
|
||||
|
||||
protected async shiftSiblings(
|
||||
manager: SqlEntityManager,
|
||||
conditions: ReorderConditions
|
||||
): Promise<void> {
|
||||
let { shouldIncrementRank, targetRank } = conditions
|
||||
const {
|
||||
shouldChangeParent,
|
||||
originalRank,
|
||||
targetParentId,
|
||||
targetCategoryId,
|
||||
shouldDeleteElement,
|
||||
} = conditions
|
||||
|
||||
// The current sibling count will replace targetRank if
|
||||
// targetRank is greater than the count of siblings.
|
||||
const siblingCount = await manager.count(ProductCategory, {
|
||||
parent_category_id: targetParentId || null,
|
||||
id: { $ne: targetCategoryId },
|
||||
})
|
||||
|
||||
// The category record that will be placed at the requested rank
|
||||
// We've temporarily placed it at a temporary rank that is
|
||||
// beyond a reasonable value (tempReorderRank)
|
||||
const targetCategory = await manager.findOne(ProductCategory, {
|
||||
id: targetCategoryId,
|
||||
parent_category_id: targetParentId || null,
|
||||
rank: tempReorderRank,
|
||||
})
|
||||
|
||||
// If the targetRank is not present, or if targetRank is beyond the
|
||||
// rank of the last category, we set the rank as the last rank
|
||||
if (targetRank === undefined || targetRank > siblingCount) {
|
||||
targetRank = siblingCount
|
||||
}
|
||||
|
||||
let rankCondition
|
||||
|
||||
// If parent doesn't change, we only need to get the ranks
|
||||
// in between the original rank and the target rank.
|
||||
if (shouldChangeParent || shouldDeleteElement) {
|
||||
rankCondition = { $gte: targetRank }
|
||||
} else if (originalRank > targetRank) {
|
||||
shouldIncrementRank = true
|
||||
rankCondition = { $gte: targetRank, $lte: originalRank }
|
||||
} else {
|
||||
shouldIncrementRank = false
|
||||
rankCondition = { $gte: originalRank, $lte: targetRank }
|
||||
}
|
||||
|
||||
// Scope out the list of siblings that we need to shift up or down
|
||||
const siblingsToShift = await manager.find(
|
||||
ProductCategory,
|
||||
{
|
||||
parent_category_id: targetParentId || null,
|
||||
rank: rankCondition,
|
||||
id: { $ne: targetCategoryId },
|
||||
},
|
||||
{
|
||||
orderBy: { rank: shouldIncrementRank ? "DESC" : "ASC" },
|
||||
}
|
||||
)
|
||||
|
||||
// Depending on the conditions, we get a subset of the siblings
|
||||
// and independently shift them up or down a rank
|
||||
for (let index = 0; index < siblingsToShift.length; index++) {
|
||||
const sibling = siblingsToShift[index]
|
||||
|
||||
// Depending on the condition, we could also have the targetCategory
|
||||
// in the siblings list, we skip shifting the target until all other siblings
|
||||
// have been shifted.
|
||||
if (sibling.id === targetCategoryId) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!isDefined(sibling.rank)) {
|
||||
throw new Error("sibling rank is not defined")
|
||||
}
|
||||
|
||||
const rank = shouldIncrementRank ? ++sibling.rank! : --sibling.rank!
|
||||
|
||||
manager.assign(sibling, { rank })
|
||||
manager.persist(sibling)
|
||||
}
|
||||
|
||||
// The targetCategory will not be present in the query when we are shifting
|
||||
// siblings of the old parent of the targetCategory.
|
||||
if (!targetCategory) {
|
||||
return
|
||||
}
|
||||
|
||||
// Place the targetCategory in the requested rank
|
||||
manager.assign(targetCategory, { rank: targetRank })
|
||||
manager.persist(targetCategory)
|
||||
}
|
||||
}
|
||||
57
packages/modules/product/src/repositories/product.ts
Normal file
57
packages/modules/product/src/repositories/product.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Product } from "@models"
|
||||
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { DALUtils } from "@medusajs/utils"
|
||||
|
||||
import { UpdateProductInput } from "../types"
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Product>(
|
||||
Product
|
||||
) {
|
||||
constructor(...args: any[]) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to be able to have a strict not in categories, and prevent a product
|
||||
* to be return in the case it also belongs to other categories, we need to
|
||||
* first find all products that are in the categories, and then exclude them
|
||||
*/
|
||||
protected async mutateNotInCategoriesConstraints(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<void> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
if (
|
||||
"categories" in findOptions.where &&
|
||||
findOptions.where.categories?.id?.["$nin"]
|
||||
) {
|
||||
const productsInCategories = await manager.find(
|
||||
Product,
|
||||
{
|
||||
categories: {
|
||||
id: { $in: findOptions.where.categories.id["$nin"] },
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: ["id"],
|
||||
}
|
||||
)
|
||||
|
||||
const productIds = productsInCategories.map((product) => product.id)
|
||||
|
||||
if (productIds.length) {
|
||||
findOptions.where.id = { $nin: productIds }
|
||||
delete findOptions.where.categories?.id
|
||||
|
||||
if (Object.keys(findOptions.where.categories).length === 0) {
|
||||
delete findOptions.where.categories
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
packages/modules/product/src/scripts/bin/run-seed.ts
Normal file
37
packages/modules/product/src/scripts/bin/run-seed.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import * as ProductModels from "@models"
|
||||
import {
|
||||
createProductCategories,
|
||||
createProducts,
|
||||
createProductVariants,
|
||||
} from "../seed-utils"
|
||||
import { EOL } from "os"
|
||||
|
||||
const args = process.argv
|
||||
const path = args.pop() as string
|
||||
|
||||
export default (async () => {
|
||||
const { config } = await import("dotenv")
|
||||
config()
|
||||
if (!path) {
|
||||
throw new Error(
|
||||
`filePath is required.${EOL}Example: medusa-product-seed <filePath>`
|
||||
)
|
||||
}
|
||||
|
||||
const run = ModulesSdkUtils.buildSeedScript({
|
||||
moduleName: Modules.PRODUCT,
|
||||
models: ProductModels,
|
||||
pathToMigrations: __dirname + "/../../migrations",
|
||||
seedHandler: async ({ manager, data }) => {
|
||||
const { productCategoriesData, productsData, variantsData } = data
|
||||
await createProductCategories(manager, productCategoriesData)
|
||||
await createProducts(manager, productsData)
|
||||
await createProductVariants(manager, variantsData)
|
||||
},
|
||||
})
|
||||
await run({ path })
|
||||
})()
|
||||
54
packages/modules/product/src/scripts/seed-utils.ts
Normal file
54
packages/modules/product/src/scripts/seed-utils.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { Product, ProductCategory, ProductVariant } from "@models"
|
||||
|
||||
export async function createProductCategories(
|
||||
manager: SqlEntityManager,
|
||||
categoriesData: any[]
|
||||
): Promise<ProductCategory[]> {
|
||||
const categories: ProductCategory[] = []
|
||||
|
||||
for (let categoryData of categoriesData) {
|
||||
let categoryDataClone = { ...categoryData }
|
||||
let parentCategory: ProductCategory | null = null
|
||||
const parentCategoryId = categoryDataClone.parent_category_id as string
|
||||
delete categoryDataClone.parent_category_id
|
||||
|
||||
if (parentCategoryId) {
|
||||
parentCategory = await manager.findOne(ProductCategory, parentCategoryId)
|
||||
}
|
||||
|
||||
const category = manager.create(ProductCategory, {
|
||||
...categoryDataClone,
|
||||
parent_category: parentCategory,
|
||||
})
|
||||
|
||||
categories.push(category)
|
||||
}
|
||||
|
||||
await manager.persistAndFlush(categories)
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
export async function createProducts(manager: SqlEntityManager, data: any[]) {
|
||||
const products: any[] = data.map((productData) => {
|
||||
return manager.create(Product, productData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(products)
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
export async function createProductVariants(
|
||||
manager: SqlEntityManager,
|
||||
data: any[]
|
||||
) {
|
||||
const variants: any[] = data.map((variantsData) => {
|
||||
return manager.create(ProductVariant, variantsData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(variants)
|
||||
|
||||
return variants
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { asValue } from "awilix"
|
||||
|
||||
export const nonExistingProductId = "non-existing-id"
|
||||
|
||||
export const productRepositoryMock = {
|
||||
productRepository: asValue({
|
||||
find: jest.fn().mockImplementation(async ({ where: { id } }) => {
|
||||
if (id === nonExistingProductId) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{}]
|
||||
}),
|
||||
findAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
getFreshManager: jest.fn().mockResolvedValue({}),
|
||||
}),
|
||||
}
|
||||
227
packages/modules/product/src/services/__tests__/product.spec.ts
Normal file
227
packages/modules/product/src/services/__tests__/product.spec.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
nonExistingProductId,
|
||||
productRepositoryMock,
|
||||
} from "../__fixtures__/product"
|
||||
import { createMedusaContainer } from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import ContainerLoader from "../../loaders/container"
|
||||
|
||||
describe("Product service", function () {
|
||||
let container
|
||||
|
||||
beforeEach(async function () {
|
||||
jest.clearAllMocks()
|
||||
|
||||
container = createMedusaContainer()
|
||||
container.register("manager", asValue({}))
|
||||
|
||||
await ContainerLoader({ container })
|
||||
|
||||
container.register(productRepositoryMock)
|
||||
})
|
||||
|
||||
it("should retrieve a product", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
const productId = "existing-product"
|
||||
|
||||
await productService.retrieve(productId)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
id: productId,
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: 0,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to retrieve a product", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const err = await productService
|
||||
.retrieve(nonExistingProductId)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
id: nonExistingProductId,
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: 0,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
expect(err.message).toBe(
|
||||
`Product with id: ${nonExistingProductId} was not found`
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const filters = {}
|
||||
const config = {
|
||||
relations: [],
|
||||
}
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
orderBy: {
|
||||
id: "ASC",
|
||||
},
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products with filters", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: [],
|
||||
}
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
orderBy: {
|
||||
id: "ASC",
|
||||
},
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products with filters and relations", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: ["tags"],
|
||||
}
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
orderBy: {
|
||||
id: "ASC",
|
||||
},
|
||||
withDeleted: undefined,
|
||||
populate: ["tags"],
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it("should list and count the products with filters and relations", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: ["tags"],
|
||||
}
|
||||
|
||||
await productService.listAndCount(filters, config)
|
||||
|
||||
expect(productRepository.findAndCount).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
orderBy: {
|
||||
id: "ASC",
|
||||
},
|
||||
withDeleted: undefined,
|
||||
populate: ["tags"],
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
3
packages/modules/product/src/services/index.ts
Normal file
3
packages/modules/product/src/services/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ProductService } from "./product"
|
||||
export { default as ProductCategoryService } from "./product-category"
|
||||
export { default as ProductModuleService } from "./product-module-service"
|
||||
169
packages/modules/product/src/services/product-category.ts
Normal file
169
packages/modules/product/src/services/product-category.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import {
|
||||
FreeTextSearchFilterKey,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
isDefined,
|
||||
} from "@medusajs/utils"
|
||||
import { ProductCategory } from "@models"
|
||||
import { ProductCategoryRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productCategoryRepository: DAL.TreeRepositoryService
|
||||
}
|
||||
|
||||
export default class ProductCategoryService<
|
||||
TEntity extends ProductCategory = ProductCategory
|
||||
> {
|
||||
protected readonly productCategoryRepository_: DAL.TreeRepositoryService
|
||||
|
||||
constructor({ productCategoryRepository }: InjectedDependencies) {
|
||||
this.productCategoryRepository_ = productCategoryRepository
|
||||
}
|
||||
|
||||
@InjectManager("productCategoryRepository_")
|
||||
async retrieve(
|
||||
productCategoryId: string,
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
if (!isDefined(productCategoryId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"productCategoryId" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
{
|
||||
id: productCategoryId,
|
||||
},
|
||||
config
|
||||
)
|
||||
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: true,
|
||||
}
|
||||
|
||||
const productCategories = await this.productCategoryRepository_.find(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!productCategories?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`ProductCategory with id: ${productCategoryId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return productCategories[0] as TEntity
|
||||
}
|
||||
|
||||
@InjectManager("productCategoryRepository_")
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false,
|
||||
includeAncestorsTree: filters?.include_ancestors_tree || false,
|
||||
}
|
||||
delete filters.include_descendants_tree
|
||||
delete filters.include_ancestors_tree
|
||||
|
||||
// Apply free text search filter
|
||||
if (filters?.q) {
|
||||
config.filters ??= {}
|
||||
config.filters[FreeTextSearchFilterKey] = {
|
||||
value: filters.q,
|
||||
fromEntity: ProductCategory.name,
|
||||
}
|
||||
|
||||
delete filters.q
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
queryOptions.where ??= {}
|
||||
|
||||
return (await this.productCategoryRepository_.find(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectManager("productCategoryRepository_")
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false,
|
||||
includeAncestorsTree: filters?.include_ancestors_tree || false,
|
||||
}
|
||||
delete filters.include_descendants_tree
|
||||
delete filters.include_ancestors_tree
|
||||
|
||||
// Apply free text search filter
|
||||
if (filters?.q) {
|
||||
config.filters ??= {}
|
||||
config.filters[FreeTextSearchFilterKey] = {
|
||||
value: filters.q,
|
||||
fromEntity: ProductCategory.name,
|
||||
}
|
||||
|
||||
delete filters.q
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
queryOptions.where ??= {}
|
||||
|
||||
return (await this.productCategoryRepository_.findAndCount(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
@InjectTransactionManager("productCategoryRepository_")
|
||||
async create(
|
||||
data: ProductTypes.CreateProductCategoryDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
return (await (
|
||||
this.productCategoryRepository_ as unknown as ProductCategoryRepository
|
||||
).create(data, sharedContext)) as TEntity
|
||||
}
|
||||
|
||||
@InjectTransactionManager("productCategoryRepository_")
|
||||
async update(
|
||||
id: string,
|
||||
data: ProductTypes.UpdateProductCategoryDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
return (await (
|
||||
this.productCategoryRepository_ as unknown as ProductCategoryRepository
|
||||
).update(id, data, sharedContext)) as TEntity
|
||||
}
|
||||
|
||||
@InjectTransactionManager("productCategoryRepository_")
|
||||
async delete(
|
||||
id: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.productCategoryRepository_.delete(id, sharedContext)
|
||||
}
|
||||
}
|
||||
1463
packages/modules/product/src/services/product-module-service.ts
Normal file
1463
packages/modules/product/src/services/product-module-service.ts
Normal file
File diff suppressed because it is too large
Load Diff
82
packages/modules/product/src/services/product.ts
Normal file
82
packages/modules/product/src/services/product.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
FindConfig,
|
||||
ProductTypes,
|
||||
BaseFilterable,
|
||||
FilterableProductProps,
|
||||
} from "@medusajs/types"
|
||||
import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { Product } from "@models"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
type NormalizedFilterableProductProps = ProductTypes.FilterableProductProps & {
|
||||
categories?: {
|
||||
id: string | { $in: string[] }
|
||||
}
|
||||
}
|
||||
|
||||
export default class ProductService<
|
||||
TEntity extends Product = Product
|
||||
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
|
||||
Product
|
||||
)<TEntity> {
|
||||
protected readonly productRepository_: DAL.RepositoryService<TEntity>
|
||||
|
||||
constructor({ productRepository }: InjectedDependencies) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(...arguments)
|
||||
|
||||
this.productRepository_ = productRepository
|
||||
}
|
||||
|
||||
@InjectManager("productRepository_")
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<TEntity> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await super.list(
|
||||
ProductService.normalizeFilters(filters),
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager("productRepository_")
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<any> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
return await super.listAndCount(
|
||||
ProductService.normalizeFilters(filters),
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
protected static normalizeFilters(
|
||||
filters: FilterableProductProps = {}
|
||||
): NormalizedFilterableProductProps {
|
||||
const normalized = filters as NormalizedFilterableProductProps
|
||||
if (normalized.category_id) {
|
||||
if (Array.isArray(normalized.category_id)) {
|
||||
normalized.categories = {
|
||||
id: { $in: normalized.category_id },
|
||||
}
|
||||
} else {
|
||||
normalized.categories = {
|
||||
id: normalized.category_id as string,
|
||||
}
|
||||
}
|
||||
delete normalized.category_id
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
67
packages/modules/product/src/types/index.ts
Normal file
67
packages/modules/product/src/types/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { IEventBusModuleService, Logger, ProductTypes } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
eventBusModuleService?: IEventBusModuleService
|
||||
}
|
||||
|
||||
export type ProductCategoryEventData = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum ProductCategoryEvents {
|
||||
CATEGORY_UPDATED = "product-category.updated",
|
||||
CATEGORY_CREATED = "product-category.created",
|
||||
CATEGORY_DELETED = "product-category.deleted",
|
||||
}
|
||||
|
||||
export type ProductEventData = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum ProductEvents {
|
||||
PRODUCT_UPDATED = "product.updated",
|
||||
PRODUCT_CREATED = "product.created",
|
||||
PRODUCT_DELETED = "product.deleted",
|
||||
}
|
||||
|
||||
export type ProductCollectionEventData = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum ProductCollectionEvents {
|
||||
COLLECTION_UPDATED = "product-collection.updated",
|
||||
COLLECTION_CREATED = "product-collection.created",
|
||||
COLLECTION_DELETED = "product-collection.deleted",
|
||||
}
|
||||
|
||||
export type UpdateProductInput = ProductTypes.UpdateProductDTO & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type UpdateProductCollection =
|
||||
ProductTypes.UpdateProductCollectionDTO & {
|
||||
products?: string[]
|
||||
}
|
||||
|
||||
export type CreateProductCollection =
|
||||
ProductTypes.CreateProductCollectionDTO & {
|
||||
products?: string[]
|
||||
}
|
||||
|
||||
export type UpdateCollectionInput = ProductTypes.UpdateProductCollectionDTO & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type UpdateTypeInput = ProductTypes.UpdateProductTypeDTO & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type UpdateProductVariantInput = ProductTypes.UpdateProductVariantDTO & {
|
||||
id: string
|
||||
product_id?: string | null
|
||||
}
|
||||
|
||||
export type UpdateProductOptionInput = ProductTypes.UpdateProductOptionDTO & {
|
||||
id: string
|
||||
}
|
||||
7
packages/modules/product/src/utils/index.ts
Normal file
7
packages/modules/product/src/utils/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function shouldForceTransaction(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
export function doNotForceTransaction(): boolean {
|
||||
return false
|
||||
}
|
||||
37
packages/modules/product/tsconfig.json
Normal file
37
packages/modules/product/tsconfig.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2020"],
|
||||
"target": "es2020",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": false,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true, // to use ES5 specific tooling
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@models": ["./src/models"],
|
||||
"@services": ["./src/services"],
|
||||
"@repositories": ["./src/repositories"],
|
||||
"@types": ["./src/types"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"./src/**/__tests__",
|
||||
"./src/**/__mocks__",
|
||||
"./src/**/__fixtures__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
8
packages/modules/product/tsconfig.spec.json
Normal file
8
packages/modules/product/tsconfig.spec.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src", "integration-tests"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user