chore(): Reorganize modules (#7210)

**What**
Move all modules to the modules directory
This commit is contained in:
Adrien de Peretti
2024-05-02 17:33:34 +02:00
committed by GitHub
parent 7a351eef09
commit 4eae25e1ef
870 changed files with 91 additions and 62 deletions

6
packages/modules/product/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View 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

View 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 youre using a Medusa database schema, youll receive the products in your database. Otherwise, the request will return an empty array.

View File

@@ -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 {}

View File

@@ -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",
},
]`)

View File

@@ -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
}

View File

@@ -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",
},
]

View File

@@ -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
}
}

View File

@@ -0,0 +1,2 @@
export * from "./categories"
export * from "./products"

View File

@@ -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,
},
]

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -0,0 +1 @@
export * from "./data/create-variant"

View File

@@ -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")
})
})
})
},
})

View File

@@ -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",
}),
])
)
})
})
})
},
})

View File

@@ -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 },
},
])
})
})
})
},
})

View File

@@ -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,
}),
})
)
})
})
})
},
})

View File

@@ -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")
})
})
})
},
})

View File

@@ -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")
})
})
})
},
})

View File

@@ -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()
}
})
})
})
},
})

View File

@@ -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([])
})
})
})
},
})

View File

@@ -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,
}),
}),
])
)
})
})
})
},
})

View File

@@ -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")
})
})
})
},
})

View File

@@ -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")
})
})
})
},
})

View File

@@ -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")
})
})
})
},
})

View File

@@ -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()
})
})
})
},
})

View 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/"],
}

View 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,
},
}

View 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"
}
}

View 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"

View 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]
}

View 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",
},
},
],
}

View 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,
})
}

View 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,
})

View File

@@ -0,0 +1,2 @@
export * from "./connection"
export * from "./container"

File diff suppressed because it is too large Load Diff

View File

@@ -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;');
}
}

View 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"

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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,
}

View File

@@ -0,0 +1,3 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
export { ProductRepository } from "./product"
export { ProductCategoryRepository } from "./product-category"

View 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)
}
}

View 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
}
}
}
}
}

View 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 })
})()

View 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
}

View File

@@ -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({}),
}),
}

View 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)
)
})
})

View 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"

View 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)
}
}

File diff suppressed because it is too large Load Diff

View 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
}
}

View 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
}

View File

@@ -0,0 +1,7 @@
export function shouldForceTransaction(): boolean {
return true
}
export function doNotForceTransaction(): boolean {
return false
}

View 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"
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["src", "integration-tests"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"sourceMap": true
}
}