mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
823 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e81636ea95 | |||
| 5417aa174e | |||
| 6093f4f46f | |||
| 2178e0ee8b | |||
| be39e8a391 | |||
| b57f362e3a | |||
| e055e477ae | |||
| 422f7893b5 | |||
| 0a50f9850c | |||
| 42f340b189 | |||
| ef41b71921 | |||
| 2847e64b77 | |||
| 28390b0b84 | |||
| 0b58cadbd0 | |||
| 574b37abcd | |||
| 0dbf71e6b5 | |||
| fd1f0f7c54 | |||
| ebad3d15ec | |||
| 2fd5e71574 | |||
| b787837dd2 | |||
| 1fc6e810ae | |||
| 88cc406002 | |||
| 511f639d7a | |||
| 0df17221df | |||
| 347fe0f6ce | |||
| f3c6cd7620 | |||
| 649c465bd0 | |||
| 67604b344a | |||
| aedae8e757 | |||
| 37c36e5d07 | |||
| 166409fdf4 | |||
| f2aad9caac | |||
| 292e4595ea | |||
| 869031bbfa | |||
| 88e5c06e9d | |||
| cfb4b0e336 | |||
| 2d56b54e0c | |||
| 5d4a77fb10 | |||
| eb0822bbff | |||
| 38902ebcba | |||
| 22e43c2523 | |||
| e68e5792be | |||
| 511c6539fb | |||
| 9070f96571 | |||
| 0347efa592 | |||
| 1b8ce41837 | |||
| 6624927286 | |||
| 803f42a72b | |||
| 7e5c28d621 | |||
| 2f88c2216c | |||
| b89c090f87 | |||
| f4a7cde487 | |||
| a2d83d01a4 | |||
| 66392d094d | |||
| 0352c72fb6 | |||
| 3f9be3a418 | |||
| fd3dd310a5 | |||
| e6ff5c3f0b | |||
| 5377ac1a41 | |||
| 0e870cda81 | |||
| b784e1c1c4 | |||
| 90f93caff4 | |||
| d6dcfe3acc | |||
| b974fe7ec7 | |||
| 6483ff74bb | |||
| 27e2064f0c | |||
| 5cf91bb35a | |||
| 81533d855a | |||
| db58911df3 | |||
| 2540c83382 | |||
| 569d021316 | |||
| ba9713587c | |||
| eecaeb7c06 | |||
| 610e263477 | |||
| 0f54e8267f | |||
| 1fdee8435d | |||
| 1eeb23232a | |||
| 6102759da8 | |||
| 6cea581e42 | |||
| 5e553a7fce | |||
| 5e71fc8947 | |||
| d1ba638cd5 | |||
| d31416f85d | |||
| 55082e9d0e | |||
| 0d74625270 | |||
| 800f14ede2 | |||
| 560379bd89 | |||
| daafee2c06 | |||
| ab3e8ce55f | |||
| a655c990ed | |||
| 2c976adb77 | |||
| 1128ac500d | |||
| 06d7558c3c | |||
| 7344b1cf3d | |||
| ace9c1b2b9 | |||
| 0629e1bc50 | |||
| 832eb6ac31 | |||
| aa8bf156b0 | |||
| 83c1c86eff | |||
| ffe53d3fb5 | |||
| 49aee37199 | |||
| 54eee8cd34 | |||
| 5e62e681e7 | |||
| 99c1645411 | |||
| 564eab23af | |||
| f2da54c92b | |||
| cd262097dc | |||
| d1726b23f1 | |||
| 50e9be833b | |||
| 806b7427c1 | |||
| 8742938074 | |||
| 36e61cab3f | |||
| 17d4adbbfa | |||
| d052871fe0 | |||
| b0e5a49b0b | |||
| 35cbcc221a | |||
| 26d1f2b583 | |||
| 95d114ea68 | |||
| 059becbf09 | |||
| 91120091a3 | |||
| 99a812cc75 | |||
| 29a2de31c6 | |||
| 2688bf261b | |||
| 15c32c0e6d | |||
| d6f2500be2 | |||
| fa8931ad15 | |||
| 6e3060141b | |||
| 07029e3797 | |||
| 3672cc7f2c | |||
| ac32eb128e | |||
| ffdf453fea | |||
| 9dd7432e6b | |||
| 8c4a8d91a2 | |||
| 9aae6e78d6 | |||
| 317b2b50ad | |||
| a0b2dde034 | |||
| 70eaa22327 | |||
| ddfc3e599f | |||
| c11dd2bd9e | |||
| 77c85bfd5c | |||
| 7f8d8d26ac | |||
| 655bb241a8 | |||
| 1ef5bf7d15 | |||
| 9a1589da0b | |||
| f231353228 | |||
| d2de642c17 | |||
| 50c5383138 | |||
| fff88a9237 | |||
| d768a80526 | |||
| 0d79b19c66 | |||
| fea2812688 | |||
| 62368c691a | |||
| dd9617598f | |||
| a7f6d45ca9 | |||
| 92656523ff | |||
| e6ef7d9ff4 | |||
| 732cd31e1f | |||
| 1a7670f57a | |||
| af573cc79a | |||
| 1df87c8a16 | |||
| 3c5e3f9638 | |||
| 7a0a7481c0 | |||
| bdcc81b2f1 | |||
| 210b22e307 | |||
| 734235c798 | |||
| 94e168589d | |||
| c22339e4bf | |||
| d01058595f | |||
| fdec5f0693 | |||
| e8f2c70279 | |||
| 2c5371610b | |||
| 758a89b5d7 | |||
| e3888537f9 | |||
| c9773d49a6 | |||
| a559a2eabc | |||
| c9fb497c2d | |||
| ccd4eb10cb | |||
| 847f75b725 | |||
| 8b4bf0d94a | |||
| bf521e2931 | |||
| 111b2ae364 | |||
| dc08d12ce6 | |||
| ce773007f0 | |||
| e78c271c2f | |||
| 7ee07adc70 | |||
| 53e5c6fd98 | |||
| 4591631d3c | |||
| 5a27c41516 | |||
| 24eefa5357 | |||
| d898018b49 | |||
| 09fdde1c5b | |||
| fb0dbb9a51 | |||
| 81f578895e | |||
| 2de12674d9 | |||
| 5ed1cf19b6 | |||
| 5b879ac6bf | |||
| c04ad89a86 | |||
| 93019827b0 | |||
| 9436b8bac5 | |||
| 8f471a29b0 | |||
| dcf88cf587 | |||
| f9184a71db | |||
| bc1ab0f6d3 | |||
| 0b21157f92 | |||
| cacb13e464 | |||
| 8ec03afbe8 | |||
| ab8be017c1 | |||
| 5c28511fe6 | |||
| deb8aacca4 | |||
| aafd333d12 | |||
| 9e0eaf42e5 | |||
| 63254de13a | |||
| 9ac5fa03c6 | |||
| 70f0240da5 | |||
| a6ed74d904 | |||
| bd3d2ea900 | |||
| 871cec9251 | |||
| 97d6af3792 | |||
| 725a2d453a | |||
| 375d2c205f | |||
| f40ea0ec5b | |||
| 1e8c373dd4 | |||
| 2e145bbf95 | |||
| 822e6fd644 | |||
| ad04d5273e | |||
| c1c23d24fe | |||
| 06300427cb | |||
| cb873381f1 | |||
| b5b4c5eef0 | |||
| 7af8e09468 | |||
| 170ea2c45b | |||
| 42db5f55c7 | |||
| 1b02feca93 | |||
| fd966b6aef | |||
| 6bed2e4fca | |||
| dfb1457dfa | |||
| 4f945842a9 | |||
| 44fdfcc8d9 | |||
| 07810ff63c | |||
| 143d6a55ff | |||
| 8fa905a8f1 | |||
| ad0d2e6ff8 | |||
| aa0e6f90d9 | |||
| 3e2938f77d | |||
| 5e5f0ef70f | |||
| 104885ea5c | |||
| ffbb61d58a | |||
| bd48bb7a18 | |||
| a298b65abf | |||
| daaaae82b6 | |||
| 79ace4f1e5 | |||
| f19977ac28 | |||
| a3782728f9 | |||
| 8b832f02ac | |||
| cec32ae881 | |||
| 0fffa26d55 | |||
| 2b6f520f46 | |||
| 7379bf2b1b | |||
| db3c2cea10 | |||
| 2b32a836cd | |||
| 957355c862 | |||
| fc282b795e | |||
| bc65f5f311 | |||
| 5355f2593e | |||
| e939debba5 | |||
| b35ee36fb1 | |||
| 109c72e84f | |||
| 7e0b02a125 | |||
| d4076803c6 | |||
| a5959acb34 | |||
| a4ddf780ce | |||
| 2f684765de | |||
| 22b54862e8 | |||
| 45138c25f0 | |||
| 80034b9b0a | |||
| e6ee75c712 | |||
| 56287eb05e | |||
| 144f35ec66 | |||
| d1378c6c51 | |||
| 4e10b36d0d | |||
| 70737c04cd | |||
| d41fd5541f | |||
| e76e3531d7 | |||
| a9f3a582eb | |||
| 57ae2f333c | |||
| b2086c4da7 | |||
| 344d729320 | |||
| c86f4c12b7 | |||
| aaa97ed113 | |||
| 69c8aec6fb | |||
| f5ceffd80e | |||
| a9f367aa2c | |||
| b87696a0db | |||
| 3338804c34 | |||
| 6b3ac9e6e2 | |||
| 8e1ecfcfe3 | |||
| 6e2596fb77 | |||
| 2ada4e9f20 | |||
| cc038a0a9a | |||
| af6d1960b9 | |||
| 96ae8f9d63 | |||
| c09d1d53b3 | |||
| 2239333f08 | |||
| 0710b5bf1f | |||
| 03c4c41d2d | |||
| 85d8542a2e | |||
| f8d2a9090e | |||
| bbb7bfb7a1 | |||
| 28e277afe1 | |||
| 8517c9b90b | |||
| fe5ccd4206 | |||
| 8b4b63b3cd | |||
| 9c4aac8843 | |||
| d5bb6ffa61 | |||
| fc513bb413 | |||
| 4162b9878a | |||
| c75f8d0937 | |||
| 046c1d21b1 | |||
| 6c2e579596 | |||
| 88419548c7 | |||
| 12c955155d | |||
| 15af9f5261 | |||
| 39efb911e9 | |||
| 4729203d47 | |||
| 0db052acca | |||
| 6e08f45857 | |||
| 7bff84d601 | |||
| e66e05b2b0 | |||
| 5b318ce485 | |||
| 22de16fe17 | |||
| 5d26673bc6 | |||
| dfa4bc0e33 | |||
| e51b89a18a | |||
| 63988e0c5f | |||
| 297fd85a4f | |||
| a82fbe7400 | |||
| 74e1e9a03f | |||
| 05a81b5bdc | |||
| 9ff49103fb | |||
| a5b59c004e | |||
| a0edcf483b | |||
| 9a18e354cd | |||
| 58c7aba1a4 | |||
| f8aa242d2b | |||
| 3da9570abe | |||
| 6ee898e606 | |||
| abbd372ddf | |||
| 3e77982996 | |||
| 3fbea8ace7 | |||
| a41f943b43 | |||
| 15a9c549e7 | |||
| 50cadbaa8e | |||
| 7b128c9701 | |||
| 061181d59d | |||
| af6cb0c8f0 | |||
| 5a693c0370 | |||
| 262588decc | |||
| 17ab35e245 | |||
| db795ec050 | |||
| 773df544dd | |||
| 0ac7d1fd6e | |||
| 6694c63990 | |||
| b3f95f9552 | |||
| 372f94d22a | |||
| d6f94a2ce1 | |||
| 89a39a7dc6 | |||
| 88cf65f792 | |||
| 07723ce6ae | |||
| 9010401af6 | |||
| 4c11789ea8 | |||
| 2988e4c3bd | |||
| ad7926db4c | |||
| 53a3dcdee7 | |||
| 53467d6750 | |||
| a80a731498 | |||
| 7e1757b7f4 | |||
| 2c29e06fb8 | |||
| ca86fa58ac | |||
| de984fa101 | |||
| a12d4e13d0 | |||
| fa6c7acaf5 | |||
| 0b41aa5a2d | |||
| a5ec40c517 | |||
| 75e2b96344 | |||
| aaae8d9681 | |||
| e4608b7df7 | |||
| 44b3f72005 | |||
| 9134d121cd | |||
| 2f067451b4 | |||
| 3f26c45dd9 | |||
| 36e4224b33 | |||
| 434d56672c | |||
| b2fd907e8c | |||
| b29c04e8c9 | |||
| 11a28eba88 | |||
| 9e3c081e50 | |||
| 40c9ef9d90 | |||
| 7facef47da | |||
| e2bf6a0724 | |||
| f6a511d998 | |||
| 9407f3d212 | |||
| 0424c08a9e | |||
| 9d122da4b9 | |||
| 0f56d554b0 | |||
| 06e92d268b | |||
| b44b87923b | |||
| 6e6c65b375 | |||
| c9c00adf6c | |||
| ee15150c78 | |||
| f8f5ef9c98 | |||
| ec015b7849 | |||
| 67e0374c04 | |||
| 9380091ba9 | |||
| aa66a81b27 | |||
| 0c15ec1434 | |||
| 7b8c7f1ae4 | |||
| f6996b1953 | |||
| ea2aa7c76c | |||
| 2531c2d8d4 | |||
| 0e81e14421 | |||
| b994c2dfb0 | |||
| 4085d569a8 | |||
| 622a7b2b9a | |||
| 5697b7891a | |||
| 6a6a7a9f26 | |||
| b763826754 | |||
| b3a0ecbe6d | |||
| 181443b320 | |||
| 6015acb914 | |||
| a55d93b5ff | |||
| 1ed94716bd | |||
| 178838e06c | |||
| c4aaa5a90b | |||
| b1b2e97a5a | |||
| e9f2da82d7 | |||
| 9cba9ada84 | |||
| c8dcf964bc | |||
| 1ea22248b4 | |||
| 4f01004262 | |||
| 051c508254 | |||
| a72b11e9fb | |||
| 96781f5d3d | |||
| de89e26ffb | |||
| 166a7d4d0a | |||
| 640f015812 | |||
| d90f14c707 | |||
| 0922d90aff | |||
| 81ca7c0e28 | |||
| 4f87f6857f | |||
| 0dd29c3b07 | |||
| eb451a8e1a | |||
| 37df32b51b | |||
| 722d0bd29e | |||
| 03d3ab8b80 | |||
| dc2d39fa14 | |||
| 368c60d7c7 | |||
| 8df30f4dae | |||
| 2d7257d2bc | |||
| 7e2de1bd0c | |||
| b786fbcd13 | |||
| eabae5d3df | |||
| a4cb012ce8 | |||
| 3704cad53a | |||
| 25a3cb9655 | |||
| a5c422d45d | |||
| 8550aef606 | |||
| 01cb9d21b0 | |||
| c5ce79102e | |||
| 67ab3d2e38 | |||
| b1b88e37f1 | |||
| 7e61ff9995 | |||
| 58f9ea4cd7 | |||
| dde5ad365e | |||
| dda02807c1 | |||
| ef629321ab | |||
| 4f52537ea0 | |||
| e82b72275b | |||
| ab73b02138 | |||
| 36fba4588f | |||
| ad353e59c9 | |||
| 675a39ad5c | |||
| 1a72b9e84b | |||
| 27a282ea6b | |||
| 6740e20f72 | |||
| 6c1eea76e5 | |||
| 5ca21b6c01 | |||
| 00f52eb096 | |||
| 09074f6591 | |||
| 2d5da63eb2 | |||
| 3bf3bb32a4 | |||
| 3e28124494 | |||
| dd48962901 | |||
| 7743f71a11 | |||
| 88b5359f1c | |||
| 6acbe1189e | |||
| 7c1fb86b20 | |||
| 36e033b0de | |||
| 4ca68e837c | |||
| 9a683c8e35 | |||
| 4a2dbf8f79 | |||
| 4d75cfe0ea | |||
| b6d28bbf42 | |||
| 2a9c1be151 | |||
| d4a6a9326f | |||
| de854eec3a | |||
| 253174c14e | |||
| 9a741f49a0 | |||
| b8f87a2097 | |||
| 4625151db8 | |||
| 280d2dccfb | |||
| 3015385290 | |||
| 147d6b7ff7 | |||
| 82fba39684 | |||
| f801d3a556 | |||
| 837b0a3d43 | |||
| 9ecfcb3e05 | |||
| fb3dbd343b | |||
| 060df210f9 | |||
| fb725be0ed | |||
| 1a95b4c8bb | |||
| cfa950aa46 | |||
| 5771298bfa | |||
| 94c40de011 | |||
| 6f776aeff7 | |||
| eaa7276173 | |||
| 89871a8ef7 | |||
| 7f0f6bec08 | |||
| 8e1ad98964 | |||
| a29e824ecc | |||
| b436758fe6 | |||
| ee7dcd44f4 | |||
| f2e5815e24 | |||
| 335fe42a22 | |||
| a01b99be27 | |||
| a4e46a2768 | |||
| f494735a79 | |||
| 400db84dd5 | |||
| 964bacd824 | |||
| 7994eabe7d | |||
| dec382b49d | |||
| 93cee4f8aa | |||
| e99abb09b6 | |||
| a5ab102b34 | |||
| 1ca6c5b43d | |||
| 64658184ac | |||
| 61ad4a3109 | |||
| e12ed1e4b3 | |||
| e103d3ac33 | |||
| e2217c411a | |||
| 3e6d381239 | |||
| 0823b182f6 | |||
| bd362607ae | |||
| 61ac31eba9 | |||
| 21a8f56c48 | |||
| fb76f8d789 | |||
| 4bd201d454 | |||
| b412da836c | |||
| 425465becd | |||
| 3b487ade2a | |||
| 7d3c76d7c1 | |||
| fd297c6c50 | |||
| 149a6d768c | |||
| 24e69eec90 | |||
| 9c38c8f2ad | |||
| f0bdb3a75e | |||
| c4e505582b | |||
| cc938db4b8 | |||
| 68277a68d5 | |||
| 8e72b5b615 | |||
| 07422f30ba | |||
| d84731d89e | |||
| 4afea14893 | |||
| 2c6a4d2bed | |||
| f466dd166d | |||
| 6d8f86b2c9 | |||
| c8fa704e32 | |||
| 61d06973a4 | |||
| 7a6321f4ce | |||
| 4dde67ca21 | |||
| f9c612e0fa | |||
| d7e772efd9 | |||
| be39d1c374 | |||
| 1e5c0dbc22 | |||
| d79da9d204 | |||
| 09d35d3cc0 | |||
| f7e6928dba | |||
| 3fa682bfe1 | |||
| 97b0ff24c8 | |||
| bb964dc835 | |||
| 5840ec6b89 | |||
| f29641c702 | |||
| 2e3ee65aa7 | |||
| 11a6f5c9b9 | |||
| 324d641410 | |||
| 02e7e1ca95 | |||
| 78d4f928ba | |||
| 0a5cc2ad98 | |||
| e14d528386 | |||
| 15003b004b | |||
| 9a4c2bd530 | |||
| ab61a1392c | |||
| 53d55448e6 | |||
| 46cf310ad6 | |||
| 8418231c43 | |||
| 4b3493a6f5 | |||
| f432473ed9 | |||
| 8f0cef1f37 | |||
| a8e78f984d | |||
| babeaae8d1 | |||
| 45d44f8177 | |||
| db476080ad | |||
| 158818520b | |||
| 4755288fed | |||
| 10db267164 | |||
| dfe7d55057 | |||
| c2b3316fcb | |||
| 86ba055f5e | |||
| 9d6ccda20d | |||
| 39c419c320 | |||
| 2d6d83ccb2 | |||
| ba1eb5fe08 | |||
| 123389786d | |||
| b65bb3b826 | |||
| 5b40d5eaf4 | |||
| 89b1fd3909 | |||
| aa7eac258b | |||
| 392a06c486 | |||
| 1beac5043b | |||
| 475488c910 | |||
| 9ddacb7ae5 | |||
| 3d1d52cbf8 | |||
| acc504cdc9 | |||
| d98d3351c0 | |||
| 3e606cb159 | |||
| cc76964d03 | |||
| b0c5f6307a | |||
| b3019d7333 | |||
| 2265451d4b | |||
| ba936604a3 | |||
| 7dd42fbff3 | |||
| f64e5dac03 | |||
| 24542f30cb | |||
| 51715158c2 | |||
| 42f935acfd | |||
| a4ce93bc7e | |||
| fc08108ee0 | |||
| 7cc437e816 | |||
| a20b6b64a9 | |||
| 664aed35f9 | |||
| fb2dff956b | |||
| db10188945 | |||
| 145b47e942 | |||
| ebd4565166 | |||
| ad61770ef4 | |||
| f4dc26198b | |||
| 89b29256cf | |||
| 7ddcfc89b4 | |||
| baab70f9da | |||
| a6fd9f0d96 | |||
| 9422cc5bb8 | |||
| c3d0f01390 | |||
| c19d3b2d75 | |||
| a723d6f618 | |||
| 1625a0c7c0 | |||
| ac3c7316ff | |||
| b61634e261 | |||
| d4f647c0db | |||
| ad3fceaf60 | |||
| 2a015ba0a1 | |||
| 8a6d8329f3 | |||
| 69ea5a00ee | |||
| c6cbd36231 | |||
| 371184d232 | |||
| 8226b644ae | |||
| 26a5fd8b34 | |||
| 37a65bcaf6 | |||
| 5343d0bd9d | |||
| 92c3457099 | |||
| 6c491653db | |||
| ec1be3c107 | |||
| 85c9020055 | |||
| b490c01226 | |||
| f47218708a | |||
| aa4aca1a90 | |||
| 6c9707d12c | |||
| 438e36f185 | |||
| 6625c9bc48 | |||
| f47d0b1761 | |||
| a4274423a1 | |||
| a81aa875a8 | |||
| 7b0a41b297 | |||
| 1ea6446fab | |||
| 4ee0f74cdb | |||
| 4e923bff69 | |||
| c7bd0a636b | |||
| a9b2c613e1 | |||
| 6855e6f7e9 | |||
| b1a32f3705 | |||
| d8671355a6 | |||
| bd4bc14282 | |||
| 5823c2d245 | |||
| c732566f63 | |||
| 0d945ab45b | |||
| c06ba061f0 | |||
| 3bf92a8c65 | |||
| 87e07570d4 | |||
| c7d983942f | |||
| bb756548a6 | |||
| e94e8d8bd1 | |||
| 1d5f14fae0 | |||
| a8c20d3675 | |||
| 475563b5e1 | |||
| f0d502133e | |||
| 0500d4fc8e | |||
| 5bccc910a3 | |||
| da3f3d98d0 | |||
| c8745b3464 | |||
| eabd57cb4e | |||
| d182640981 | |||
| 138a3579cb | |||
| 99df7f1402 | |||
| 47f48be514 | |||
| 5c7bfeab8f | |||
| 7f337e25bb | |||
| fcab042e88 | |||
| aa86d54553 | |||
| 6026613d29 | |||
| 7d3d1e811f | |||
| 9f2c20ccee | |||
| 064bbba8aa | |||
| d1aa05a9bb | |||
| ab46611839 | |||
| 24226382e8 | |||
| d04c4a6191 | |||
| 06125f1afe | |||
| 8ef11aa382 | |||
| 76da14294c | |||
| c1e92e503b | |||
| 70e42a02d7 | |||
| a17fd2df67 | |||
| 0648f71a9e | |||
| 131b7dd52e | |||
| 3a79ce2cf8 | |||
| deddf00c38 | |||
| a7e3ce878a | |||
| d7ee13011f | |||
| 1d1ebff64d | |||
| ac2ae2657e | |||
| 8e987d58f2 | |||
| 386c0d3ff0 | |||
| 602235b372 | |||
| c4138ef823 | |||
| 1631bbb754 | |||
| 8c91dff94d | |||
| 50c5adce0c | |||
| 9d9aa7b7a5 | |||
| 43ec075a32 | |||
| 29ae6c6f9d | |||
| bd86f8ef91 | |||
| 25027429df | |||
| 35959aaf6d | |||
| 56fb779f99 | |||
| 134fbd6d34 | |||
| d2c75b1605 | |||
| a474e3fe3a | |||
| d81b3acb94 | |||
| 2ca66f4fc9 | |||
| a8bbb6e760 | |||
| 32459a1562 | |||
| 2395d7fe97 | |||
| b7ec4982dc | |||
| 10d606f8e9 | |||
| 5cca911e9d | |||
| 13a88be8d1 | |||
| a5264e22ee | |||
| 290498de6e | |||
| 847b08b505 | |||
| 86a7bee3d9 | |||
| 3932396084 | |||
| 94cc4042d3 | |||
| 154703f873 | |||
| 71dbfc5469 | |||
| e5924b33e5 | |||
| c25abd4a48 | |||
| 7c58f20cca | |||
| ef18d8d955 | |||
| c7c6aa7df8 | |||
| 060af0a1e3 | |||
| 44a427de67 | |||
| aa75355a69 | |||
| 110c7681b7 | |||
| ba517feab5 | |||
| 7e990feb99 | |||
| 77a6b6e7f5 | |||
| a3348122e6 | |||
| 3757157a06 | |||
| 6c2f7b32e5 | |||
| 067213ff60 | |||
| a4079804c2 | |||
| 2bd4b720bb | |||
| f3619bee26 | |||
| c54455870c | |||
| 20477a655e | |||
| 1497470d33 | |||
| abe38fe69d | |||
| 665c41f051 | |||
| 0184f47a41 | |||
| 5374afc9c8 | |||
| d6978f9873 | |||
| 3c5bd95988 | |||
| b3e02394ac | |||
| 91470d530f | |||
| 0c8f3cd22a | |||
| feeb13c053 | |||
| b55fdd7516 | |||
| 894a90fca9 | |||
| 582dbd4272 | |||
| 6eb0d2242f | |||
| 4693be5615 | |||
| bdc6eb55bf | |||
| 54b38401d8 | |||
| 49c2a7b4db | |||
| 6718419d41 |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"bknd": {
|
||||
"url": "http://localhost:28623/api/system/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.2.22"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
working-directory: ./app
|
||||
run: bun run build:ci
|
||||
|
||||
- name: Run Bun tests
|
||||
working-directory: ./app
|
||||
run: bun run test:bun
|
||||
|
||||
- name: Run Node tests
|
||||
working-directory: ./app
|
||||
run: npm run test:node
|
||||
+11
-1
@@ -14,13 +14,23 @@ packages/media/.env
|
||||
**/*/.env
|
||||
**/*/.dev.vars
|
||||
**/*/.wrangler
|
||||
**/*/*.tgz
|
||||
**/*/vite.config.ts.timestamp*
|
||||
.history
|
||||
**/*/.db/*
|
||||
**/*/.configs/*
|
||||
**/*/.template/*
|
||||
**/*/*.db
|
||||
**/*/*.db-shm
|
||||
**/*/*.db-wal
|
||||
**/*/.tmp
|
||||
.npmrc
|
||||
/.verdaccio
|
||||
.idea
|
||||
.vscode
|
||||
.git_old
|
||||
.git_old
|
||||
docker/tmp
|
||||
.debug
|
||||
.history
|
||||
.aider*
|
||||
.vercel
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ FSL-1.1-MIT
|
||||
|
||||
## Notice
|
||||
|
||||
Copyright 2024 Webintex GmbH
|
||||
Copyright 2025 Dennis Senn
|
||||
|
||||
## Terms and Conditions
|
||||
|
||||
|
||||
@@ -1,43 +1,158 @@
|
||||

|
||||
[](https://npmjs.org/package/bknd)
|
||||
|
||||
bknd simplifies app development by providing fully functional backend for data management,
|
||||
authentication, workflows and media. Since it's lightweight and built on Web Standards, it can
|
||||
be deployed nearly anywhere, including running inside your framework of choice. No more
|
||||
deploying multiple separate services!
|
||||

|
||||
|
||||
<p align="center" width="100%">
|
||||
<a href="https://stackblitz.com/github/bknd-io/bknd-demo?hideExplorer=1&embed=1&view=preview&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||
<strong>⭐ Live Demo</strong>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
bknd simplifies app development by providing a fully functional visual backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
||||
|
||||
It's designed to avoid vendor lock-in and architectural limitations. Built exclusively on [WinterTC Minimum Common Web Platform API](https://min-common-api.proposal.wintertc.org/) for universal compatibility, all functionality (data, auth, media, flows) is modular and opt-in, and infrastructure access is adapter-based with direct access to underlying drivers giving you full control without abstractions getting in your way.
|
||||
|
||||
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
|
||||
* **Databases**:
|
||||
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
|
||||
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
|
||||
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
|
||||
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem
|
||||
* **Deployment**: Standalone, Docker, Cloudflare Workers, Vercel, Netlify, Deno Deploy, AWS Lambda, Valtown etc.
|
||||
|
||||
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||
|
||||
> [!WARNING]
|
||||
> This project requires Node.js 22.13 or higher (because of `node:sqlite`).
|
||||
>
|
||||
> Please keep in mind that **bknd** is still under active development
|
||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||
|
||||
## ✨ Features
|
||||
**📊 Data**: Define, query, and control your data with ease.
|
||||
- Define entities with fields and relationships, synced directly to your database.
|
||||
- Supported field types: `primary`, `text`, `number`, `date`, `boolean`, `enum`, `json`, `jsonschema`.
|
||||
- Relationship types: `one-to-one`, `many-to-one`, `many-to-many`, and `polymorphic`.
|
||||
- Advanced querying with the **Repository**: filtering, sorting, pagination, and relational data handling.
|
||||
- Seamlessly manage data with mutators and a robust event system.
|
||||
- Extend database functionality with batching, introspection, and support for multiple SQL dialects.
|
||||
## Use Cases
|
||||
|
||||
**🔐 Auth**: Easily implement reliable authentication strategies.
|
||||
- Built-in `user` entity with customizable fields.
|
||||
- Supports multiple authentication strategies:
|
||||
- Email/password (with hashed storage).
|
||||
- OAuth/OIDC (Google, GitHub, and more).
|
||||
- Secure JWT generation and session management.
|
||||
bknd is a general purpose backend system that implements the primitives almost any backend needs. This way, you can use it for any backend use case, including but not limited to:
|
||||
|
||||
**🖼️ Media**: Effortlessly manage and serve all your media files.
|
||||
- Upload files with ease.
|
||||
- Adapter-based support for S3, S3-compatible storage (e.g., R2, Tigris), and Cloudinary.
|
||||
- **Content Management System (CMS)** as Wordpress alternative, hosted separately or embedded in your frontend
|
||||
- **AI Agent Backends** for managing agent state with built-in data persistence, regardless where it is hosted. Optionally communicate over the integrated MCP server.
|
||||
- **SaaS Products** with multi-tenant data isolation (RLS) and user management, with freedom to choose your own database and storage provider
|
||||
- **Prototypes & MVPs** to validate ideas quickly without infrastructure overhead
|
||||
- **API-First Applications** where you need a reliable, type-safe backend without vendor lock-in either with the integrated TypeScript SDK or REST API using OpenAPI
|
||||
- **IoT & Embedded Devices** where minimal footprint matters
|
||||
|
||||
**🔄 Flows** (UI integration coming soon!): Design and run workflows with seamless automation.
|
||||
- Create and run workflows with trigger-based automation:
|
||||
- Manual triggers or events from data, auth, media, or server actions.
|
||||
- HTTP triggers for external integrations.
|
||||
- Define tasks in sequence, parallel, or loops, with conditional execution.
|
||||
- Use reusable sub-workflows to organize complex processes.
|
||||
- Leverage OpenAPI specifications for API-based tasks.
|
||||
|
||||
## Size
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
||||
|
||||
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 300 kB gzipped (e.g. deployed as Cloudflare Worker).
|
||||
|
||||
## Motivation
|
||||
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
||||
|
||||
**The solution:** A backend system that only assumes and implements primitive details, integrates into multiple environments, and adheres to industry standards.
|
||||
|
||||
## Features
|
||||
* ⚡ Instant backend with full REST API:
|
||||
* **Data**: Define, query, and control your data with ease.
|
||||
* **Auth**: Easily implement reliable authentication strategies.
|
||||
* **Media**: Effortlessly manage and serve all your media files.
|
||||
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
|
||||
* 🌐 Built on Web Standards for maximum compatibility
|
||||
* 🛠️ MCP server, client and UI built-in to control your backend
|
||||
* 🏃♂️ Multiple run modes
|
||||
* standalone using the CLI
|
||||
* using a JavaScript runtime (Node, Bun, workerd)
|
||||
* using a React framework (Next.js, React Router, Astro)
|
||||
* 📦 Official API and React SDK with type-safety
|
||||
* ⚛️ React elements for auto-configured authentication and media components
|
||||
|
||||
## Structure
|
||||
The package is mainly split into 4 parts, each serving a specific purpose:
|
||||
|
||||
| Import | Purpose |
|
||||
|-----------------------------|------------------------------------------------------|
|
||||
| `bknd`<br/>`bknd/adapter/*` | Backend including APIs and adapters |
|
||||
| `bknd/ui` | Admin UI components for react frameworks |
|
||||
| `bknd/client` | TypeScript SDK and React hooks for the API endpoints |
|
||||
| `bknd/elements` | React components for authentication and media |
|
||||
|
||||
|
||||
### The backend (`bknd`)
|
||||
Serve the backend as an API for any JS runtime or framework. The latter is especially handy, as it allows you to deploy your frontend and backend bundled together. Furthermore it allows adding additional logic in a way you're already familar with. Just add another route and you're good to go.
|
||||
|
||||
Here is an example of serving the API using node:
|
||||
```js index.js
|
||||
import { serve } from "bknd/adapter/node"
|
||||
serve();
|
||||
```
|
||||
|
||||
### Integrated admin UI (`bknd/ui`)
|
||||
The admin UI allows to manage your data including full configuration of your backend using a graphical user interface. Using `vite`, your admin route looks like this:
|
||||
```tsx
|
||||
import { Admin } from "bknd/ui"
|
||||
import "bknd/dist/styles.css";
|
||||
|
||||
export default function AdminPage() {
|
||||
return <Admin />
|
||||
}
|
||||
```
|
||||
|
||||
### Using the REST API or TypeScript SDK (`bknd/client`)
|
||||
If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
|
||||
```bash
|
||||
curl -XGET <your-endpoint>/api/data/entity/<entity>
|
||||
{
|
||||
"data": [
|
||||
{ "id": 1, ... },
|
||||
{ "id": 2, ... }
|
||||
],
|
||||
"meta": { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
In a JavaScript environment, you can use the TypeScript SDK with type-safety. The above example would look like this:
|
||||
```ts
|
||||
import { Api } from "bknd/client";
|
||||
|
||||
const api = new Api({ host: "<endpoint>" });
|
||||
const { data } = await api.data.readMany("<entity>");
|
||||
```
|
||||
|
||||
If you're using React, there are 2 hooks exposed (`useApi`, `useEntity`), as well as an `swr` wrapper around each (`useApiQuery`, `useEntityQuery`). The `swr` wrapped hooks automatically handled query invalidation:
|
||||
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
import { useEntityQuery } from "bknd/client";
|
||||
|
||||
export default function App() {
|
||||
const { data } = useEntityQuery("todos");
|
||||
return <ul>
|
||||
{data?.map(todo => (
|
||||
<li key={todo.id}>{todo.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
|
||||
### React elements (`bknd/elements`)
|
||||
You don't have to figure out API details to include media uploads to your app. For an user avatar upload, this is all you need:
|
||||
```tsx
|
||||
import { Media } from "bknd/elements"
|
||||
import "bknd/dist/main.css"
|
||||
|
||||
export function UserAvatar() {
|
||||
return <Media.Dropzone
|
||||
entity={{ name: "users", id: 1, field: "avatar" }}
|
||||
maxItems={1}
|
||||
overwrite
|
||||
/>
|
||||
}
|
||||
```
|
||||
The import path also exports components for login and registration forms which are automatically pointed to the `bknd` defaults.
|
||||
|
||||
|
||||
## 🚀 Quick start
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# ===== DB Settings =====
|
||||
VITE_DB_URL=:memory:
|
||||
# you can set a location for a database here, it'll overwrite the previous setting
|
||||
# ideally use the ".db" folder (create it first), it's git ignored
|
||||
VITE_DB_URL=file:.db/dev.db
|
||||
|
||||
# alternatively, you can use url/token combination
|
||||
#VITE_DB_URL=
|
||||
#VITE_DB_TOKEN=
|
||||
|
||||
|
||||
# ===== DEV Server =====
|
||||
# restart the dev server on every change (enable with "1")
|
||||
VITE_APP_FRESH=
|
||||
# displays react-scan widget (enable with "1")
|
||||
VITE_DEBUG_RERENDERS=
|
||||
# console logs registered routes on start (enable with "1")
|
||||
VITE_SHOW_ROUTES=
|
||||
|
||||
|
||||
# ===== Test Credentials =====
|
||||
RESEND_API_KEY=
|
||||
R2_TOKEN=
|
||||
|
||||
R2_ACCESS_KEY=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_URL=
|
||||
|
||||
AWS_ACCESS_KEY=
|
||||
AWS_SECRET_KEY=
|
||||
AWS_S3_URL=
|
||||
|
||||
OAUTH_CLIENT_ID=
|
||||
OAUTH_CLIENT_SECRET=
|
||||
|
||||
PUBLIC_POSTHOG_KEY=
|
||||
PUBLIC_POSTHOG_HOST=
|
||||
|
||||
# ===== Internals =====
|
||||
BKND_CLI_CREATE_REF=main
|
||||
BKND_CLI_LOG_LEVEL=debug
|
||||
BKND_MODULES_DEBUG=1
|
||||
@@ -0,0 +1,4 @@
|
||||
playwright-report
|
||||
test-results
|
||||
bknd.config.*
|
||||
__test__/helper.d.ts
|
||||
+137
-4
@@ -1,15 +1,148 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { App } from "../src";
|
||||
import { afterEach, describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { App, createApp } from "core/test/utils";
|
||||
import { getDummyConnection } from "./helper";
|
||||
import { Hono } from "hono";
|
||||
import * as proto from "../src/data/prototype";
|
||||
import { pick } from "lodash-es";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
afterEach(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("App tests", async () => {
|
||||
test("boots and pongs", async () => {
|
||||
const app = new App(dummyConnection);
|
||||
await app.build();
|
||||
|
||||
//expect(await app.data?.em.ping()).toBeTrue();
|
||||
expect(await app.em.ping()).toBeTrue();
|
||||
});
|
||||
|
||||
test("plugins", async () => {
|
||||
const called: string[] = [];
|
||||
const app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
plugins: [
|
||||
(app) => {
|
||||
expect(app).toBeDefined();
|
||||
expect(app).toBeInstanceOf(App);
|
||||
return {
|
||||
name: "test",
|
||||
schema: () => {
|
||||
called.push("schema");
|
||||
return proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text(),
|
||||
}),
|
||||
users: proto.entity("users", {
|
||||
email_verified: proto.boolean(),
|
||||
}),
|
||||
},
|
||||
(fn, s) => {
|
||||
fn.relation(s.comments).manyToOne(s.posts);
|
||||
fn.index(s.posts).on(["title"]);
|
||||
},
|
||||
);
|
||||
},
|
||||
onBoot: async () => {
|
||||
called.push("onBoot");
|
||||
},
|
||||
beforeBuild: async () => {
|
||||
called.push("beforeBuild");
|
||||
},
|
||||
onBuilt: async () => {
|
||||
called.push("onBuilt");
|
||||
},
|
||||
onServerInit: async (server) => {
|
||||
called.push("onServerInit");
|
||||
expect(server).toBeDefined();
|
||||
expect(server).toBeInstanceOf(Hono);
|
||||
},
|
||||
onFirstBoot: async () => {
|
||||
called.push("onFirstBoot");
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
|
||||
expect(app.em.entities.map((e) => e.name)).toEqual(["users", "posts", "comments"]);
|
||||
expect(app.em.indices.map((i) => i.name)).toEqual([
|
||||
"idx_unique_users_email",
|
||||
"idx_users_strategy",
|
||||
"idx_users_strategy_value",
|
||||
"idx_posts_title",
|
||||
]);
|
||||
expect(
|
||||
app.em.relations.all.map((r) => pick(r.toJSON(), ["type", "source", "target"])),
|
||||
).toEqual([
|
||||
{
|
||||
type: "n:1",
|
||||
source: "comments",
|
||||
target: "posts",
|
||||
},
|
||||
]);
|
||||
expect(called).toEqual([
|
||||
"onBoot",
|
||||
"onServerInit",
|
||||
"beforeBuild",
|
||||
"onServerInit",
|
||||
"schema",
|
||||
"onFirstBoot",
|
||||
"onBuilt",
|
||||
]);
|
||||
expect(app.plugins.size).toBe(1);
|
||||
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
|
||||
});
|
||||
|
||||
test.only("drivers", async () => {
|
||||
const called: string[] = [];
|
||||
const app = new App(dummyConnection, undefined, {
|
||||
drivers: {
|
||||
email: {
|
||||
send: async (to, subject, body) => {
|
||||
called.push("email.send");
|
||||
return {
|
||||
id: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
get: async (key) => {
|
||||
called.push("cache.get");
|
||||
return "";
|
||||
},
|
||||
set: async (key, value, ttl) => {
|
||||
called.push("cache.set");
|
||||
},
|
||||
del: async (key) => {
|
||||
called.push("cache.del");
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
expect(app.drivers.cache).toBeDefined();
|
||||
expect(app.drivers.email).toBeDefined();
|
||||
await app.drivers.email.send("", "", "");
|
||||
await app.drivers.cache.get("");
|
||||
await app.drivers.cache.set("", "", 0);
|
||||
await app.drivers.cache.del("");
|
||||
|
||||
expect(called).toEqual(["email.send", "cache.get", "cache.set", "cache.del"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type TSchema, Type, stripMark } from "../src/core/utils";
|
||||
import { Module } from "../src/modules/Module";
|
||||
|
||||
function createModule<Schema extends TSchema>(schema: Schema) {
|
||||
class TestModule extends Module<typeof schema> {
|
||||
getSchema() {
|
||||
return schema;
|
||||
}
|
||||
toJSON() {
|
||||
return this.config;
|
||||
}
|
||||
useForceParse() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return TestModule;
|
||||
}
|
||||
|
||||
describe("Module", async () => {
|
||||
test("basic", async () => {});
|
||||
|
||||
test("listener", async () => {
|
||||
let result: any;
|
||||
|
||||
const module = createModule(Type.Object({ a: Type.String() }));
|
||||
const m = new module({ a: "test" });
|
||||
|
||||
await m.schema().set({ a: "test2" });
|
||||
m.setListener(async (c) => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
result = stripMark(c);
|
||||
});
|
||||
await m.schema().set({ a: "test3" });
|
||||
expect(result).toEqual({ a: "test3" });
|
||||
});
|
||||
});
|
||||
@@ -1,197 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mark, stripMark } from "../src/core/utils";
|
||||
import { ModuleManager } from "../src/modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME, migrateSchema } from "../src/modules/migrations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
describe("ModuleManager", async () => {
|
||||
test("s1: no config, no build", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
|
||||
// that is because no module is built
|
||||
expect(mm.toJSON()).toEqual({ version: 0 } as any);
|
||||
});
|
||||
|
||||
test("s2: no config, build", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
await mm.build();
|
||||
|
||||
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||
expect(mm.built()).toBe(true);
|
||||
});
|
||||
|
||||
test("s3: config given, table exists, version matches", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const version = mm.version();
|
||||
const json = mm.configs();
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
await migrateSchema(CURRENT_VERSION, { db });
|
||||
await db
|
||||
.updateTable(TABLE_NAME)
|
||||
.set({ json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, { initial: { version, ...json } });
|
||||
await mm2.build();
|
||||
|
||||
expect(json).toEqual(mm2.configs());
|
||||
});
|
||||
|
||||
test("s4: config given, table exists, version outdated, migrate", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const version = mm.version();
|
||||
const json = mm.configs();
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
console.log("here2");
|
||||
await migrateSchema(CURRENT_VERSION, { db });
|
||||
await db
|
||||
.updateTable(TABLE_NAME)
|
||||
.set({ json: JSON.stringify(json), version: CURRENT_VERSION - 1 })
|
||||
.execute();
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json }
|
||||
});
|
||||
console.log("here3");
|
||||
await mm2.build();
|
||||
});
|
||||
|
||||
test("s5: config given, table exists, version mismatch", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const version = mm.version();
|
||||
const json = mm.configs();
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
await migrateSchema(CURRENT_VERSION, { db });
|
||||
await db
|
||||
.updateTable(TABLE_NAME)
|
||||
.set({ json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json }
|
||||
});
|
||||
|
||||
expect(mm2.build()).rejects.toThrow(/version.*do not match/);
|
||||
});
|
||||
|
||||
test("s6: no config given, table exists, fetch", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const json = mm.configs();
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
await migrateSchema(CURRENT_VERSION, { db });
|
||||
|
||||
const config = {
|
||||
...json,
|
||||
data: {
|
||||
...json.data,
|
||||
basepath: "/api/data2"
|
||||
}
|
||||
};
|
||||
await db
|
||||
.updateTable(TABLE_NAME)
|
||||
.set({ json: JSON.stringify(config), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
// run without config given
|
||||
const mm2 = new ModuleManager(c2.dummyConnection);
|
||||
await mm2.build();
|
||||
|
||||
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
||||
});
|
||||
|
||||
test("blank app, modify config", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
await mm.build();
|
||||
const configs = stripMark(mm.configs());
|
||||
|
||||
expect(mm.configs().server.admin.color_scheme).toBe("light");
|
||||
expect(() => mm.get("server").schema().patch("admin", { color_scheme: "violet" })).toThrow();
|
||||
await mm.get("server").schema().patch("admin", { color_scheme: "dark" });
|
||||
await mm.save();
|
||||
|
||||
expect(mm.configs().server.admin.color_scheme).toBe("dark");
|
||||
expect(stripMark(mm.configs())).toEqual({
|
||||
...configs,
|
||||
server: {
|
||||
...configs.server,
|
||||
admin: {
|
||||
...configs.server.admin,
|
||||
color_scheme: "dark"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// @todo: check what happens here
|
||||
/*test("blank app, modify deep config", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
await mm.build();
|
||||
|
||||
/!* await mm
|
||||
.get("data")
|
||||
.schema()
|
||||
.patch("entities.test", {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
});
|
||||
await mm.build();
|
||||
|
||||
expect(mm.configs().data.entities?.users?.fields?.email.type).toBe("text");
|
||||
|
||||
expect(
|
||||
mm.get("data").schema().patch("desc", "entities.users.config.sort_dir")
|
||||
).rejects.toThrow();
|
||||
await mm.build();*!/
|
||||
expect(mm.configs().data.entities?.users?.fields?.email.type).toBe("text");
|
||||
console.log("here", mm.configs());
|
||||
await mm
|
||||
.get("data")
|
||||
.schema()
|
||||
.patch("entities.users", { config: { sort_dir: "desc" } });
|
||||
await mm.build();
|
||||
expect(mm.toJSON());
|
||||
|
||||
//console.log(_jsonp(mm.toJSON().data));
|
||||
/!*expect(mm.configs().data.entities!.test!.fields!.content.type).toBe("text");
|
||||
expect(mm.configs().data.entities!.users!.config!.sort_dir).toBe("desc");*!/
|
||||
});*/
|
||||
|
||||
/*test("accessing modules", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
|
||||
//mm.get("auth").mutate().set({});
|
||||
});*/
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
tmp/*
|
||||
!tmp/.gitkeep
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
@@ -0,0 +1 @@
|
||||
hello
|
||||
@@ -0,0 +1,66 @@
|
||||
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
||||
import * as adapter from "adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { omitKeys } from "core/utils";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("adapter", () => {
|
||||
it("makes config", async () => {
|
||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||
expect(
|
||||
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
||||
).toEqual({});
|
||||
|
||||
// merges everything returned from `app` with the config
|
||||
expect(
|
||||
omitKeys(
|
||||
await adapter.makeConfig(
|
||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
{ env: { TEST: "test" } },
|
||||
),
|
||||
["connection"],
|
||||
),
|
||||
).toEqual({
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
});
|
||||
});
|
||||
|
||||
it("allows all properties in app function", async () => {
|
||||
const called = mock(() => null);
|
||||
const config = await adapter.makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: "test" },
|
||||
config: { server: { cors: { origin: "test" } } },
|
||||
options: {
|
||||
mode: "db",
|
||||
},
|
||||
onBuilt: () => {
|
||||
called();
|
||||
expect(env).toEqual({ foo: "bar" });
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ foo: "bar" },
|
||||
);
|
||||
expect(config.connection).toEqual({ url: "test" });
|
||||
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
|
||||
expect(config.options).toEqual({ mode: "db" });
|
||||
await config.onBuilt?.(null as any);
|
||||
expect(called).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: adapter.createFrameworkApp,
|
||||
label: "framework app",
|
||||
});
|
||||
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: adapter.createRuntimeApp,
|
||||
label: "runtime app",
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { sign } from "hono/jwt";
|
||||
import { Api } from "../../src/Api";
|
||||
|
||||
describe("Api", async () => {
|
||||
it("should construct without options", () => {
|
||||
const api = new Api();
|
||||
expect(api.baseUrl).toBe("http://localhost");
|
||||
|
||||
// verified is true, because no token, user, headers or request given
|
||||
// therefore nothing to check, auth state is verified
|
||||
expect(api.isAuthVerified()).toBe(true);
|
||||
});
|
||||
|
||||
it("should ignore force verify if no claims given", () => {
|
||||
const api = new Api({ verified: true });
|
||||
expect(api.baseUrl).toBe("http://localhost");
|
||||
expect(api.isAuthVerified()).toBe(true);
|
||||
});
|
||||
|
||||
it("should construct from request (token)", async () => {
|
||||
const token = await sign({ foo: "bar" }, "1234");
|
||||
const request = new Request("http://example.com/test", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const api = new Api({ request });
|
||||
expect(api.isAuthVerified()).toBe(false);
|
||||
|
||||
const params = api.getParams();
|
||||
expect(params.token).toBe(token);
|
||||
expect(params.token_transport).toBe("header");
|
||||
expect(params.host).toBe("http://example.com");
|
||||
});
|
||||
|
||||
it("should construct from request (cookie)", async () => {
|
||||
const token = await sign({ foo: "bar" }, "1234");
|
||||
const request = new Request("http://example.com/test", {
|
||||
headers: {
|
||||
Cookie: `auth=${token}`,
|
||||
},
|
||||
});
|
||||
const api = new Api({ request });
|
||||
expect(api.isAuthVerified()).toBe(false);
|
||||
|
||||
const params = api.getParams();
|
||||
expect(params.token).toBe(token);
|
||||
expect(params.token_transport).toBe("cookie");
|
||||
expect(params.host).toBe("http://example.com");
|
||||
});
|
||||
|
||||
it("should construct from token", async () => {
|
||||
const token = await sign({ foo: "bar" }, "1234");
|
||||
const api = new Api({ token });
|
||||
expect(api.isAuthVerified()).toBe(false);
|
||||
|
||||
const params = api.getParams();
|
||||
expect(params.token).toBe(token);
|
||||
expect(params.token_transport).toBe("header");
|
||||
expect(params.host).toBe("http://localhost");
|
||||
});
|
||||
|
||||
it("should prefer host when request is given", async () => {
|
||||
const request = new Request("http://example.com/test");
|
||||
const api = new Api({ request, host: "http://another.com" });
|
||||
|
||||
const params = api.getParams();
|
||||
expect(params.token).toBeUndefined();
|
||||
expect(params.token_transport).toBe("header");
|
||||
expect(params.host).toBe("http://another.com");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
import { DataApi } from "../../src/data/api/DataApi";
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
import { schemaToEm } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { parse } from "core/utils/schema";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
const dataConfig = parse(dataConfigSchema, {});
|
||||
describe("DataApi", () => {
|
||||
it("should switch to post for long url reads", async () => {
|
||||
const api = new DataApi();
|
||||
|
||||
const get = api.readMany("a".repeat(300), { select: ["id", "name"] });
|
||||
expect(get.request.method).toBe("GET");
|
||||
expect(new URL(get.request.url).pathname).toBe(`/api/data/entity/${"a".repeat(300)}`);
|
||||
|
||||
const post = api.readMany("a".repeat(1000), { select: ["id", "name"] });
|
||||
expect(post.request.method).toBe("POST");
|
||||
expect(new URL(post.request.url).pathname).toBe(`/api/data/entity/${"a".repeat(1000)}/query`);
|
||||
});
|
||||
|
||||
it("returns result", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
{
|
||||
const res = (await app.request("/entity/posts")) as Response;
|
||||
const { data } = (await res.json()) as any;
|
||||
expect(data.length).toEqual(3);
|
||||
}
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
|
||||
{
|
||||
const req = api.readMany("posts", { select: ["title"] });
|
||||
expect(req.request.method).toBe("GET");
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
const req = api.readMany("posts", {
|
||||
select: ["title"],
|
||||
limit: 100000,
|
||||
offset: 0,
|
||||
sort: "id",
|
||||
});
|
||||
expect(req.request.method).toBe("POST");
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
// make sure sort is working
|
||||
const req = await api.readMany("posts", {
|
||||
select: ["title"],
|
||||
sort: "-id",
|
||||
});
|
||||
expect(req.data).toEqual(payload.reverse() as any);
|
||||
}
|
||||
});
|
||||
|
||||
it("updates many", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [
|
||||
{ title: "foo", count: 0 },
|
||||
{ title: "bar", count: 0 },
|
||||
{ title: "baz", count: 0 },
|
||||
{ title: "bla", count: 2 },
|
||||
];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
{
|
||||
const req = api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
// update with empty where
|
||||
expect(() => api.updateMany("posts", {}, { count: 1 })).toThrow();
|
||||
expect(() => api.updateMany("posts", undefined, { count: 1 })).toThrow();
|
||||
}
|
||||
|
||||
{
|
||||
// update
|
||||
const req = await api.updateMany("posts", { count: 0 }, { count: 1 });
|
||||
expect(req.res.status).toBe(200);
|
||||
}
|
||||
|
||||
{
|
||||
// compare
|
||||
const res = await api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
expect(res.map((p) => p.count)).toEqual([1, 1, 1, 2]);
|
||||
}
|
||||
});
|
||||
|
||||
it("refines", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
const normalOne = api.readOne("posts", 1);
|
||||
const normal = api.readMany("posts", { select: ["title"], where: { title: "baz" } });
|
||||
expect((await normal).data).toEqual([{ title: "baz" }] as any);
|
||||
|
||||
// refine
|
||||
const refined = normal.refine((data) => data[0]);
|
||||
expect((await refined).data).toEqual({ title: "baz" } as any);
|
||||
|
||||
// one
|
||||
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
||||
const oneByRes = await oneBy;
|
||||
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
||||
expect(oneByRes.body.meta.items).toEqual(1);
|
||||
});
|
||||
|
||||
it("exists/count", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
|
||||
const exists = api.exists("posts", { id: 1 });
|
||||
expect((await exists).exists).toBeTrue();
|
||||
|
||||
expect((await api.count("posts")).count).toEqual(3);
|
||||
});
|
||||
|
||||
it("creates many", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [
|
||||
{ title: "foo", count: 0 },
|
||||
{ title: "bar", count: 0 },
|
||||
{ title: "baz", count: 0 },
|
||||
{ title: "bla", count: 2 },
|
||||
];
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
|
||||
{
|
||||
// create many
|
||||
const res = await api.createMany("posts", payload);
|
||||
expect(res.data?.length).toEqual(4);
|
||||
expect(res.ok).toBeTrue();
|
||||
}
|
||||
|
||||
{
|
||||
const req = api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
// create with empty
|
||||
expect(() => api.createMany("posts", [])).toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/// <reference types="@types/bun" />
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
|
||||
import { MediaApi } from "media/api/MediaApi";
|
||||
import { assetsPath, assetsTmpPath } from "../helper";
|
||||
|
||||
const mockedBackend = new Hono()
|
||||
.basePath("/api/media")
|
||||
.post("/upload/:name", async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const body = await getFileFromContext(c);
|
||||
return c.json({ name, is_file: isFile(body), size: body.size });
|
||||
})
|
||||
.get("/file/:name", async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const file = Bun.file(`${assetsPath}/${name}`);
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Content-Length": file.size.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("MediaApi", () => {
|
||||
it("should give correct file upload url", () => {
|
||||
const host = "http://localhost";
|
||||
const basepath = "/api/media";
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({
|
||||
host,
|
||||
basepath,
|
||||
});
|
||||
expect(api.getFileUploadUrl({ path: "path" } as any)).toBe(`${host}${basepath}/upload/path`);
|
||||
});
|
||||
|
||||
it("should have correct upload headers", () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({
|
||||
token: "token",
|
||||
token_transport: "header",
|
||||
});
|
||||
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should return empty headers if not using `header` transport", () => {
|
||||
expect(
|
||||
new MediaApi({
|
||||
token_transport: "cookie",
|
||||
})
|
||||
.getUploadHeaders()
|
||||
.has("Authorization"),
|
||||
).toBe(false);
|
||||
expect(
|
||||
new MediaApi({
|
||||
token_transport: "none",
|
||||
})
|
||||
.getUploadHeaders()
|
||||
.has("Authorization"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should get file: native", async () => {
|
||||
const name = "image.png";
|
||||
const path = `${assetsTmpPath}/${name}`;
|
||||
const res = await mockedBackend.request("/api/media/file/" + name);
|
||||
await Bun.write(path, res);
|
||||
|
||||
const file = await Bun.file(path);
|
||||
expect(file.size).toBeGreaterThan(0);
|
||||
expect(file.type).toBe("image/png");
|
||||
await file.delete();
|
||||
});
|
||||
|
||||
it("download", async () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({}, mockedBackend.request);
|
||||
|
||||
const name = "image.png";
|
||||
const file = await api.download(name);
|
||||
expect(isFile(file)).toBe(true);
|
||||
expect(file.size).toBeGreaterThan(0);
|
||||
expect(file.type).toBe("image/png");
|
||||
expect(file.name).toContain(name);
|
||||
});
|
||||
|
||||
it("getFile", async () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({}, mockedBackend.request);
|
||||
|
||||
const name = "image.png";
|
||||
const res = await api.getFile(name);
|
||||
expect(res.ok).toBe(true);
|
||||
// make sure it's a normal api request as usual
|
||||
expect(res.res.ok).toBe(true);
|
||||
expect(isReadableStream(res)).toBe(true);
|
||||
expect(isReadableStream(res.body)).toBe(true);
|
||||
expect(isReadableStream(res.res.body)).toBe(true);
|
||||
|
||||
const blob = await res.res.blob();
|
||||
expect(isFile(blob)).toBe(true);
|
||||
expect(blob.size).toBeGreaterThan(0);
|
||||
expect(blob.type).toBe("image/png");
|
||||
expect(blob.name).toContain(name);
|
||||
});
|
||||
|
||||
it("getFileStream", async () => {
|
||||
// @ts-ignore tests
|
||||
const api = new MediaApi({}, mockedBackend.request);
|
||||
|
||||
const name = "image.png";
|
||||
const res = await api.getFileStream(name);
|
||||
expect(isReadableStream(res)).toBe(true);
|
||||
|
||||
const blob = await new Response(res).blob();
|
||||
expect(isFile(blob)).toBe(true);
|
||||
expect(blob.size).toBeGreaterThan(0);
|
||||
expect(blob.type).toBe("image/png");
|
||||
expect(blob.name).toContain(name);
|
||||
});
|
||||
|
||||
it("should upload file in various ways", async () => {
|
||||
const api = new MediaApi(
|
||||
{
|
||||
upload_fetcher: mockedBackend.request,
|
||||
},
|
||||
mockedBackend.request,
|
||||
);
|
||||
const file = Bun.file(`${assetsPath}/image.png`);
|
||||
|
||||
async function matches(req: Promise<any>, filename: string) {
|
||||
const res: any = await req;
|
||||
expect(res.name).toBe(filename);
|
||||
expect(res.is_file).toBe(true);
|
||||
expect(res.size).toBe(file.size);
|
||||
}
|
||||
|
||||
const url = "http://localhost/api/media/file/image.png";
|
||||
|
||||
// upload bun file
|
||||
await matches(api.upload(file as any, { filename: "bunfile.png" }), "bunfile.png");
|
||||
|
||||
// upload via request
|
||||
await matches(api.upload(new Request(url), { filename: "request.png" }), "request.png");
|
||||
|
||||
// upload via url
|
||||
await matches(api.upload(url, { filename: "url.png" }), "url.png");
|
||||
|
||||
// upload via response
|
||||
{
|
||||
const response = await mockedBackend.request(url);
|
||||
await matches(api.upload(response, { filename: "response.png" }), "response.png");
|
||||
}
|
||||
|
||||
// upload via readable from bun
|
||||
await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
||||
|
||||
// upload via readable from response
|
||||
{
|
||||
const response = (await mockedBackend.request(url)) as Response;
|
||||
await matches(
|
||||
await api.upload(response.body!, { filename: "readable.png" }),
|
||||
"readable.png",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { secureRandomString } from "../../src/core/utils";
|
||||
import { ModuleApi } from "../../src/modules";
|
||||
|
||||
class Api extends ModuleApi {
|
||||
_getUrl(path: string) {
|
||||
return this.getUrl(path);
|
||||
}
|
||||
}
|
||||
|
||||
const host = "http://localhost";
|
||||
|
||||
describe("ModuleApi", () => {
|
||||
it("resolves options correctly", () => {
|
||||
const api = new Api({ host });
|
||||
expect(api.options).toEqual({ host });
|
||||
});
|
||||
|
||||
it("returns correct url from path", () => {
|
||||
const api = new Api({ host });
|
||||
expect(api._getUrl("/test")).toEqual("http://localhost/test");
|
||||
expect(api._getUrl("test")).toEqual("http://localhost/test");
|
||||
expect(api._getUrl("test/")).toEqual("http://localhost/test");
|
||||
expect(api._getUrl("//test?foo=1")).toEqual("http://localhost/test?foo=1");
|
||||
});
|
||||
|
||||
it("fetches endpoint", async () => {
|
||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
||||
const api = new Api({ host }, app.request as typeof fetch);
|
||||
|
||||
const res = await api.get("/endpoint");
|
||||
expect(res.res.ok).toEqual(true);
|
||||
expect(res.res.status).toEqual(200);
|
||||
expect(res.data).toEqual({ foo: "bar" });
|
||||
expect(res.body).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("has accessible request", async () => {
|
||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
|
||||
const api = new Api({ host }, app.request as typeof fetch);
|
||||
|
||||
const promise = api.get("/endpoint");
|
||||
expect(promise.request).toBeDefined();
|
||||
expect(promise.request.url).toEqual("http://localhost/endpoint");
|
||||
|
||||
expect((await promise).body).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("adds token to headers when given in options", () => {
|
||||
const token = secureRandomString(20);
|
||||
const api = new Api({ host, token, token_transport: "header" });
|
||||
|
||||
expect(api.get("/").request.headers.get("Authorization")).toEqual(`Bearer ${token}`);
|
||||
});
|
||||
|
||||
it("sets header to accept json", () => {
|
||||
const api = new Api({ host });
|
||||
expect(api.get("/").request.headers.get("Accept")).toEqual("application/json");
|
||||
});
|
||||
|
||||
it("adds additional headers from options", () => {
|
||||
const headers = new Headers({
|
||||
"X-Test": "123",
|
||||
});
|
||||
const api = new Api({ host, headers });
|
||||
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
||||
});
|
||||
|
||||
it("uses basepath & removes trailing slash", () => {
|
||||
const api = new Api({ host, basepath: "/api" });
|
||||
expect(api.get("/").request.url).toEqual("http://localhost/api");
|
||||
});
|
||||
|
||||
it("uses search params", () => {
|
||||
const api = new Api({ host });
|
||||
const search = new URLSearchParams({
|
||||
foo: "bar",
|
||||
});
|
||||
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
||||
});
|
||||
|
||||
it("resolves method shortcut fns correctly", () => {
|
||||
const api = new Api({ host });
|
||||
expect(api.get("/").request.method).toEqual("GET");
|
||||
expect(api.post("/").request.method).toEqual("POST");
|
||||
expect(api.put("/").request.method).toEqual("PUT");
|
||||
expect(api.patch("/").request.method).toEqual("PATCH");
|
||||
expect(api.delete("/").request.method).toEqual("DELETE");
|
||||
});
|
||||
|
||||
it("refines", async () => {
|
||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: ["bar"] }));
|
||||
const api = new Api({ host }, app.request as typeof fetch);
|
||||
|
||||
expect((await api.get("/endpoint")).data).toEqual({ foo: ["bar"] });
|
||||
expect((await api.get("/endpoint").refine((data) => data.foo)).data).toEqual(["bar"]);
|
||||
});
|
||||
|
||||
// @todo: test error response
|
||||
// @todo: test method shortcut functions
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { describe, test } from "bun:test";
|
||||
import { DataApi } from "../../src/modules/data/api/DataApi";
|
||||
|
||||
describe("Api", async () => {
|
||||
test("...", async () => {
|
||||
/*const dataApi = new DataApi({
|
||||
host: "https://dev-config-soma.bknd.run"
|
||||
});
|
||||
|
||||
const one = await dataApi.readOne("users", 1);
|
||||
const many = await dataApi.readMany("users", { limit: 2 });
|
||||
console.log("one", one);
|
||||
console.log("many", many);*/
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||
import type { ModuleBuildContext } from "../../src";
|
||||
import { App, createApp } from "core/test/utils";
|
||||
import * as proto from "data/prototype";
|
||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("App", () => {
|
||||
test("use db mode by default", async () => {
|
||||
const app = createApp();
|
||||
await app.build();
|
||||
|
||||
expect(app.mode).toBe("db");
|
||||
expect(app.isReadOnly()).toBe(false);
|
||||
expect(app.modules instanceof DbModuleManager).toBe(true);
|
||||
});
|
||||
|
||||
test("seed includes ctx and app", async () => {
|
||||
const called = mock(() => null);
|
||||
await createApp({
|
||||
options: {
|
||||
seed: async ({ app, ...ctx }) => {
|
||||
called();
|
||||
expect(app).toBeDefined();
|
||||
expect(ctx).toBeDefined();
|
||||
expect(Object.keys(ctx)).toEqual([
|
||||
"connection",
|
||||
"server",
|
||||
"em",
|
||||
"emgr",
|
||||
"guard",
|
||||
"flags",
|
||||
"logger",
|
||||
"mcp",
|
||||
"helper",
|
||||
]);
|
||||
},
|
||||
},
|
||||
}).build();
|
||||
expect(called).toHaveBeenCalled();
|
||||
|
||||
const app = createApp({
|
||||
config: {
|
||||
data: proto
|
||||
.em({
|
||||
todos: proto.entity("todos", {
|
||||
title: proto.text(),
|
||||
}),
|
||||
})
|
||||
.toJSON(),
|
||||
},
|
||||
options: {
|
||||
//manager: { verbosity: 2 },
|
||||
seed: async ({ app, ...ctx }: ModuleBuildContext & { app: App }) => {
|
||||
await ctx.em.mutator("todos").insertOne({ title: "ctx" });
|
||||
await app.getApi().data.createOne("todos", { title: "api" });
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
const todos = await app.getApi().data.readMany("todos");
|
||||
expect(todos.length).toBe(2);
|
||||
expect(todos[0]?.title).toBe("ctx");
|
||||
expect(todos[1]?.title).toBe("api");
|
||||
});
|
||||
|
||||
test("lifecycle events are triggered", async () => {
|
||||
const firstBoot = mock(() => null);
|
||||
const configUpdate = mock(() => null);
|
||||
const appBuilt = mock(() => null);
|
||||
const appRequest = mock(() => null);
|
||||
const beforeResponse = mock(() => null);
|
||||
|
||||
const app = createApp();
|
||||
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppFirstBoot,
|
||||
(event) => {
|
||||
expect(event).toBeInstanceOf(App.Events.AppFirstBoot);
|
||||
expect(event.params.app.version()).toBe(app.version());
|
||||
firstBoot();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppBuiltEvent,
|
||||
(event) => {
|
||||
expect(event).toBeInstanceOf(App.Events.AppBuiltEvent);
|
||||
expect(event.params.app.version()).toBe(app.version());
|
||||
appBuilt();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppConfigUpdatedEvent,
|
||||
() => {
|
||||
configUpdate();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppRequest,
|
||||
(event) => {
|
||||
expect(event).toBeInstanceOf(App.Events.AppRequest);
|
||||
expect(event.params.app.version()).toBe(app.version());
|
||||
expect(event.params.request).toBeInstanceOf(Request);
|
||||
appRequest();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppBeforeResponse,
|
||||
(event) => {
|
||||
expect(event).toBeInstanceOf(App.Events.AppBeforeResponse);
|
||||
expect(event.params.app.version()).toBe(app.version());
|
||||
expect(event.params.response).toBeInstanceOf(Response);
|
||||
beforeResponse();
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
|
||||
await app.build();
|
||||
expect(firstBoot).toHaveBeenCalled();
|
||||
expect(appBuilt).toHaveBeenCalled();
|
||||
//expect(configUpdate).toHaveBeenCalled();
|
||||
expect(appRequest).not.toHaveBeenCalled();
|
||||
expect(beforeResponse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("emgr exec modes", async () => {
|
||||
const called = mock(() => null);
|
||||
const app = createApp({
|
||||
options: {
|
||||
asyncEventsMode: "sync",
|
||||
},
|
||||
});
|
||||
|
||||
// register async listener
|
||||
app.emgr.onEvent(App.Events.AppFirstBoot, async () => {
|
||||
called();
|
||||
});
|
||||
|
||||
await app.build();
|
||||
await app.server.request(new Request("http://localhost"));
|
||||
|
||||
// expect async listeners to be executed sync after request
|
||||
expect(called).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("getMcpClient", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
const client = app.getMcpClient();
|
||||
const res = await client.listTools();
|
||||
expect(res).toBeDefined();
|
||||
expect(res?.tools.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { AppServer, serverConfigSchema } from "modules/server/AppServer";
|
||||
import { describe, test, expect } from "bun:test";
|
||||
|
||||
describe("AppServer", () => {
|
||||
test("config", () => {
|
||||
{
|
||||
const server = new AppServer();
|
||||
expect(server).toBeDefined();
|
||||
expect(server.config).toEqual({
|
||||
cors: {
|
||||
allow_credentials: true,
|
||||
origin: "*",
|
||||
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||
},
|
||||
mcp: {
|
||||
enabled: false,
|
||||
path: "/api/system/mcp",
|
||||
logLevel: "warning",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const server = new AppServer({
|
||||
cors: {
|
||||
origin: "https",
|
||||
allow_methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
expect(server).toBeDefined();
|
||||
expect(server.config).toEqual({
|
||||
cors: {
|
||||
allow_credentials: true,
|
||||
origin: "https",
|
||||
allow_methods: ["GET", "POST"],
|
||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||
},
|
||||
mcp: {
|
||||
enabled: false,
|
||||
path: "/api/system/mcp",
|
||||
logLevel: "warning",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
|
||||
import { getDummyConnection } from "../../__test__/helper";
|
||||
import { ModuleManager } from "modules/ModuleManager";
|
||||
import { em, entity, text } from "data/prototype";
|
||||
|
||||
async function createApp(config: CreateAppConfig = {}) {
|
||||
const app = internalCreateApp({
|
||||
connection: getDummyConnection().dummyConnection,
|
||||
...config,
|
||||
options: {
|
||||
...config.options,
|
||||
mode: "code",
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
return app;
|
||||
}
|
||||
|
||||
describe("code-only", () => {
|
||||
test("should create app with correct manager", async () => {
|
||||
const app = await createApp();
|
||||
await app.build();
|
||||
|
||||
expect(app.version()).toBeDefined();
|
||||
expect(app.modules).toBeInstanceOf(ModuleManager);
|
||||
});
|
||||
|
||||
test("should not perform database syncs", async () => {
|
||||
const app = await createApp({
|
||||
config: {
|
||||
data: em({
|
||||
test: entity("test", {
|
||||
name: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
});
|
||||
expect(app.em.entities.map((e) => e.name)).toEqual(["test"]);
|
||||
expect(
|
||||
await app.em.connection.kysely
|
||||
.selectFrom("sqlite_master")
|
||||
.where("type", "=", "table")
|
||||
.selectAll()
|
||||
.execute(),
|
||||
).toEqual([]);
|
||||
|
||||
// only perform when explicitly forced
|
||||
await app.em.schema().sync({ force: true });
|
||||
expect(
|
||||
await app.em.connection.kysely
|
||||
.selectFrom("sqlite_master")
|
||||
.where("type", "=", "table")
|
||||
.selectAll()
|
||||
.execute()
|
||||
.then((r) => r.map((r) => r.name)),
|
||||
).toEqual(["test", "sqlite_sequence"]);
|
||||
});
|
||||
|
||||
test("should not perform seeding", async () => {
|
||||
const called = mock(() => null);
|
||||
const app = await createApp({
|
||||
config: {
|
||||
data: em({
|
||||
test: entity("test", {
|
||||
name: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
options: {
|
||||
seed: async (ctx) => {
|
||||
called();
|
||||
await ctx.em.mutator("test").insertOne({ name: "test" });
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.em.schema().sync({ force: true });
|
||||
expect(called).not.toHaveBeenCalled();
|
||||
expect(
|
||||
await app.em
|
||||
.repo("test")
|
||||
.findMany({})
|
||||
.then((r) => r.data),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test("should sync and perform seeding", async () => {
|
||||
const called = mock(() => null);
|
||||
const app = await createApp({
|
||||
config: {
|
||||
data: em({
|
||||
test: entity("test", {
|
||||
name: text(),
|
||||
}),
|
||||
}).toJSON(),
|
||||
},
|
||||
options: {
|
||||
seed: async (ctx) => {
|
||||
called();
|
||||
await ctx.em.mutator("test").insertOne({ name: "test" });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await app.em.schema().sync({ force: true });
|
||||
await app.options?.seed?.({
|
||||
...app.modules.ctx(),
|
||||
app: app,
|
||||
});
|
||||
expect(called).toHaveBeenCalled();
|
||||
expect(
|
||||
await app.em
|
||||
.repo("test")
|
||||
.findMany({})
|
||||
.then((r) => r.data),
|
||||
).toEqual([{ id: 1, name: "test" }]);
|
||||
});
|
||||
|
||||
test("should not allow to modify config", async () => {
|
||||
const app = await createApp();
|
||||
// biome-ignore lint/suspicious/noPrototypeBuiltins: <explanation>
|
||||
expect(app.modules.hasOwnProperty("mutateConfigSafe")).toBe(false);
|
||||
expect(() => {
|
||||
app.modules.configs().auth.enabled = true;
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
import { describe, test, expect, beforeEach, beforeAll, afterAll } from "bun:test";
|
||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import type { McpServer } from "bknd/utils";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
/**
|
||||
* - [x] auth_me
|
||||
* - [x] auth_strategies
|
||||
* - [x] auth_user_create
|
||||
* - [x] auth_user_token
|
||||
* - [x] auth_user_password_change
|
||||
* - [x] auth_user_password_test
|
||||
* - [x] config_auth_get
|
||||
* - [x] config_auth_update
|
||||
* - [x] config_auth_strategies_get
|
||||
* - [x] config_auth_strategies_add
|
||||
* - [x] config_auth_strategies_update
|
||||
* - [x] config_auth_strategies_remove
|
||||
* - [x] config_auth_roles_get
|
||||
* - [x] config_auth_roles_add
|
||||
* - [x] config_auth_roles_update
|
||||
* - [x] config_auth_roles_remove
|
||||
*/
|
||||
describe("mcp auth", async () => {
|
||||
let app: App;
|
||||
let server: McpServer;
|
||||
beforeEach(async () => {
|
||||
app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "secret",
|
||||
},
|
||||
},
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
await app.getMcpClient().ping();
|
||||
server = app.mcp!;
|
||||
server.setLogLevel("error");
|
||||
server.onNotification((message) => {
|
||||
console.dir(message, { depth: null });
|
||||
});
|
||||
});
|
||||
|
||||
const tool = createMcpToolCaller();
|
||||
|
||||
test("auth_*", async () => {
|
||||
const me = await tool(server, "auth_me", {});
|
||||
expect(me.user).toBeNull();
|
||||
|
||||
// strategies
|
||||
const strategies = await tool(server, "auth_strategies", {});
|
||||
expect(Object.keys(strategies.strategies).length).toEqual(1);
|
||||
expect(strategies.strategies.password.enabled).toBe(true);
|
||||
|
||||
// create user
|
||||
const user = await tool(
|
||||
server,
|
||||
"auth_user_create",
|
||||
{
|
||||
email: "test@test.com",
|
||||
password: "12345678",
|
||||
},
|
||||
new Headers(),
|
||||
);
|
||||
expect(user.email).toBe("test@test.com");
|
||||
|
||||
// create token
|
||||
const token = await tool(
|
||||
server,
|
||||
"auth_user_token",
|
||||
{
|
||||
email: "test@test.com",
|
||||
},
|
||||
new Headers(),
|
||||
);
|
||||
expect(token.token).toBeDefined();
|
||||
expect(token.user.email).toBe("test@test.com");
|
||||
|
||||
// me
|
||||
const me2 = await tool(
|
||||
server,
|
||||
"auth_me",
|
||||
{},
|
||||
new Request("http://localhost", {
|
||||
headers: new Headers({
|
||||
Authorization: `Bearer ${token.token}`,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(me2.user.email).toBe("test@test.com");
|
||||
|
||||
// change password
|
||||
const changePassword = await tool(
|
||||
server,
|
||||
"auth_user_password_change",
|
||||
{
|
||||
email: "test@test.com",
|
||||
password: "87654321",
|
||||
},
|
||||
new Headers(),
|
||||
);
|
||||
expect(changePassword.changed).toBe(true);
|
||||
|
||||
// test password
|
||||
const testPassword = await tool(
|
||||
server,
|
||||
"auth_user_password_test",
|
||||
{
|
||||
email: "test@test.com",
|
||||
password: "87654321",
|
||||
},
|
||||
new Headers(),
|
||||
);
|
||||
expect(testPassword.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("config_auth_{get,update}", async () => {
|
||||
expect(await tool(server, "config_auth_get", {})).toEqual({
|
||||
path: "",
|
||||
secrets: false,
|
||||
partial: false,
|
||||
value: app.toJSON().auth,
|
||||
});
|
||||
|
||||
// update
|
||||
await tool(server, "config_auth_update", {
|
||||
value: {
|
||||
allow_register: false,
|
||||
},
|
||||
});
|
||||
expect(app.toJSON().auth.allow_register).toBe(false);
|
||||
});
|
||||
|
||||
test("config_auth_strategies_{get,add,update,remove}", async () => {
|
||||
const strategies = await tool(server, "config_auth_strategies_get", {
|
||||
key: "password",
|
||||
});
|
||||
expect(strategies).toEqual({
|
||||
secrets: false,
|
||||
module: "auth",
|
||||
key: "password",
|
||||
value: {
|
||||
enabled: true,
|
||||
type: "password",
|
||||
},
|
||||
});
|
||||
|
||||
// add google oauth
|
||||
const addGoogleOauth = await tool(server, "config_auth_strategies_add", {
|
||||
key: "google",
|
||||
value: {
|
||||
type: "oauth",
|
||||
enabled: true,
|
||||
config: {
|
||||
name: "google",
|
||||
type: "oidc",
|
||||
client: {
|
||||
client_id: "client_id",
|
||||
client_secret: "client_secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
return_config: true,
|
||||
});
|
||||
expect(addGoogleOauth.config.google.enabled).toBe(true);
|
||||
expect(app.toJSON().auth.strategies.google?.enabled).toBe(true);
|
||||
|
||||
// update (disable) google oauth
|
||||
await tool(server, "config_auth_strategies_update", {
|
||||
key: "google",
|
||||
value: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
expect(app.toJSON().auth.strategies.google?.enabled).toBe(false);
|
||||
|
||||
// remove google oauth
|
||||
await tool(server, "config_auth_strategies_remove", {
|
||||
key: "google",
|
||||
});
|
||||
expect(app.toJSON().auth.strategies.google).toBeUndefined();
|
||||
});
|
||||
|
||||
test("config_auth_roles_{get,add,update,remove}", async () => {
|
||||
// add role
|
||||
const addGuestRole = await tool(server, "config_auth_roles_add", {
|
||||
key: "guest",
|
||||
value: {
|
||||
permissions: ["read", "write"],
|
||||
},
|
||||
return_config: true,
|
||||
});
|
||||
expect(addGuestRole.config.guest.permissions.map((p) => p.permission)).toEqual([
|
||||
"read",
|
||||
"write",
|
||||
]);
|
||||
|
||||
// update role
|
||||
await tool(server, "config_auth_roles_update", {
|
||||
key: "guest",
|
||||
value: {
|
||||
permissions: ["read"],
|
||||
},
|
||||
});
|
||||
expect(app.toJSON().auth.roles?.guest?.permissions?.map((p) => p.permission)).toEqual([
|
||||
"read",
|
||||
]);
|
||||
|
||||
// get role
|
||||
const getGuestRole = await tool(server, "config_auth_roles_get", {
|
||||
key: "guest",
|
||||
});
|
||||
expect(getGuestRole.value.permissions.map((p) => p.permission)).toEqual(["read"]);
|
||||
|
||||
// remove role
|
||||
await tool(server, "config_auth_roles_remove", {
|
||||
key: "guest",
|
||||
});
|
||||
expect(app.toJSON().auth.roles?.guest).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { registries } from "index";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("mcp", () => {
|
||||
it("should have tools", async () => {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
|
||||
const app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./",
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
// expect mcp to not be loaded yet
|
||||
expect(app.mcp).toBeNull();
|
||||
|
||||
// after first request, mcp should be loaded
|
||||
await app.getMcpClient().listTools();
|
||||
expect(app.mcp?.tools.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,347 @@
|
||||
import { describe, test, expect, beforeEach, beforeAll, afterAll } from "bun:test";
|
||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||
import { getSystemMcp } from "modules/mcp/system-mcp";
|
||||
import { pickKeys, type McpServer } from "bknd/utils";
|
||||
import { entity, text } from "bknd";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
/**
|
||||
* - [ ] data_sync
|
||||
* - [x] data_entity_fn_count
|
||||
* - [x] data_entity_fn_exists
|
||||
* - [x] data_entity_read_one
|
||||
* - [x] data_entity_read_many
|
||||
* - [x] data_entity_insert
|
||||
* - [x] data_entity_update_many
|
||||
* - [x] data_entity_update_one
|
||||
* - [x] data_entity_delete_one
|
||||
* - [x] data_entity_delete_many
|
||||
* - [x] data_entity_info
|
||||
* - [ ] config_data_get
|
||||
* - [ ] config_data_update
|
||||
* - [x] config_data_entities_get
|
||||
* - [x] config_data_entities_add
|
||||
* - [x] config_data_entities_update
|
||||
* - [x] config_data_entities_remove
|
||||
* - [x] config_data_relations_add
|
||||
* - [x] config_data_relations_get
|
||||
* - [x] config_data_relations_update
|
||||
* - [x] config_data_relations_remove
|
||||
* - [x] config_data_indices_get
|
||||
* - [x] config_data_indices_add
|
||||
* - [x] config_data_indices_update
|
||||
* - [x] config_data_indices_remove
|
||||
*/
|
||||
describe("mcp data", async () => {
|
||||
let app: App;
|
||||
let server: McpServer;
|
||||
beforeEach(async () => {
|
||||
const time = performance.now();
|
||||
app = createApp({
|
||||
config: {
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
await app.getMcpClient().ping();
|
||||
server = app.mcp!;
|
||||
server.setLogLevel("error");
|
||||
server.onNotification((message) => {
|
||||
console.dir(message, { depth: null });
|
||||
});
|
||||
});
|
||||
|
||||
const tool = createMcpToolCaller();
|
||||
|
||||
test("config_data_entities_{add,get,update,remove}", async () => {
|
||||
const result = await tool(server, "config_data_entities_add", {
|
||||
key: "test",
|
||||
return_config: true,
|
||||
value: {},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.module).toBe("data");
|
||||
expect(result.config.test?.type).toEqual("regular");
|
||||
|
||||
const entities = Object.keys(app.toJSON().data.entities ?? {});
|
||||
expect(entities).toContain("test");
|
||||
|
||||
{
|
||||
// get
|
||||
const result = await tool(server, "config_data_entities_get", {
|
||||
key: "test",
|
||||
});
|
||||
expect(result.module).toBe("data");
|
||||
expect(result.key).toBe("test");
|
||||
expect(result.value.type).toEqual("regular");
|
||||
}
|
||||
|
||||
{
|
||||
// update
|
||||
const result = await tool(server, "config_data_entities_update", {
|
||||
key: "test",
|
||||
return_config: true,
|
||||
value: {
|
||||
config: {
|
||||
name: "Test",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.module).toBe("data");
|
||||
expect(result.config.test.config?.name).toEqual("Test");
|
||||
expect(app.toJSON().data.entities?.test?.config?.name).toEqual("Test");
|
||||
}
|
||||
|
||||
{
|
||||
// remove
|
||||
const result = await tool(server, "config_data_entities_remove", {
|
||||
key: "test",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.module).toBe("data");
|
||||
expect(app.toJSON().data.entities?.test).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("config_data_relations_{add,get,update,remove}", async () => {
|
||||
// create posts and comments
|
||||
await tool(server, "config_data_entities_add", {
|
||||
key: "posts",
|
||||
value: {},
|
||||
});
|
||||
await tool(server, "config_data_entities_add", {
|
||||
key: "comments",
|
||||
value: {},
|
||||
});
|
||||
|
||||
expect(Object.keys(app.toJSON().data.entities ?? {})).toEqual(["posts", "comments"]);
|
||||
|
||||
// create relation
|
||||
await tool(server, "config_data_relations_add", {
|
||||
key: "", // doesn't matter
|
||||
value: {
|
||||
type: "n:1",
|
||||
source: "comments",
|
||||
target: "posts",
|
||||
},
|
||||
});
|
||||
|
||||
const config = app.toJSON().data;
|
||||
expect(
|
||||
pickKeys((config.relations?.n1_comments_posts as any) ?? {}, ["type", "source", "target"]),
|
||||
).toEqual({
|
||||
type: "n:1",
|
||||
source: "comments",
|
||||
target: "posts",
|
||||
});
|
||||
|
||||
expect(config.entities?.comments?.fields?.posts_id?.type).toBe("relation");
|
||||
|
||||
{
|
||||
// info
|
||||
const postsInfo = await tool(server, "data_entity_info", {
|
||||
entity: "posts",
|
||||
});
|
||||
expect(postsInfo.fields).toEqual(["id"]);
|
||||
expect(postsInfo.relations.all.length).toBe(1);
|
||||
|
||||
const commentsInfo = await tool(server, "data_entity_info", {
|
||||
entity: "comments",
|
||||
});
|
||||
expect(commentsInfo.fields).toEqual(["id", "posts_id"]);
|
||||
expect(commentsInfo.relations.all.length).toBe(1);
|
||||
}
|
||||
|
||||
// update
|
||||
await tool(server, "config_data_relations_update", {
|
||||
key: "n1_comments_posts",
|
||||
value: {
|
||||
config: {
|
||||
with_limit: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect((app.toJSON().data.relations?.n1_comments_posts?.config as any)?.with_limit).toBe(10);
|
||||
|
||||
// delete
|
||||
await tool(server, "config_data_relations_remove", {
|
||||
key: "n1_comments_posts",
|
||||
});
|
||||
expect(app.toJSON().data.relations?.n1_comments_posts).toBeUndefined();
|
||||
});
|
||||
|
||||
test("config_data_indices_update", async () => {
|
||||
expect(server.tools.map((t) => t.name).includes("config_data_indices_update")).toBe(false);
|
||||
});
|
||||
|
||||
test("config_data_indices_{add,get,remove}", async () => {
|
||||
// create posts and comments
|
||||
await tool(server, "config_data_entities_add", {
|
||||
key: "posts",
|
||||
value: entity("posts", {
|
||||
title: text(),
|
||||
content: text(),
|
||||
}).toJSON(),
|
||||
});
|
||||
|
||||
// add index on title
|
||||
await tool(server, "config_data_indices_add", {
|
||||
key: "", // auto generated
|
||||
value: {
|
||||
entity: "posts",
|
||||
fields: ["title"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.toJSON().data.indices?.idx_posts_title).toEqual({
|
||||
entity: "posts",
|
||||
fields: ["title"],
|
||||
unique: false,
|
||||
});
|
||||
|
||||
// delete
|
||||
await tool(server, "config_data_indices_remove", {
|
||||
key: "idx_posts_title",
|
||||
});
|
||||
expect(app.toJSON().data.indices?.idx_posts_title).toBeUndefined();
|
||||
});
|
||||
|
||||
test("data_entity_*", async () => {
|
||||
// create posts and comments
|
||||
await tool(server, "config_data_entities_add", {
|
||||
key: "posts",
|
||||
value: entity("posts", {
|
||||
title: text(),
|
||||
content: text(),
|
||||
}).toJSON(),
|
||||
});
|
||||
await tool(server, "config_data_entities_add", {
|
||||
key: "comments",
|
||||
value: entity("comments", {
|
||||
content: text(),
|
||||
}).toJSON(),
|
||||
});
|
||||
|
||||
// insert a few posts
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await tool(server, "data_entity_insert", {
|
||||
entity: "posts",
|
||||
json: {
|
||||
title: `Post ${i}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
// insert a few comments
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await tool(server, "data_entity_insert", {
|
||||
entity: "comments",
|
||||
json: {
|
||||
content: `Comment ${i}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const result = await tool(server, "data_entity_read_many", {
|
||||
entity: "posts",
|
||||
limit: 5,
|
||||
});
|
||||
expect(result.data.length).toBe(5);
|
||||
expect(result.meta.items).toBe(5);
|
||||
expect(result.meta.total).toBe(10);
|
||||
expect(result.data[0].title).toBe("Post 0");
|
||||
|
||||
{
|
||||
// count
|
||||
const result = await tool(server, "data_entity_fn_count", {
|
||||
entity: "posts",
|
||||
});
|
||||
expect(result.count).toBe(10);
|
||||
}
|
||||
|
||||
{
|
||||
// exists
|
||||
const res = await tool(server, "data_entity_fn_exists", {
|
||||
entity: "posts",
|
||||
json: {
|
||||
id: result.data[0].id,
|
||||
},
|
||||
});
|
||||
expect(res.exists).toBe(true);
|
||||
|
||||
const res2 = await tool(server, "data_entity_fn_exists", {
|
||||
entity: "posts",
|
||||
json: {
|
||||
id: "123",
|
||||
},
|
||||
});
|
||||
expect(res2.exists).toBe(false);
|
||||
}
|
||||
|
||||
// update
|
||||
await tool(server, "data_entity_update_one", {
|
||||
entity: "posts",
|
||||
id: result.data[0].id,
|
||||
json: {
|
||||
title: "Post 0 updated",
|
||||
},
|
||||
});
|
||||
const result2 = await tool(server, "data_entity_read_one", {
|
||||
entity: "posts",
|
||||
id: result.data[0].id,
|
||||
});
|
||||
expect(result2.data.title).toBe("Post 0 updated");
|
||||
|
||||
// delete the second post
|
||||
await tool(server, "data_entity_delete_one", {
|
||||
entity: "posts",
|
||||
id: result.data[1].id,
|
||||
});
|
||||
const result3 = await tool(server, "data_entity_read_many", {
|
||||
entity: "posts",
|
||||
limit: 2,
|
||||
});
|
||||
expect(result3.data.map((p) => p.id)).toEqual([1, 3]);
|
||||
|
||||
// update many
|
||||
await tool(server, "data_entity_update_many", {
|
||||
entity: "posts",
|
||||
update: {
|
||||
title: "Post updated",
|
||||
},
|
||||
where: {
|
||||
title: { $isnull: 0 },
|
||||
},
|
||||
});
|
||||
const result4 = await tool(server, "data_entity_read_many", {
|
||||
entity: "posts",
|
||||
limit: 10,
|
||||
});
|
||||
expect(result4.data.length).toBe(9);
|
||||
expect(result4.data.map((p) => p.title)).toEqual(
|
||||
Array.from({ length: 9 }, () => "Post updated"),
|
||||
);
|
||||
|
||||
// delete many
|
||||
await tool(server, "data_entity_delete_many", {
|
||||
entity: "posts",
|
||||
json: {
|
||||
title: { $isnull: 0 },
|
||||
},
|
||||
});
|
||||
const result5 = await tool(server, "data_entity_read_many", {
|
||||
entity: "posts",
|
||||
limit: 10,
|
||||
});
|
||||
expect(result5.data.length).toBe(0);
|
||||
expect(result5.meta.items).toBe(0);
|
||||
expect(result5.meta.total).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, test, expect, beforeAll, beforeEach, afterAll } from "bun:test";
|
||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||
import { getSystemMcp } from "modules/mcp/system-mcp";
|
||||
import { registries } from "index";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import type { McpServer } from "bknd/utils";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
/**
|
||||
* - [x] config_media_get
|
||||
* - [x] config_media_update
|
||||
* - [x] config_media_adapter_get
|
||||
* - [x] config_media_adapter_update
|
||||
*/
|
||||
describe("mcp media", async () => {
|
||||
let app: App;
|
||||
let server: McpServer;
|
||||
beforeEach(async () => {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
app = createApp({
|
||||
config: {
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./",
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
await app.getMcpClient().ping();
|
||||
server = app.mcp!;
|
||||
server.setLogLevel("error");
|
||||
server.onNotification((message) => {
|
||||
console.dir(message, { depth: null });
|
||||
});
|
||||
});
|
||||
|
||||
const tool = createMcpToolCaller();
|
||||
|
||||
test("config_media_{get,update}", async () => {
|
||||
const result = await tool(server, "config_media_get", {});
|
||||
expect(result).toEqual({
|
||||
path: "",
|
||||
secrets: false,
|
||||
partial: false,
|
||||
value: app.toJSON().media,
|
||||
});
|
||||
|
||||
// partial
|
||||
expect((await tool(server, "config_media_get", { path: "adapter" })).value).toEqual({
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./",
|
||||
},
|
||||
});
|
||||
|
||||
// update
|
||||
await tool(server, "config_media_update", {
|
||||
value: {
|
||||
storage: {
|
||||
body_max_size: 1024 * 1024 * 10,
|
||||
},
|
||||
},
|
||||
return_config: true,
|
||||
});
|
||||
expect(app.toJSON().media.storage.body_max_size).toBe(1024 * 1024 * 10);
|
||||
});
|
||||
|
||||
test("config_media_adapter_{get,update}", async () => {
|
||||
const result = await tool(server, "config_media_adapter_get", {});
|
||||
expect(result).toEqual({
|
||||
secrets: false,
|
||||
value: app.toJSON().media.adapter,
|
||||
});
|
||||
|
||||
// update
|
||||
await tool(server, "config_media_adapter_update", {
|
||||
value: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./subdir",
|
||||
},
|
||||
},
|
||||
});
|
||||
const adapter = app.toJSON().media.adapter as any;
|
||||
expect(adapter.config.path).toBe("./subdir");
|
||||
expect(adapter.type).toBe("local");
|
||||
|
||||
// set to s3
|
||||
{
|
||||
await tool(server, "config_media_adapter_update", {
|
||||
value: {
|
||||
type: "s3",
|
||||
config: {
|
||||
access_key: "123",
|
||||
secret_access_key: "456",
|
||||
url: "https://example.com/what",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const adapter = app.toJSON(true).media.adapter as any;
|
||||
expect(adapter.type).toBe("s3");
|
||||
expect(adapter.config.url).toBe("https://example.com/what");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||
import type { McpServer } from "bknd/utils";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
/**
|
||||
* - [x] config_server_get
|
||||
* - [x] config_server_update
|
||||
*/
|
||||
describe("mcp system", async () => {
|
||||
let app: App;
|
||||
let server: McpServer;
|
||||
beforeAll(async () => {
|
||||
app = createApp({
|
||||
config: {
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
await app.getMcpClient().ping();
|
||||
server = app.mcp!;
|
||||
});
|
||||
|
||||
const tool = createMcpToolCaller();
|
||||
|
||||
test("config_server_get", async () => {
|
||||
const result = await tool(server, "config_server_get", {});
|
||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
||||
path: "",
|
||||
secrets: false,
|
||||
partial: false,
|
||||
value: JSON.parse(JSON.stringify(app.toJSON().server)),
|
||||
});
|
||||
});
|
||||
|
||||
test("config_server_get2", async () => {
|
||||
const result = await tool(server, "config_server_get", {});
|
||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
||||
path: "",
|
||||
secrets: false,
|
||||
partial: false,
|
||||
value: JSON.parse(JSON.stringify(app.toJSON().server)),
|
||||
});
|
||||
});
|
||||
|
||||
test("config_server_update", async () => {
|
||||
const original = JSON.parse(JSON.stringify(app.toJSON().server));
|
||||
const result = await tool(server, "config_server_update", {
|
||||
value: {
|
||||
cors: {
|
||||
origin: "http://localhost",
|
||||
},
|
||||
},
|
||||
return_config: true,
|
||||
});
|
||||
|
||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
||||
success: true,
|
||||
module: "server",
|
||||
config: {
|
||||
...original,
|
||||
cors: {
|
||||
...original.cors,
|
||||
origin: "http://localhost",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(app.toJSON().server.cors.origin).toBe("http://localhost");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { AppEvents } from "App";
|
||||
import { describe, test, expect, beforeAll, mock } from "bun:test";
|
||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||
import type { McpServer } from "bknd/utils";
|
||||
|
||||
/**
|
||||
* - [x] system_config
|
||||
* - [x] system_build
|
||||
* - [x] system_ping
|
||||
* - [x] system_info
|
||||
*/
|
||||
describe("mcp system", async () => {
|
||||
let app: App;
|
||||
let server: McpServer;
|
||||
beforeAll(async () => {
|
||||
app = createApp({
|
||||
config: {
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
await app.getMcpClient().ping();
|
||||
server = app.mcp!;
|
||||
});
|
||||
|
||||
const tool = createMcpToolCaller();
|
||||
|
||||
test("system_ping", async () => {
|
||||
const result = await tool(server, "system_ping", {});
|
||||
expect(result).toEqual({ pong: true });
|
||||
});
|
||||
|
||||
test("system_info", async () => {
|
||||
const result = await tool(server, "system_info", {});
|
||||
expect(Object.keys(result).length).toBeGreaterThan(0);
|
||||
expect(Object.keys(result)).toContainValues(["version", "runtime", "connection"]);
|
||||
});
|
||||
|
||||
test("system_build", async () => {
|
||||
const called = mock(() => null);
|
||||
|
||||
app.emgr.onEvent(AppEvents.AppBuiltEvent, () => void called(), { once: true });
|
||||
|
||||
const result = await tool(server, "system_build", {});
|
||||
expect(called).toHaveBeenCalledTimes(1);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
test("system_config", async () => {
|
||||
const result = await tool(server, "system_config", {});
|
||||
expect(result).toEqual(app.toJSON());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, mock, beforeAll, afterAll } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { syncConfig } from "plugins/dev/sync-config.plugin";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("syncConfig", () => {
|
||||
it("should only sync if enabled", async () => {
|
||||
const called = mock(() => null);
|
||||
const app = createApp();
|
||||
await app.build();
|
||||
|
||||
await syncConfig({
|
||||
write: () => {
|
||||
called();
|
||||
},
|
||||
enabled: false,
|
||||
includeFirstBoot: false,
|
||||
})(app).onBuilt?.();
|
||||
expect(called).not.toHaveBeenCalled();
|
||||
|
||||
await syncConfig({
|
||||
write: () => {
|
||||
called();
|
||||
},
|
||||
enabled: false,
|
||||
includeFirstBoot: true,
|
||||
})(app).onBuilt?.();
|
||||
expect(called).not.toHaveBeenCalled();
|
||||
|
||||
await syncConfig({
|
||||
write: () => {
|
||||
called();
|
||||
},
|
||||
enabled: true,
|
||||
includeFirstBoot: true,
|
||||
})(app).onBuilt?.();
|
||||
expect(called).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should respect secrets", async () => {
|
||||
const called = mock(() => null);
|
||||
const app = createApp({
|
||||
config: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
await syncConfig({
|
||||
write: async (config) => {
|
||||
called();
|
||||
expect(config.auth.jwt.secret).toBe("test");
|
||||
},
|
||||
enabled: true,
|
||||
includeSecrets: true,
|
||||
includeFirstBoot: true,
|
||||
})(app).onBuilt?.();
|
||||
|
||||
await syncConfig({
|
||||
write: async (config) => {
|
||||
called();
|
||||
// it's an important test, because the `jwt` part is omitted if secrets=false in general app.toJSON()
|
||||
// but it's required to get the app running
|
||||
expect(config.auth.jwt.secret).toBe("");
|
||||
},
|
||||
enabled: true,
|
||||
includeSecrets: false,
|
||||
includeFirstBoot: true,
|
||||
})(app).onBuilt?.();
|
||||
expect(called).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { registries } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("repros", async () => {
|
||||
/**
|
||||
* steps:
|
||||
* 1. enable media
|
||||
* 2. create 'test' entity
|
||||
* 3. add media to 'test'
|
||||
*
|
||||
* There was an issue that AppData had old configs because of system entity "media"
|
||||
*/
|
||||
test("registers media entity correctly to relate to it", async () => {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
const app = createApp();
|
||||
await app.build();
|
||||
|
||||
{
|
||||
// 1. enable media
|
||||
const [, config] = await app.module.media.schema().patch("", {
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.enabled).toBe(true);
|
||||
}
|
||||
|
||||
{
|
||||
// 2. create 'test' entity
|
||||
await app.module.data.schema().patch(
|
||||
"entities.test",
|
||||
proto
|
||||
.entity("test", {
|
||||
content: proto.text(),
|
||||
})
|
||||
.toJSON(),
|
||||
);
|
||||
expect(app.em.entities.map((e) => e.name)).toContain("test");
|
||||
}
|
||||
|
||||
{
|
||||
await app.module.data.schema().patch("entities.test.fields.files", {
|
||||
type: "media",
|
||||
config: {
|
||||
required: false,
|
||||
fillable: ["update"],
|
||||
hidden: false,
|
||||
mime_types: [],
|
||||
virtual: true,
|
||||
entity: "test",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
app.module.data.schema().patch("relations.000", {
|
||||
type: "poly",
|
||||
source: "test",
|
||||
target: "media",
|
||||
config: { mappedBy: "files" },
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
}
|
||||
|
||||
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
||||
});
|
||||
|
||||
test.only("verify inversedBy", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
products: proto.entity("products", {
|
||||
title: proto.text(),
|
||||
}),
|
||||
product_likes: proto.entity("product_likes", {
|
||||
created_at: proto.date(),
|
||||
}),
|
||||
users: proto.entity("users", {}),
|
||||
},
|
||||
(fns, schema) => {
|
||||
fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" });
|
||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||
},
|
||||
);
|
||||
const app = createApp({ config: { data: schema.toJSON() } });
|
||||
await app.build();
|
||||
|
||||
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||
|
||||
expect(info.fields).toEqual(["id", "title"]);
|
||||
expect(info.relations.listable).toEqual([
|
||||
{
|
||||
entity: "product_likes",
|
||||
ref: "likes",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,46 +1,41 @@
|
||||
import { Authenticator } from "auth/authenticate/Authenticator";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
||||
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
||||
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
||||
import * as hash from "../../src/auth/utils/hash";
|
||||
import { Default, parse } from "../../src/core/utils";
|
||||
|
||||
/*class MemoryUserPool implements UserPool {
|
||||
constructor(private users: User[] = []) {}
|
||||
|
||||
async findBy(prop: "id" | "email" | "username", value: string | number) {
|
||||
return this.users.find((user) => user[prop] === value);
|
||||
}
|
||||
|
||||
async create(user: Pick<User, "email" | "password">) {
|
||||
const id = this.users.length + 1;
|
||||
const newUser = { ...user, id, username: user.email };
|
||||
this.users.push(newUser);
|
||||
return newUser;
|
||||
}
|
||||
}*/
|
||||
|
||||
describe("Authenticator", async () => {
|
||||
test("cookie options", async () => {
|
||||
console.log("parsed", parse(cookieConfig, undefined));
|
||||
console.log(Default(cookieConfig, {}));
|
||||
});
|
||||
/*const userpool = new MemoryUserPool([
|
||||
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
||||
]);
|
||||
|
||||
test("sha256 login", async () => {
|
||||
const auth = new Authenticator(userpool, {
|
||||
password: new PasswordStrategy({
|
||||
hashing: "sha256",
|
||||
}),
|
||||
test("should return auth cookie headers", async () => {
|
||||
const auth = new Authenticator({}, null as any, {
|
||||
jwt: {
|
||||
secret: "secret",
|
||||
fields: [],
|
||||
},
|
||||
cookie: {
|
||||
sameSite: "strict",
|
||||
},
|
||||
});
|
||||
const headers = await auth.getAuthCookieHeader("token");
|
||||
const cookie = headers.get("Set-Cookie");
|
||||
expect(cookie).toStartWith("auth=");
|
||||
expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
||||
|
||||
const { token } = await auth.login("password", { email: "d", password: "test" });
|
||||
expect(token).toBeDefined();
|
||||
// now expect it to be removed
|
||||
const headers2 = await auth.removeAuthCookieHeader(headers);
|
||||
const cookie2 = headers2.get("Set-Cookie");
|
||||
expect(cookie2).toStartWith("auth=; Max-Age=0; Path=/; Expires=");
|
||||
expect(cookie2).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
||||
});
|
||||
|
||||
const { iat, ...decoded } = decodeJwt<any>(token);
|
||||
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
|
||||
expect(await auth.verify(token)).toBe(true);
|
||||
});*/
|
||||
test("should return auth cookie string", async () => {
|
||||
const auth = new Authenticator({}, null as any, {
|
||||
jwt: {
|
||||
secret: "secret",
|
||||
fields: [],
|
||||
},
|
||||
cookie: {
|
||||
sameSite: "strict",
|
||||
},
|
||||
});
|
||||
const cookie = await auth.unsafeGetAuthCookie("token");
|
||||
expect(cookie).toStartWith("auth=");
|
||||
expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,89 +1,243 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Guard } from "../../../src/auth";
|
||||
import { Guard, type GuardConfig } from "auth/authorize/Guard";
|
||||
import { Permission } from "auth/authorize/Permission";
|
||||
import { Role, type RoleSchema } from "auth/authorize/Role";
|
||||
import { objectTransform, s } from "bknd/utils";
|
||||
|
||||
function createGuard(
|
||||
permissionNames: string[],
|
||||
roles?: Record<string, Omit<RoleSchema, "name">>,
|
||||
config?: GuardConfig,
|
||||
) {
|
||||
const _roles = roles
|
||||
? objectTransform(roles, ({ permissions = [], is_default, implicit_allow }, name) => {
|
||||
return Role.create(name, { permissions, is_default, implicit_allow });
|
||||
})
|
||||
: {};
|
||||
const _permissions = permissionNames.map((name) => new Permission(name));
|
||||
return new Guard(_permissions, Object.values(_roles), config);
|
||||
}
|
||||
|
||||
describe("authorize", () => {
|
||||
const read = new Permission("read", {
|
||||
filterable: true,
|
||||
});
|
||||
const write = new Permission("write");
|
||||
|
||||
test("basic", async () => {
|
||||
const guard = Guard.create(
|
||||
const guard = createGuard(
|
||||
["read", "write"],
|
||||
{
|
||||
admin: {
|
||||
permissions: ["read", "write"]
|
||||
}
|
||||
permissions: ["read", "write"],
|
||||
},
|
||||
},
|
||||
{ enabled: true }
|
||||
{ enabled: true },
|
||||
);
|
||||
const user = {
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
guard.setUserContext(user);
|
||||
expect(guard.granted(read, user)).toBeUndefined();
|
||||
expect(guard.granted(write, user)).toBeUndefined();
|
||||
|
||||
expect(guard.granted("read")).toBe(true);
|
||||
expect(guard.granted("write")).toBe(true);
|
||||
|
||||
expect(() => guard.granted("something")).toThrow();
|
||||
expect(() => guard.granted(new Permission("something"), {})).toThrow();
|
||||
});
|
||||
|
||||
test("with default", async () => {
|
||||
const guard = Guard.create(
|
||||
const guard = createGuard(
|
||||
["read", "write"],
|
||||
{
|
||||
admin: {
|
||||
permissions: ["read", "write"]
|
||||
permissions: ["read", "write"],
|
||||
},
|
||||
guest: {
|
||||
permissions: ["read"],
|
||||
is_default: true
|
||||
}
|
||||
is_default: true,
|
||||
},
|
||||
},
|
||||
{ enabled: true }
|
||||
{ enabled: true },
|
||||
);
|
||||
|
||||
expect(guard.granted("read")).toBe(true);
|
||||
expect(guard.granted("write")).toBe(false);
|
||||
expect(guard.granted(read, {})).toBeUndefined();
|
||||
expect(() => guard.granted(write, {})).toThrow();
|
||||
|
||||
const user = {
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
guard.setUserContext(user);
|
||||
|
||||
expect(guard.granted("read")).toBe(true);
|
||||
expect(guard.granted("write")).toBe(true);
|
||||
expect(guard.granted(read, user)).toBeUndefined();
|
||||
expect(guard.granted(write, user)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("guard implicit allow", async () => {
|
||||
const guard = Guard.create([], {}, { enabled: false });
|
||||
const guard = createGuard([], {}, { enabled: false });
|
||||
|
||||
expect(guard.granted("read")).toBe(true);
|
||||
expect(guard.granted("write")).toBe(true);
|
||||
expect(guard.granted(read, {})).toBeUndefined();
|
||||
expect(guard.granted(write, {})).toBeUndefined();
|
||||
});
|
||||
|
||||
test("role implicit allow", async () => {
|
||||
const guard = Guard.create(["read", "write"], {
|
||||
const guard = createGuard(["read", "write"], {
|
||||
admin: {
|
||||
implicit_allow: true
|
||||
}
|
||||
implicit_allow: true,
|
||||
},
|
||||
});
|
||||
|
||||
guard.setUserContext({
|
||||
role: "admin"
|
||||
});
|
||||
const user = {
|
||||
role: "admin",
|
||||
};
|
||||
|
||||
expect(guard.granted("read")).toBe(true);
|
||||
expect(guard.granted("write")).toBe(true);
|
||||
expect(guard.granted(read, user)).toBeUndefined();
|
||||
expect(guard.granted(write, user)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("guard with guest role implicit allow", async () => {
|
||||
const guard = Guard.create(["read", "write"], {
|
||||
const guard = createGuard(["read", "write"], {
|
||||
guest: {
|
||||
implicit_allow: true,
|
||||
is_default: true
|
||||
}
|
||||
is_default: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(guard.getUserRole()?.name).toBe("guest");
|
||||
expect(guard.granted("read")).toBe(true);
|
||||
expect(guard.granted("write")).toBe(true);
|
||||
expect(guard.granted(read, {})).toBeUndefined();
|
||||
expect(guard.granted(write, {})).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("cases", () => {
|
||||
test("guest none, member deny if user.enabled is false", () => {
|
||||
const guard = createGuard(
|
||||
["read"],
|
||||
{
|
||||
guest: {
|
||||
is_default: true,
|
||||
},
|
||||
member: {
|
||||
permissions: [
|
||||
{
|
||||
permission: "read",
|
||||
policies: [
|
||||
{
|
||||
condition: {},
|
||||
effect: "filter",
|
||||
filter: {
|
||||
type: "member",
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: {
|
||||
"user.enabled": false,
|
||||
},
|
||||
effect: "deny",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{ enabled: true },
|
||||
);
|
||||
|
||||
expect(() => guard.granted(read, { role: "guest" })).toThrow();
|
||||
|
||||
// member is allowed, because default role permission effect is allow
|
||||
// and no deny policy is met
|
||||
expect(guard.granted(read, { role: "member" })).toBeUndefined();
|
||||
|
||||
// member is allowed, because deny policy is not met
|
||||
expect(guard.granted(read, { role: "member", enabled: true })).toBeUndefined();
|
||||
|
||||
// member is denied, because deny policy is met
|
||||
expect(() => guard.granted(read, { role: "member", enabled: false })).toThrow();
|
||||
|
||||
// get the filter for member role
|
||||
expect(guard.filters(read, { role: "member" }).filter).toEqual({
|
||||
type: "member",
|
||||
});
|
||||
|
||||
// get filter for guest
|
||||
expect(guard.filters(read, {}).filter).toBeUndefined();
|
||||
});
|
||||
|
||||
test("guest should only read posts that are public", () => {
|
||||
const read = new Permission(
|
||||
"read",
|
||||
{
|
||||
// make this permission filterable
|
||||
// without this, `filter` policies have no effect
|
||||
filterable: true,
|
||||
},
|
||||
// expect the context to match this schema
|
||||
// otherwise exit with 500 to ensure proper policy checking
|
||||
s.object({
|
||||
entity: s.string(),
|
||||
}),
|
||||
);
|
||||
const guard = createGuard(
|
||||
["read"],
|
||||
{
|
||||
guest: {
|
||||
// this permission is applied if no (or invalid) role is provided
|
||||
is_default: true,
|
||||
permissions: [
|
||||
{
|
||||
permission: "read",
|
||||
// effect deny means only having this permission, doesn't guarantee access
|
||||
effect: "deny",
|
||||
policies: [
|
||||
{
|
||||
// only if this condition is met
|
||||
condition: {
|
||||
entity: {
|
||||
$in: ["posts"],
|
||||
},
|
||||
},
|
||||
// the effect is allow
|
||||
effect: "allow",
|
||||
},
|
||||
{
|
||||
condition: {
|
||||
entity: "posts",
|
||||
},
|
||||
effect: "filter",
|
||||
filter: {
|
||||
public: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// members should be allowed to read all
|
||||
member: {
|
||||
permissions: [
|
||||
{
|
||||
permission: "read",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{ enabled: true },
|
||||
);
|
||||
|
||||
// guest can only read posts
|
||||
expect(guard.granted(read, {}, { entity: "posts" })).toBeUndefined();
|
||||
expect(() => guard.granted(read, {}, { entity: "users" })).toThrow();
|
||||
|
||||
// and guests can only read public posts
|
||||
expect(guard.filters(read, {}, { entity: "posts" }).filter).toEqual({
|
||||
public: true,
|
||||
});
|
||||
|
||||
// member can read posts and users
|
||||
expect(guard.granted(read, { role: "member" }, { entity: "posts" })).toBeUndefined();
|
||||
expect(guard.granted(read, { role: "member" }, { entity: "users" })).toBeUndefined();
|
||||
|
||||
// member should not have a filter
|
||||
expect(
|
||||
guard.filters(read, { role: "member" }, { entity: "posts" }).filter,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import type { CreateAppConfig } from "App";
|
||||
import * as proto from "data/prototype";
|
||||
import { mergeObject } from "core/utils/objects";
|
||||
import type { App, DB } from "bknd";
|
||||
import type { CreateUserPayload } from "auth/AppAuth";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(() => enableConsoleLog());
|
||||
|
||||
async function makeApp(config: Partial<CreateAppConfig["config"]> = {}) {
|
||||
const app = createApp({
|
||||
config: mergeObject(
|
||||
{
|
||||
data: proto
|
||||
.em(
|
||||
{
|
||||
users: proto.systemEntity("users", {}),
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
content: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { users, posts, comments }) => {
|
||||
relation(posts).manyToOne(users);
|
||||
relation(comments).manyToOne(posts);
|
||||
},
|
||||
)
|
||||
.toJSON(),
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
secret: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
config,
|
||||
),
|
||||
});
|
||||
await app.build();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
async function createUsers(app: App, users: CreateUserPayload[]) {
|
||||
return Promise.all(
|
||||
users.map(async (user) => {
|
||||
return await app.createUser(user);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadFixtures(app: App, fixtures: Record<string, any[]> = {}) {
|
||||
const results = {} as any;
|
||||
for (const [entity, data] of Object.entries(fixtures)) {
|
||||
results[entity] = await app.em
|
||||
.mutator(entity as any)
|
||||
.insertMany(data)
|
||||
.then((result) => result.data);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
describe("data permissions", async () => {
|
||||
const app = await makeApp({
|
||||
server: {
|
||||
mcp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
guard: {
|
||||
enabled: true,
|
||||
},
|
||||
roles: {
|
||||
guest: {
|
||||
is_default: true,
|
||||
permissions: [
|
||||
{
|
||||
permission: "system.access.api",
|
||||
},
|
||||
{
|
||||
permission: "data.entity.read",
|
||||
policies: [
|
||||
{
|
||||
condition: {
|
||||
entity: "posts",
|
||||
},
|
||||
effect: "filter",
|
||||
filter: {
|
||||
users_id: { $isnull: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
permission: "data.entity.create",
|
||||
policies: [
|
||||
{
|
||||
condition: {
|
||||
entity: "posts",
|
||||
},
|
||||
effect: "filter",
|
||||
filter: {
|
||||
users_id: { $isnull: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
permission: "data.entity.update",
|
||||
policies: [
|
||||
{
|
||||
condition: {
|
||||
entity: "posts",
|
||||
},
|
||||
effect: "filter",
|
||||
filter: {
|
||||
users_id: { $isnull: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
permission: "data.entity.delete",
|
||||
policies: [
|
||||
{
|
||||
condition: { entity: "posts" },
|
||||
},
|
||||
{
|
||||
condition: { entity: "posts" },
|
||||
effect: "filter",
|
||||
filter: {
|
||||
users_id: { $isnull: 1 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const users = [
|
||||
{ email: "foo@example.com", password: "password" },
|
||||
{ email: "bar@example.com", password: "password" },
|
||||
];
|
||||
const fixtures = {
|
||||
posts: [
|
||||
{ content: "post 1", users_id: 1 },
|
||||
{ content: "post 2", users_id: 2 },
|
||||
{ content: "post 3", users_id: null },
|
||||
],
|
||||
comments: [
|
||||
{ content: "comment 1", posts_id: 1 },
|
||||
{ content: "comment 2", posts_id: 2 },
|
||||
{ content: "comment 3", posts_id: 3 },
|
||||
],
|
||||
};
|
||||
await createUsers(app, users);
|
||||
const results = await loadFixtures(app, fixtures);
|
||||
|
||||
describe("http", async () => {
|
||||
it("read many", async () => {
|
||||
// many only includes posts with users_id is null
|
||||
const res = await app.server.request("/api/data/entity/posts");
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(data).toEqual([results.posts[2]]);
|
||||
|
||||
// same with /query
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts/query", {
|
||||
method: "POST",
|
||||
});
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(data).toEqual([results.posts[2]]);
|
||||
}
|
||||
});
|
||||
|
||||
it("read one", async () => {
|
||||
// one only includes posts with users_id is null
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts/1");
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(res.status).toBe(404);
|
||||
expect(data).toBeUndefined();
|
||||
}
|
||||
|
||||
// read one by allowed id
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts/3");
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(res.status).toBe(200);
|
||||
expect(data).toEqual(results.posts[2]);
|
||||
}
|
||||
});
|
||||
|
||||
it("read many by reference", async () => {
|
||||
const res = await app.server.request("/api/data/entity/posts/1/comments");
|
||||
const data = await res.json().then((r: any) => r.data);
|
||||
expect(res.status).toBe(200);
|
||||
expect(data).toEqual(results.comments.filter((c: any) => c.posts_id === 1));
|
||||
});
|
||||
|
||||
it("mutation create one", async () => {
|
||||
// not allowed
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "post 4" }),
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
}
|
||||
// allowed
|
||||
{
|
||||
const res = await app.server.request("/api/data/entity/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "post 4", users_id: null }),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
}
|
||||
});
|
||||
|
||||
it("mutation update one", async () => {
|
||||
// update one: not allowed
|
||||
const res = await app.server.request("/api/data/entity/posts/1", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ content: "post 4" }),
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
|
||||
{
|
||||
// update one: allowed
|
||||
const res = await app.server.request("/api/data/entity/posts/3", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ content: "post 3 (updated)" }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json().then((r: any) => r.data.content)).toBe("post 3 (updated)");
|
||||
}
|
||||
});
|
||||
|
||||
it("mutation update many", async () => {
|
||||
// update many: not allowed
|
||||
const res = await app.server.request("/api/data/entity/posts", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
update: { content: "post 4" },
|
||||
where: { users_id: { $isnull: 0 } },
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200); // because filtered
|
||||
const _data = await res.json().then((r: any) => r.data.map((p: any) => p.users_id));
|
||||
expect(_data.every((u: any) => u === null)).toBe(true);
|
||||
|
||||
// verify
|
||||
const data = await app.em
|
||||
.repo("posts")
|
||||
.findMany({ select: ["content", "users_id"] })
|
||||
.then((r) => r.data);
|
||||
|
||||
// expect non null users_id to not have content "post 4"
|
||||
expect(
|
||||
data.filter((p: any) => p.users_id !== null).every((p: any) => p.content !== "post 4"),
|
||||
).toBe(true);
|
||||
// expect null users_id to have content "post 4"
|
||||
expect(
|
||||
data.filter((p: any) => p.users_id === null).every((p: any) => p.content === "post 4"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
const count = async () => {
|
||||
const {
|
||||
data: { count: _count },
|
||||
} = await app.em.repo("posts").count();
|
||||
return _count;
|
||||
};
|
||||
it("mutation delete one", async () => {
|
||||
const initial = await count();
|
||||
|
||||
// delete one: not allowed
|
||||
const res = await app.server.request("/api/data/entity/posts/1", {
|
||||
method: "DELETE",
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
expect(await count()).toBe(initial);
|
||||
|
||||
{
|
||||
// delete one: allowed
|
||||
const res = await app.server.request("/api/data/entity/posts/3", {
|
||||
method: "DELETE",
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(await count()).toBe(initial - 1);
|
||||
}
|
||||
});
|
||||
|
||||
it("mutation delete many", async () => {
|
||||
// delete many: not allowed
|
||||
const res = await app.server.request("/api/data/entity/posts", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
where: {},
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// only deleted posts with users_id is null
|
||||
const remaining = await app.em
|
||||
.repo("posts")
|
||||
.findMany()
|
||||
.then((r) => r.data);
|
||||
expect(remaining.every((p: any) => p.users_id !== null)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { SystemController } from "modules/server/SystemController";
|
||||
import { createApp } from "core/test/utils";
|
||||
import type { CreateAppConfig } from "App";
|
||||
import { getPermissionRoutes } from "auth/middlewares/permission.middleware";
|
||||
|
||||
async function makeApp(config: Partial<CreateAppConfig> = {}) {
|
||||
const app = createApp(config);
|
||||
await app.build();
|
||||
return app;
|
||||
}
|
||||
|
||||
describe.skip("SystemController", () => {
|
||||
it("...", async () => {
|
||||
const app = await makeApp();
|
||||
const controller = new SystemController(app);
|
||||
const hono = controller.getController();
|
||||
console.log(getPermissionRoutes(hono));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,543 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { s } from "bknd/utils";
|
||||
import { Permission } from "auth/authorize/Permission";
|
||||
import { Policy } from "auth/authorize/Policy";
|
||||
import { Hono } from "hono";
|
||||
import { getPermissionRoutes, permission } from "auth/middlewares/permission.middleware";
|
||||
import { auth } from "auth/middlewares/auth.middleware";
|
||||
import { Guard, mergeFilters, type GuardConfig } from "auth/authorize/Guard";
|
||||
import { Role, RolePermission } from "auth/authorize/Role";
|
||||
import { Exception } from "bknd";
|
||||
import { convert } from "core/object/query/object-query";
|
||||
|
||||
describe("Permission", () => {
|
||||
it("works with minimal schema", () => {
|
||||
expect(() => new Permission("test")).not.toThrow();
|
||||
});
|
||||
|
||||
it("parses context", () => {
|
||||
const p = new Permission(
|
||||
"test3",
|
||||
{
|
||||
filterable: true,
|
||||
},
|
||||
s.object({
|
||||
a: s.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => p.parseContext({ a: [] })).toThrow();
|
||||
expect(p.parseContext({ a: "test" })).toEqual({ a: "test" });
|
||||
// @ts-expect-error
|
||||
expect(p.parseContext({ a: 1 })).toEqual({ a: "1" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Policy", () => {
|
||||
it("works with minimal schema", () => {
|
||||
expect(() => new Policy().toJSON()).not.toThrow();
|
||||
});
|
||||
|
||||
it("checks condition", () => {
|
||||
const p = new Policy({
|
||||
condition: {
|
||||
a: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(p.meetsCondition({ a: 1 })).toBe(true);
|
||||
expect(p.meetsCondition({ a: 2 })).toBe(false);
|
||||
expect(p.meetsCondition({ a: 1, b: 1 })).toBe(true);
|
||||
expect(p.meetsCondition({})).toBe(false);
|
||||
|
||||
const p2 = new Policy({
|
||||
condition: {
|
||||
a: { $gt: 1 },
|
||||
$or: {
|
||||
b: { $lt: 2 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(p2.meetsCondition({ a: 2 })).toBe(true);
|
||||
expect(p2.meetsCondition({ a: 1 })).toBe(false);
|
||||
expect(p2.meetsCondition({ a: 1, b: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("filters", () => {
|
||||
const p = new Policy({
|
||||
filter: {
|
||||
age: { $gt: 18 },
|
||||
},
|
||||
});
|
||||
const subjects = [{ age: 19 }, { age: 17 }, { age: 12 }];
|
||||
|
||||
expect(p.getFiltered(subjects)).toEqual([{ age: 19 }]);
|
||||
|
||||
expect(p.meetsFilter({ age: 19 })).toBe(true);
|
||||
expect(p.meetsFilter({ age: 17 })).toBe(false);
|
||||
expect(p.meetsFilter({ age: 12 })).toBe(false);
|
||||
});
|
||||
|
||||
it("replaces placeholders", () => {
|
||||
const p = new Policy({
|
||||
condition: {
|
||||
a: "@auth.username",
|
||||
},
|
||||
filter: {
|
||||
a: "@auth.username",
|
||||
},
|
||||
});
|
||||
const vars = { auth: { username: "test" } };
|
||||
|
||||
expect(p.meetsCondition({ a: "test" }, vars)).toBe(true);
|
||||
expect(p.meetsCondition({ a: "test2" }, vars)).toBe(false);
|
||||
expect(p.meetsCondition({ a: "test2" })).toBe(false);
|
||||
expect(p.meetsFilter({ a: "test" }, vars)).toBe(true);
|
||||
expect(p.meetsFilter({ a: "test2" }, vars)).toBe(false);
|
||||
expect(p.meetsFilter({ a: "test2" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Guard", () => {
|
||||
it("collects filters", () => {
|
||||
const p = new Permission(
|
||||
"test",
|
||||
{
|
||||
filterable: true,
|
||||
},
|
||||
s.object({
|
||||
a: s.number(),
|
||||
}),
|
||||
);
|
||||
const r = new Role("test", [
|
||||
new RolePermission(p, [
|
||||
new Policy({
|
||||
condition: { a: { $eq: 1 } },
|
||||
filter: { foo: "bar" },
|
||||
effect: "filter",
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
const guard = new Guard([p], [r], {
|
||||
enabled: true,
|
||||
});
|
||||
expect(guard.filters(p, { role: r.name }, { a: 1 }).filter).toEqual({ foo: "bar" });
|
||||
expect(guard.filters(p, { role: r.name }, { a: 2 }).filter).toBeUndefined();
|
||||
// if no user context given, filter cannot be applied
|
||||
expect(guard.filters(p, {}, { a: 1 }).filter).toBeUndefined();
|
||||
});
|
||||
|
||||
it("collects filters for default role", () => {
|
||||
const p = new Permission(
|
||||
"test",
|
||||
{
|
||||
filterable: true,
|
||||
},
|
||||
s.object({
|
||||
a: s.number(),
|
||||
}),
|
||||
);
|
||||
const r = new Role(
|
||||
"test",
|
||||
[
|
||||
new RolePermission(p, [
|
||||
new Policy({
|
||||
condition: { a: { $eq: 1 } },
|
||||
filter: { foo: "bar" },
|
||||
effect: "filter",
|
||||
}),
|
||||
]),
|
||||
],
|
||||
true,
|
||||
);
|
||||
const guard = new Guard([p], [r], {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
guard.filters(
|
||||
p,
|
||||
{
|
||||
role: r.name,
|
||||
},
|
||||
{ a: 1 },
|
||||
).filter,
|
||||
).toEqual({ foo: "bar" });
|
||||
expect(
|
||||
guard.filters(
|
||||
p,
|
||||
{
|
||||
role: r.name,
|
||||
},
|
||||
{ a: 2 },
|
||||
).filter,
|
||||
).toBeUndefined();
|
||||
// if no user context given, the default role is applied
|
||||
// hence it can be found
|
||||
expect(guard.filters(p, {}, { a: 1 }).filter).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("merges filters correctly", () => {
|
||||
expect(mergeFilters({ foo: "bar" }, { baz: "qux" })).toEqual({
|
||||
foo: { $eq: "bar" },
|
||||
baz: { $eq: "qux" },
|
||||
});
|
||||
expect(mergeFilters({ foo: "bar" }, { baz: { $eq: "qux" } })).toEqual({
|
||||
foo: { $eq: "bar" },
|
||||
baz: { $eq: "qux" },
|
||||
});
|
||||
expect(mergeFilters({ foo: "bar" }, { foo: "baz" })).toEqual({ foo: { $eq: "baz" } });
|
||||
|
||||
expect(mergeFilters({ foo: "bar" }, { foo: { $lt: 1 } })).toEqual({
|
||||
foo: { $eq: "bar", $lt: 1 },
|
||||
});
|
||||
|
||||
// overwrite base $or with priority
|
||||
expect(mergeFilters({ $or: { foo: "one" } }, { foo: "bar" })).toEqual({
|
||||
$or: {
|
||||
foo: {
|
||||
$eq: "bar",
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
$eq: "bar",
|
||||
},
|
||||
});
|
||||
|
||||
// ignore base $or if priority has different key
|
||||
expect(mergeFilters({ $or: { other: "one" } }, { foo: "bar" })).toEqual({
|
||||
$or: {
|
||||
other: {
|
||||
$eq: "one",
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
$eq: "bar",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("permission middleware", () => {
|
||||
const makeApp = (
|
||||
permissions: Permission<any, any, any, any>[],
|
||||
roles: Role[] = [],
|
||||
config: Partial<GuardConfig> = {},
|
||||
) => {
|
||||
const app = {
|
||||
module: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
ctx: () => ({
|
||||
guard: new Guard(permissions, roles, {
|
||||
enabled: true,
|
||||
...config,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
};
|
||||
return new Hono()
|
||||
.use(async (c, next) => {
|
||||
// @ts-expect-error
|
||||
c.set("app", app);
|
||||
await next();
|
||||
})
|
||||
.use(auth())
|
||||
.onError((err, c) => {
|
||||
if (err instanceof Exception) {
|
||||
return c.json(err.toJSON(), err.code as any);
|
||||
}
|
||||
return c.json({ error: err.message }, "code" in err ? (err.code as any) : 500);
|
||||
});
|
||||
};
|
||||
|
||||
it("allows if guard is disabled", async () => {
|
||||
const p = new Permission("test");
|
||||
const hono = makeApp([p], [], { enabled: false }).get("/test", permission(p, {}), async (c) =>
|
||||
c.text("test"),
|
||||
);
|
||||
|
||||
const res = await hono.request("/test");
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe("test");
|
||||
});
|
||||
|
||||
it("denies if guard is enabled", async () => {
|
||||
const p = new Permission("test");
|
||||
const hono = makeApp([p]).get("/test", permission(p, {}), async (c) => c.text("test"));
|
||||
|
||||
const res = await hono.request("/test");
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it("allows if user has (plain) role", async () => {
|
||||
const p = new Permission("test");
|
||||
const r = Role.create("test", { permissions: [p.name] });
|
||||
const hono = makeApp([p], [r])
|
||||
.use(async (c, next) => {
|
||||
// @ts-expect-error
|
||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
||||
await next();
|
||||
})
|
||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
||||
|
||||
const res = await hono.request("/test");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("allows if user has role with policy", async () => {
|
||||
const p = new Permission("test");
|
||||
const r = new Role("test", [
|
||||
new RolePermission(p, [
|
||||
new Policy({
|
||||
condition: {
|
||||
a: { $gte: 1 },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
const hono = makeApp([p], [r], {
|
||||
context: {
|
||||
a: 1,
|
||||
},
|
||||
})
|
||||
.use(async (c, next) => {
|
||||
// @ts-expect-error
|
||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
||||
await next();
|
||||
})
|
||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
||||
|
||||
const res = await hono.request("/test");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("denies if user with role doesn't meet condition", async () => {
|
||||
const p = new Permission("test");
|
||||
const r = new Role("test", [
|
||||
new RolePermission(
|
||||
p,
|
||||
[
|
||||
new Policy({
|
||||
condition: {
|
||||
a: { $lt: 1 },
|
||||
},
|
||||
// default effect is allow
|
||||
}),
|
||||
],
|
||||
// change default effect to deny if no condition is met
|
||||
"deny",
|
||||
),
|
||||
]);
|
||||
const hono = makeApp([p], [r], {
|
||||
context: {
|
||||
a: 1,
|
||||
},
|
||||
})
|
||||
.use(async (c, next) => {
|
||||
// @ts-expect-error
|
||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
||||
await next();
|
||||
})
|
||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
||||
|
||||
const res = await hono.request("/test");
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it("allows if user with role doesn't meet condition (from middleware)", async () => {
|
||||
const p = new Permission(
|
||||
"test",
|
||||
{},
|
||||
s.object({
|
||||
a: s.number(),
|
||||
}),
|
||||
);
|
||||
const r = new Role("test", [
|
||||
new RolePermission(p, [
|
||||
new Policy({
|
||||
condition: {
|
||||
a: { $eq: 1 },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
const hono = makeApp([p], [r])
|
||||
.use(async (c, next) => {
|
||||
// @ts-expect-error
|
||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
||||
await next();
|
||||
})
|
||||
.get(
|
||||
"/test",
|
||||
permission(p, {
|
||||
context: (c) => ({
|
||||
a: 1,
|
||||
}),
|
||||
}),
|
||||
async (c) => c.text("test"),
|
||||
);
|
||||
|
||||
const res = await hono.request("/test");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("throws if permission context is invalid", async () => {
|
||||
const p = new Permission(
|
||||
"test",
|
||||
{},
|
||||
s.object({
|
||||
a: s.number({ minimum: 2 }),
|
||||
}),
|
||||
);
|
||||
const r = new Role("test", [
|
||||
new RolePermission(p, [
|
||||
new Policy({
|
||||
condition: {
|
||||
a: { $eq: 1 },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
const hono = makeApp([p], [r])
|
||||
.use(async (c, next) => {
|
||||
// @ts-expect-error
|
||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
||||
await next();
|
||||
})
|
||||
.get(
|
||||
"/test",
|
||||
permission(p, {
|
||||
context: (c) => ({
|
||||
a: 1,
|
||||
}),
|
||||
}),
|
||||
async (c) => c.text("test"),
|
||||
);
|
||||
|
||||
const res = await hono.request("/test");
|
||||
// expecting 500 because bknd should have handled it correctly
|
||||
expect(res.status).toBe(500);
|
||||
});
|
||||
|
||||
it("checks context on routes with permissions", async () => {
|
||||
const make = (user: any) => {
|
||||
const p = new Permission(
|
||||
"test",
|
||||
{},
|
||||
s.object({
|
||||
a: s.number(),
|
||||
}),
|
||||
);
|
||||
const r = new Role("test", [
|
||||
new RolePermission(p, [
|
||||
new Policy({
|
||||
condition: {
|
||||
a: { $eq: 1 },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
return makeApp([p], [r])
|
||||
.use(async (c, next) => {
|
||||
// @ts-expect-error
|
||||
c.set("auth", { registered: true, user });
|
||||
await next();
|
||||
})
|
||||
.get(
|
||||
"/valid",
|
||||
permission(p, {
|
||||
context: (c) => ({
|
||||
a: 1,
|
||||
}),
|
||||
}),
|
||||
async (c) => c.text("test"),
|
||||
)
|
||||
.get(
|
||||
"/invalid",
|
||||
permission(p, {
|
||||
// @ts-expect-error
|
||||
context: (c) => ({
|
||||
b: "1",
|
||||
}),
|
||||
}),
|
||||
async (c) => c.text("test"),
|
||||
)
|
||||
.get(
|
||||
"/invalid2",
|
||||
permission(p, {
|
||||
// @ts-expect-error
|
||||
context: (c) => ({}),
|
||||
}),
|
||||
async (c) => c.text("test"),
|
||||
)
|
||||
.get(
|
||||
"/invalid3",
|
||||
// @ts-expect-error
|
||||
permission(p),
|
||||
async (c) => c.text("test"),
|
||||
);
|
||||
};
|
||||
|
||||
const hono = make({ id: 0, role: "test" });
|
||||
const valid = await hono.request("/valid");
|
||||
expect(valid.status).toBe(200);
|
||||
const invalid = await hono.request("/invalid");
|
||||
expect(invalid.status).toBe(500);
|
||||
const invalid2 = await hono.request("/invalid2");
|
||||
expect(invalid2.status).toBe(500);
|
||||
const invalid3 = await hono.request("/invalid3");
|
||||
expect(invalid3.status).toBe(500);
|
||||
|
||||
{
|
||||
const hono = make(null);
|
||||
const valid = await hono.request("/valid");
|
||||
expect(valid.status).toBe(403);
|
||||
const invalid = await hono.request("/invalid");
|
||||
expect(invalid.status).toBe(500);
|
||||
const invalid2 = await hono.request("/invalid2");
|
||||
expect(invalid2.status).toBe(500);
|
||||
const invalid3 = await hono.request("/invalid3");
|
||||
expect(invalid3.status).toBe(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Role", () => {
|
||||
it("serializes and deserializes", () => {
|
||||
const p = new Permission(
|
||||
"test",
|
||||
{
|
||||
filterable: true,
|
||||
},
|
||||
s.object({
|
||||
a: s.number({ minimum: 2 }),
|
||||
}),
|
||||
);
|
||||
const r = new Role(
|
||||
"test",
|
||||
[
|
||||
new RolePermission(p, [
|
||||
new Policy({
|
||||
condition: {
|
||||
a: { $eq: 1 },
|
||||
},
|
||||
effect: "deny",
|
||||
filter: {
|
||||
b: { $lt: 1 },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
],
|
||||
true,
|
||||
);
|
||||
const json = JSON.parse(JSON.stringify(r.toJSON()));
|
||||
const r2 = Role.create(p.name, json);
|
||||
expect(r2.toJSON()).toEqual(r.toJSON());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
@@ -3,14 +3,16 @@ import { OAuthStrategy } from "../../../src/auth/authenticate/strategies";
|
||||
|
||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||
|
||||
// @todo: add mock response
|
||||
describe("OAuthStrategy", async () => {
|
||||
const strategy = new OAuthStrategy({
|
||||
return;
|
||||
/*const strategy = new OAuthStrategy({
|
||||
type: "oidc",
|
||||
client: {
|
||||
client_id: process.env.OAUTH_CLIENT_ID,
|
||||
client_secret: process.env.OAUTH_CLIENT_SECRET
|
||||
client_id: process.env.OAUTH_CLIENT_ID!,
|
||||
client_secret: process.env.OAUTH_CLIENT_SECRET!,
|
||||
},
|
||||
name: "google"
|
||||
name: "google",
|
||||
});
|
||||
const state = "---";
|
||||
const redirect_uri = "http://localhost:3000/auth/google/callback";
|
||||
@@ -19,28 +21,29 @@ describe("OAuthStrategy", async () => {
|
||||
const config = await strategy.getConfig();
|
||||
console.log("config", JSON.stringify(config, null, 2));
|
||||
|
||||
const request = await strategy.request({
|
||||
redirect_uri,
|
||||
state
|
||||
});
|
||||
|
||||
const server = Bun.serve({
|
||||
fetch: async (req) => {
|
||||
console.log("req", req.method, req.url);
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/auth/google/callback") {
|
||||
console.log("req", req);
|
||||
const user = await strategy.callback(url, {
|
||||
redirect_uri,
|
||||
state
|
||||
state,
|
||||
});
|
||||
|
||||
console.log("---user", user);
|
||||
}
|
||||
return new Response("Bun!");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const request = await strategy.request({
|
||||
redirect_uri,
|
||||
state,
|
||||
});
|
||||
console.log("request", request);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100000));
|
||||
});
|
||||
});*/
|
||||
});
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { Endpoint } from "../../src/core";
|
||||
import { mockFetch2, unmockFetch } from "./helper";
|
||||
|
||||
const testC: any = {
|
||||
json: (res: any) => Response.json(res)
|
||||
};
|
||||
const testNext = async () => {};
|
||||
|
||||
describe("Endpoint", async () => {
|
||||
it("behaves as expected", async () => {
|
||||
const endpoint = new Endpoint("GET", "/test", async () => {
|
||||
return { hello: "test" };
|
||||
});
|
||||
|
||||
expect(endpoint.method).toBe("GET");
|
||||
expect(endpoint.path).toBe("/test");
|
||||
|
||||
const handler = endpoint.toHandler();
|
||||
const response = await handler(testC, testNext);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(await response.json()).toEqual({ hello: "test" });
|
||||
});
|
||||
|
||||
it("can be $request(ed)", async () => {
|
||||
const obj = { hello: "test" };
|
||||
const baseUrl = "https://local.com:123";
|
||||
const endpoint = Endpoint.get("/test", async () => obj);
|
||||
|
||||
mockFetch2(async (input: RequestInfo, init: RequestInit) => {
|
||||
expect(input).toBe(`${baseUrl}/test`);
|
||||
return new Response(JSON.stringify(obj), { status: 200 });
|
||||
});
|
||||
const response = await endpoint.$request({}, baseUrl);
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
ok: true,
|
||||
response: obj
|
||||
});
|
||||
unmockFetch();
|
||||
});
|
||||
|
||||
it("resolves helper functions", async () => {
|
||||
const params = ["/test", () => ({ hello: "test" })];
|
||||
|
||||
["get", "post", "patch", "put", "delete"].forEach((method) => {
|
||||
const endpoint = Endpoint[method](...params);
|
||||
expect(endpoint.method).toBe(method.toUpperCase());
|
||||
expect(endpoint.path).toBe(params[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Event, EventManager, NoParamEvent } from "../../src/core/events";
|
||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
class SpecialEvent extends Event<{ foo: string }> {
|
||||
static slug = "special-event";
|
||||
static override slug = "special-event";
|
||||
|
||||
isBar() {
|
||||
return this.params.foo === "bar";
|
||||
@@ -10,37 +14,142 @@ class SpecialEvent extends Event<{ foo: string }> {
|
||||
}
|
||||
|
||||
class InformationalEvent extends NoParamEvent {
|
||||
static slug = "informational-event";
|
||||
static override slug = "informational-event";
|
||||
}
|
||||
|
||||
class ReturnEvent extends Event<{ foo: string }, string> {
|
||||
static override slug = "return-event";
|
||||
|
||||
override validate(value: string) {
|
||||
if (typeof value !== "string") {
|
||||
throw new InvalidEventReturn("string", typeof value);
|
||||
}
|
||||
|
||||
return this.clone({
|
||||
foo: [this.params.foo, value].join("-"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe("EventManager", async () => {
|
||||
test("test", async () => {
|
||||
test("executes", async () => {
|
||||
const call = mock(() => null);
|
||||
const delayed = mock(() => null);
|
||||
|
||||
const emgr = new EventManager();
|
||||
emgr.registerEvents([SpecialEvent, InformationalEvent]);
|
||||
|
||||
expect(emgr.eventExists("special-event")).toBe(true);
|
||||
expect(emgr.eventExists("informational-event")).toBe(true);
|
||||
expect(emgr.eventExists("unknown-event")).toBe(false);
|
||||
|
||||
emgr.onEvent(
|
||||
SpecialEvent,
|
||||
async (event, name) => {
|
||||
console.log("Event: ", name, event.params.foo, event.isBar());
|
||||
console.log("wait...");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
console.log("done waiting");
|
||||
expect(name).toBe("special-event");
|
||||
expect(event.isBar()).toBe(true);
|
||||
call();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
delayed();
|
||||
},
|
||||
"sync"
|
||||
"sync",
|
||||
);
|
||||
|
||||
// don't allow unknown
|
||||
expect(() => emgr.on("unknown", () => void 0)).toThrow();
|
||||
|
||||
emgr.onEvent(InformationalEvent, async (event, name) => {
|
||||
console.log("Event: ", name, event.params);
|
||||
call();
|
||||
expect(name).toBe("informational-event");
|
||||
});
|
||||
|
||||
await emgr.emit(new SpecialEvent({ foo: "bar" }));
|
||||
console.log("done");
|
||||
await emgr.emit(new InformationalEvent());
|
||||
|
||||
// expect construct signatures to not cause ts errors
|
||||
new SpecialEvent({ foo: "bar" });
|
||||
new InformationalEvent();
|
||||
|
||||
expect(true).toBe(true);
|
||||
// execute asyncs
|
||||
await emgr.executeAsyncs();
|
||||
|
||||
expect(call).toHaveBeenCalledTimes(2);
|
||||
expect(delayed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("custom async executor", async () => {
|
||||
const call = mock(() => null);
|
||||
const asyncExecutor = (p: Promise<any>[]) => {
|
||||
call();
|
||||
return Promise.all(p);
|
||||
};
|
||||
const emgr = new EventManager({ InformationalEvent });
|
||||
|
||||
emgr.onEvent(InformationalEvent, async () => {});
|
||||
await emgr.emit(new InformationalEvent());
|
||||
await emgr.executeAsyncs(asyncExecutor);
|
||||
expect(call).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("piping", async () => {
|
||||
const onInvalidReturn = mock(() => null);
|
||||
const asyncEventCallback = mock(() => null);
|
||||
const emgr = new EventManager(
|
||||
{ ReturnEvent, InformationalEvent },
|
||||
{
|
||||
onInvalidReturn,
|
||||
},
|
||||
);
|
||||
|
||||
// @ts-expect-error InformationalEvent has no return value
|
||||
emgr.onEvent(InformationalEvent, async () => {
|
||||
asyncEventCallback();
|
||||
return 1;
|
||||
});
|
||||
|
||||
emgr.onEvent(ReturnEvent, async () => "1", "sync");
|
||||
emgr.onEvent(ReturnEvent, async () => "0", "sync");
|
||||
|
||||
// @todo: fix this
|
||||
// @ts-expect-error must be string
|
||||
emgr.onEvent(ReturnEvent, async () => 0, "sync");
|
||||
|
||||
// return is not required
|
||||
emgr.onEvent(ReturnEvent, async () => {}, "sync");
|
||||
|
||||
// was "async", will not return
|
||||
const e1 = await emgr.emit(new InformationalEvent());
|
||||
expect(e1.returned).toBe(false);
|
||||
|
||||
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
||||
expect(e2.returned).toBe(true);
|
||||
expect(e2.params.foo).toBe("bar-1-0");
|
||||
|
||||
await emgr.executeAsyncs();
|
||||
|
||||
expect(onInvalidReturn).toHaveBeenCalled();
|
||||
expect(asyncEventCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("once", async () => {
|
||||
const call = mock(() => null);
|
||||
const emgr = new EventManager({ InformationalEvent });
|
||||
|
||||
emgr.onEvent(
|
||||
InformationalEvent,
|
||||
async (event, slug) => {
|
||||
expect(event).toBeInstanceOf(InformationalEvent);
|
||||
expect(slug).toBe("informational-event");
|
||||
call();
|
||||
},
|
||||
{ mode: "sync", once: true },
|
||||
);
|
||||
|
||||
expect(emgr.getListeners().length).toBe(1);
|
||||
await emgr.emit(new InformationalEvent());
|
||||
expect(emgr.getListeners().length).toBe(0);
|
||||
await emgr.emit(new InformationalEvent());
|
||||
expect(emgr.getListeners().length).toBe(0);
|
||||
expect(call).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, test } from "bun:test";
|
||||
import type { TObject, TString } from "@sinclair/typebox";
|
||||
import { Registry } from "../../src/core/registry/Registry";
|
||||
import { type TSchema, Type } from "../../src/core/utils";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Registry } from "core/registry/Registry";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
@@ -11,13 +10,16 @@ class What {
|
||||
method() {
|
||||
return null;
|
||||
}
|
||||
getType() {
|
||||
return s.object({ type: s.string() });
|
||||
}
|
||||
}
|
||||
class What2 extends What {}
|
||||
class NotAllowed {}
|
||||
|
||||
type Test1 = {
|
||||
cls: new (...args: any[]) => What;
|
||||
schema: TObject<{ type: TString }>;
|
||||
schema: s.ObjectSchema<{ type: s.StringSchema }>;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
@@ -26,31 +28,61 @@ describe("Registry", () => {
|
||||
const registry = new Registry<Test1>().set({
|
||||
first: {
|
||||
cls: What,
|
||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
||||
enabled: true
|
||||
}
|
||||
schema: s.object({ type: s.string(), what: s.string() }),
|
||||
enabled: true,
|
||||
},
|
||||
} satisfies Record<string, Test1>);
|
||||
|
||||
const item = registry.get("first");
|
||||
expect(item).toBeDefined();
|
||||
expect(item?.cls).toBe(What);
|
||||
|
||||
const second = s.object({ type: s.string(), what: s.string() });
|
||||
registry.add("second", {
|
||||
cls: What2,
|
||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
||||
enabled: true
|
||||
schema: second,
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(registry.get("second").schema).toEqual(second);
|
||||
|
||||
const third = s.object({ type: s.string({ default: "1" }), what22: s.string() });
|
||||
registry.add("third", {
|
||||
// @ts-expect-error
|
||||
cls: NotAllowed,
|
||||
schema: Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() }),
|
||||
enabled: true
|
||||
schema: third,
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(registry.get("third").schema).toEqual(third);
|
||||
|
||||
const fourth = s.object({ type: s.number(), what22: s.string() });
|
||||
registry.add("fourth", {
|
||||
cls: What,
|
||||
// @ts-expect-error
|
||||
schema: Type.Object({ type: Type.Number(), what22: Type.String() }),
|
||||
enabled: true
|
||||
schema: fourth,
|
||||
enabled: true,
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(registry.get("fourth").schema).toEqual(fourth);
|
||||
|
||||
expect(Object.keys(registry.all()).length).toBe(4);
|
||||
});
|
||||
|
||||
test("uses registration fn", async () => {
|
||||
const registry = new Registry<Test1>((a: ClassRef<What>) => {
|
||||
return {
|
||||
cls: a,
|
||||
schema: a.prototype.getType(),
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
|
||||
console.log("list", registry.all());
|
||||
registry.register("what2", What2);
|
||||
expect(registry.get("what2")).toBeDefined();
|
||||
expect(registry.get("what2").cls).toBe(What2);
|
||||
expect(JSON.stringify(registry.get("what2").schema)).toEqual(
|
||||
JSON.stringify(What2.prototype.getType()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import * as assert from "node:assert/strict";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { after, beforeEach, describe, test } from "node:test";
|
||||
import { Miniflare } from "miniflare";
|
||||
import {
|
||||
CloudflareKVCacheItem,
|
||||
CloudflareKVCachePool
|
||||
} from "../../../src/core/cache/adapters/CloudflareKvCache";
|
||||
import { runTests } from "./cache-test-suite";
|
||||
|
||||
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
|
||||
console.log = async (message: any) => {
|
||||
const tty = createWriteStream("/dev/tty");
|
||||
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
|
||||
return tty.write(`${msg}\n`);
|
||||
};
|
||||
|
||||
describe("CloudflareKv", async () => {
|
||||
let mf: Miniflare;
|
||||
runTests({
|
||||
createCache: async () => {
|
||||
if (mf) {
|
||||
await mf.dispose();
|
||||
}
|
||||
|
||||
mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
kvNamespaces: ["TEST"]
|
||||
});
|
||||
const kv = await mf.getKVNamespace("TEST");
|
||||
return new CloudflareKVCachePool(kv as any);
|
||||
},
|
||||
createItem: (key, value) => new CloudflareKVCacheItem(key, value),
|
||||
tester: {
|
||||
test,
|
||||
beforeEach,
|
||||
expect: (actual?: any) => {
|
||||
return {
|
||||
toBe(expected: any) {
|
||||
assert.equal(actual, expected);
|
||||
},
|
||||
toEqual(expected: any) {
|
||||
assert.deepEqual(actual, expected);
|
||||
},
|
||||
toBeUndefined() {
|
||||
assert.equal(actual, undefined);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await mf?.dispose();
|
||||
});
|
||||
});
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { MemoryCache, MemoryCacheItem } from "../../../src/core/cache/adapters/MemoryCache";
|
||||
import { runTests } from "./cache-test-suite";
|
||||
|
||||
describe("MemoryCache", () => {
|
||||
runTests({
|
||||
createCache: async () => new MemoryCache(),
|
||||
createItem: (key, value) => new MemoryCacheItem(key, value),
|
||||
tester: {
|
||||
test,
|
||||
beforeEach,
|
||||
expect
|
||||
}
|
||||
});
|
||||
});
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
//import { beforeEach as bunBeforeEach, expect as bunExpect, test as bunTest } from "bun:test";
|
||||
import type { ICacheItem, ICachePool } from "../../../src/core/cache/cache-interface";
|
||||
|
||||
export type TestOptions = {
|
||||
createCache: () => Promise<ICachePool>;
|
||||
createItem: (key: string, value: any) => ICacheItem;
|
||||
tester: {
|
||||
test: (name: string, fn: () => Promise<void>) => void;
|
||||
beforeEach: (fn: () => Promise<void>) => void;
|
||||
expect: (actual?: any) => {
|
||||
toBe(expected: any): void;
|
||||
toEqual(expected: any): void;
|
||||
toBeUndefined(): void;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export function runTests({ createCache, createItem, tester }: TestOptions) {
|
||||
let cache: ICachePool<string>;
|
||||
const { test, beforeEach, expect } = tester;
|
||||
|
||||
beforeEach(async () => {
|
||||
cache = await createCache();
|
||||
});
|
||||
|
||||
test("getItem returns correct item", async () => {
|
||||
const item = createItem("key1", "value1");
|
||||
await cache.save(item);
|
||||
const retrievedItem = await cache.get("key1");
|
||||
expect(retrievedItem.value()).toEqual(item.value());
|
||||
});
|
||||
|
||||
test("getItem returns new item when key does not exist", async () => {
|
||||
const retrievedItem = await cache.get("key1");
|
||||
expect(retrievedItem.key()).toEqual("key1");
|
||||
expect(retrievedItem.value()).toBeUndefined();
|
||||
});
|
||||
|
||||
test("getItems returns correct items", async () => {
|
||||
const item1 = createItem("key1", "value1");
|
||||
const item2 = createItem("key2", "value2");
|
||||
await cache.save(item1);
|
||||
await cache.save(item2);
|
||||
const retrievedItems = await cache.getMany(["key1", "key2"]);
|
||||
expect(retrievedItems.get("key1")?.value()).toEqual(item1.value());
|
||||
expect(retrievedItems.get("key2")?.value()).toEqual(item2.value());
|
||||
});
|
||||
|
||||
test("hasItem returns true when item exists and is a hit", async () => {
|
||||
const item = createItem("key1", "value1");
|
||||
await cache.save(item);
|
||||
expect(await cache.has("key1")).toBe(true);
|
||||
});
|
||||
|
||||
test("clear and deleteItem correctly clear the cache and delete items", async () => {
|
||||
const item = createItem("key1", "value1");
|
||||
await cache.save(item);
|
||||
|
||||
if (cache.supports().clear) {
|
||||
await cache.clear();
|
||||
} else {
|
||||
await cache.delete("key1");
|
||||
}
|
||||
|
||||
expect(await cache.has("key1")).toBe(false);
|
||||
});
|
||||
|
||||
test("save correctly saves items to the cache", async () => {
|
||||
const item = createItem("key1", "value1");
|
||||
await cache.save(item);
|
||||
expect(await cache.has("key1")).toBe(true);
|
||||
});
|
||||
|
||||
test("putItem correctly puts items in the cache ", async () => {
|
||||
await cache.put("key1", "value1", { ttl: 60 });
|
||||
const item = await cache.get("key1");
|
||||
expect(item.value()).toEqual("value1");
|
||||
expect(item.hit()).toBe(true);
|
||||
});
|
||||
|
||||
/*test("commit returns true", async () => {
|
||||
expect(await cache.commit()).toBe(true);
|
||||
});*/
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import { describe, test } from "bun:test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { checksum, hash } from "../../src/core/utils";
|
||||
|
||||
describe("crypto", async () => {
|
||||
test("sha256", async () => {
|
||||
console.log(await hash.sha256("test"));
|
||||
expect(await hash.sha256("test")).toBe(
|
||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||
);
|
||||
});
|
||||
test("sha1", async () => {
|
||||
console.log(await hash.sha1("test"));
|
||||
expect(await hash.sha1("test")).toBe("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3");
|
||||
});
|
||||
test("checksum", async () => {
|
||||
console.log(checksum("hello world"));
|
||||
expect(await checksum("hello world")).toBe("2aae6c35c94fcfb415dbe95f408b9ce91ee846ed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { env, is_toggled } from "core/env";
|
||||
|
||||
describe("env", () => {
|
||||
test("is_toggled", () => {
|
||||
expect(is_toggled("true")).toBe(true);
|
||||
expect(is_toggled("1")).toBe(true);
|
||||
expect(is_toggled("false")).toBe(false);
|
||||
expect(is_toggled("0")).toBe(false);
|
||||
expect(is_toggled(true)).toBe(true);
|
||||
expect(is_toggled(false)).toBe(false);
|
||||
expect(is_toggled(undefined)).toBe(false);
|
||||
expect(is_toggled(null)).toBe(false);
|
||||
expect(is_toggled(1)).toBe(true);
|
||||
expect(is_toggled(0)).toBe(false);
|
||||
expect(is_toggled("anything else")).toBe(false);
|
||||
expect(is_toggled(undefined, true)).toBe(true);
|
||||
});
|
||||
|
||||
test("env()", () => {
|
||||
expect(env("cli_log_level", undefined, { source: {} })).toBeUndefined();
|
||||
expect(env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "log" } })).toBe(
|
||||
"log" as any,
|
||||
);
|
||||
expect(env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "LOG" } })).toBe(
|
||||
"log" as any,
|
||||
);
|
||||
expect(
|
||||
env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "asdf" } }),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(env("modules_debug", undefined, { source: {} })).toBeFalse();
|
||||
expect(env("modules_debug", undefined, { source: { BKND_MODULES_DEBUG: "1" } })).toBeTrue();
|
||||
expect(env("modules_debug", undefined, { source: { BKND_MODULES_DEBUG: "0" } })).toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { SchemaObject } from "../../../src/core";
|
||||
import { Type } from "../../../src/core/utils";
|
||||
import { s } from "core/utils/schema";
|
||||
import { SchemaObject } from "core/object/SchemaObject";
|
||||
|
||||
describe("SchemaObject", async () => {
|
||||
test("basic", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({ a: Type.String({ default: "b" }) }),
|
||||
s.strictObject({ a: s.string({ default: "b" }) }),
|
||||
{ a: "test" },
|
||||
{
|
||||
forceParse: true
|
||||
}
|
||||
forceParse: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(m.get()).toEqual({ a: "test" });
|
||||
@@ -23,28 +23,28 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("patch", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.strictObject({
|
||||
s: s.strictObject(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.strictObject(
|
||||
{
|
||||
c: Type.String({ default: "d" }),
|
||||
e: Type.String({ default: "f" })
|
||||
c: s.string({ default: "d" }),
|
||||
e: s.string({ default: "f" }),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {}, additionalProperties: false }
|
||||
)
|
||||
})
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
|
||||
|
||||
await m.patch("s.a", "c");
|
||||
|
||||
// non-existing path on no additional properties
|
||||
expect(() => m.patch("s.s.s", "c")).toThrow();
|
||||
expect(m.patch("s.s.s", "c")).rejects.toThrow();
|
||||
// wrong type
|
||||
expect(() => m.patch("s.a", 1)).toThrow();
|
||||
|
||||
@@ -58,9 +58,9 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("patch array", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
})
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||
|
||||
@@ -75,20 +75,20 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("remove", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.object({
|
||||
s: s.object(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.object(
|
||||
{
|
||||
c: Type.String({ default: "d" })
|
||||
c: s.string({ default: "d" }),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
})
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
|
||||
|
||||
@@ -107,9 +107,9 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("set", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
})
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||
|
||||
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
|
||||
let called = false;
|
||||
let result: any;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
called = true;
|
||||
result = config;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await m.set({ methods: ["GET", "POST"] });
|
||||
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
|
||||
test("listener: onBeforeUpdate", async () => {
|
||||
let called = false;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
|
||||
called = true;
|
||||
to.methods.push("OPTIONS");
|
||||
return to;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const result = await m.set({ methods: ["GET", "POST"] });
|
||||
@@ -167,8 +167,8 @@ describe("SchemaObject", async () => {
|
||||
});
|
||||
|
||||
test("throwIfRestricted", async () => {
|
||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
||||
restrictPaths: ["a.b"]
|
||||
const m = new SchemaObject(s.strictObject({}), undefined, {
|
||||
restrictPaths: ["a.b"],
|
||||
});
|
||||
|
||||
expect(() => m.throwIfRestricted("a.b")).toThrow();
|
||||
@@ -179,24 +179,24 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("restriction bypass", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.strictObject({
|
||||
s: s.strictObject(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.strictObject(
|
||||
{
|
||||
c: Type.String({ default: "d" })
|
||||
c: s.string({ default: "d" }),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {} }
|
||||
)
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
restrictPaths: ["s.b"]
|
||||
}
|
||||
restrictPaths: ["s.b"],
|
||||
},
|
||||
);
|
||||
|
||||
expect(m.patch("s.b.c", "e")).rejects.toThrow();
|
||||
@@ -205,7 +205,21 @@ describe("SchemaObject", async () => {
|
||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
|
||||
});
|
||||
|
||||
const dataEntitiesSchema = Type.Object(
|
||||
const dataEntitiesSchema = s.strictObject({
|
||||
entities: s.record(
|
||||
s.object({
|
||||
fields: s.record(
|
||||
s.object({
|
||||
type: s.string(),
|
||||
config: s.object({}).optional(),
|
||||
}),
|
||||
),
|
||||
config: s.record(s.string()).optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
/* const dataEntitiesSchema = Type.Object(
|
||||
{
|
||||
entities: Type.Object(
|
||||
{},
|
||||
@@ -217,33 +231,33 @@ describe("SchemaObject", async () => {
|
||||
additionalProperties: Type.Object({
|
||||
type: Type.String(),
|
||||
config: Type.Optional(
|
||||
Type.Object({}, { additionalProperties: Type.String() })
|
||||
)
|
||||
})
|
||||
}
|
||||
Type.Object({}, { additionalProperties: Type.String() }),
|
||||
),
|
||||
}),
|
||||
},
|
||||
),
|
||||
config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() }))
|
||||
})
|
||||
}
|
||||
)
|
||||
config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() })),
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
additionalProperties: false
|
||||
}
|
||||
);
|
||||
additionalProperties: false,
|
||||
},
|
||||
); */
|
||||
test("patch safe object, overwrite", async () => {
|
||||
const data = {
|
||||
entities: {
|
||||
some: {
|
||||
fields: {
|
||||
a: { type: "string", config: { some: "thing" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
a: { type: "string", config: { some: "thing" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config/],
|
||||
});
|
||||
|
||||
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
|
||||
@@ -252,10 +266,10 @@ describe("SchemaObject", async () => {
|
||||
entities: {
|
||||
some: {
|
||||
fields: {
|
||||
a: { type: "string", config: { another: "one" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
a: { type: "string", config: { another: "one" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,22 +279,22 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
|
||||
});
|
||||
|
||||
await m.patch("entities.test", {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(m.get()).toEqual({
|
||||
@@ -288,17 +302,17 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
test: {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,14 +322,14 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||
forceParse: true,
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
|
||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
|
||||
});
|
||||
|
||||
expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow();
|
||||
@@ -323,13 +337,13 @@ describe("SchemaObject", async () => {
|
||||
await m.patch("entities.test", {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await m.patch("entities.users.config", {
|
||||
sort_dir: "desc"
|
||||
sort_dir: "desc",
|
||||
});
|
||||
|
||||
expect(m.get()).toEqual({
|
||||
@@ -337,20 +351,20 @@ describe("SchemaObject", async () => {
|
||||
users: {
|
||||
fields: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" }
|
||||
password: { type: "string" },
|
||||
},
|
||||
config: {
|
||||
sort_dir: "desc"
|
||||
}
|
||||
sort_dir: "desc",
|
||||
},
|
||||
},
|
||||
test: {
|
||||
fields: {
|
||||
content: {
|
||||
type: "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { apply, diff, revert } from "../../../src/core/object/diff";
|
||||
|
||||
describe("diff", () => {
|
||||
it("should detect added properties", () => {
|
||||
const oldObj = { a: 1 };
|
||||
const newObj = { a: 1, b: 2 };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "a",
|
||||
p: ["b"],
|
||||
o: undefined,
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should detect removed properties", () => {
|
||||
const oldObj = { a: 1, b: 2 };
|
||||
const newObj = { a: 1 };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "r",
|
||||
p: ["b"],
|
||||
o: 2,
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should detect edited properties", () => {
|
||||
const oldObj = { a: 1 };
|
||||
const newObj = { a: 2 };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should detect changes in nested objects", () => {
|
||||
const oldObj = { a: { b: 1 } };
|
||||
const newObj = { a: { b: 2 } };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a", "b"],
|
||||
o: 1,
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should detect changes in arrays", () => {
|
||||
const oldObj = { a: [1, 2, 3] };
|
||||
const newObj = { a: [1, 4, 3, 5] };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a", 1],
|
||||
o: 2,
|
||||
n: 4,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 3],
|
||||
o: undefined,
|
||||
n: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle adding elements to an empty array", () => {
|
||||
const oldObj = { a: [] };
|
||||
const newObj = { a: [1, 2, 3] };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 0],
|
||||
o: undefined,
|
||||
n: 1,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 1],
|
||||
o: undefined,
|
||||
n: 2,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", 2],
|
||||
o: undefined,
|
||||
n: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle removing elements from an array", () => {
|
||||
const oldObj = { a: [1, 2, 3] };
|
||||
const newObj = { a: [1, 3] };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a", 1],
|
||||
o: 2,
|
||||
n: 3,
|
||||
},
|
||||
{
|
||||
t: "r",
|
||||
p: ["a", 2],
|
||||
o: 3,
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle complex nested changes", () => {
|
||||
const oldObj = {
|
||||
a: {
|
||||
b: [1, 2, { c: 3 }],
|
||||
},
|
||||
};
|
||||
|
||||
const newObj = {
|
||||
a: {
|
||||
b: [1, 2, { c: 4 }, 5],
|
||||
},
|
||||
};
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a", "b", 2, "c"],
|
||||
o: 3,
|
||||
n: 4,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["a", "b", 3],
|
||||
o: undefined,
|
||||
n: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle undefined and null values", () => {
|
||||
const oldObj = { a: undefined, b: null };
|
||||
const newObj = { a: null, b: undefined };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: undefined,
|
||||
n: null,
|
||||
},
|
||||
{
|
||||
t: "e",
|
||||
p: ["b"],
|
||||
o: null,
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle type changes", () => {
|
||||
const oldObj = { a: 1 };
|
||||
const newObj = { a: "1" };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: "1",
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle properties added and removed simultaneously", () => {
|
||||
const oldObj = { a: 1, b: 2 };
|
||||
const newObj = { a: 1, c: 3 };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "r",
|
||||
p: ["b"],
|
||||
o: 2,
|
||||
n: undefined,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["c"],
|
||||
o: undefined,
|
||||
n: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle arrays replaced with objects", () => {
|
||||
const oldObj = { a: [1, 2, 3] };
|
||||
const newObj = { a: { b: 4 } };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: [1, 2, 3],
|
||||
n: { b: 4 },
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle objects replaced with primitives", () => {
|
||||
const oldObj = { a: { b: 1 } };
|
||||
const newObj = { a: 2 };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "e",
|
||||
p: ["a"],
|
||||
o: { b: 1 },
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle root object changes", () => {
|
||||
const oldObj = { a: 1 };
|
||||
const newObj = { b: 2 };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "r",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: undefined,
|
||||
},
|
||||
{
|
||||
t: "a",
|
||||
p: ["b"],
|
||||
o: undefined,
|
||||
n: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle identical objects", () => {
|
||||
const oldObj = { a: 1, b: { c: 2 } };
|
||||
const newObj = { a: 1, b: { c: 2 } };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle empty objects", () => {
|
||||
const oldObj = {};
|
||||
const newObj = {};
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle changes from empty object to non-empty object", () => {
|
||||
const oldObj = {};
|
||||
const newObj = { a: 1 };
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "a",
|
||||
p: ["a"],
|
||||
o: undefined,
|
||||
n: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
|
||||
it("should handle changes from non-empty object to empty object", () => {
|
||||
const oldObj = { a: 1 };
|
||||
const newObj = {};
|
||||
|
||||
const diffs = diff(oldObj, newObj);
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
t: "r",
|
||||
p: ["a"],
|
||||
o: 1,
|
||||
n: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const appliedObj = apply(oldObj, diffs);
|
||||
expect(appliedObj).toEqual(newObj);
|
||||
|
||||
const revertedObj = revert(newObj, diffs);
|
||||
expect(revertedObj).toEqual(oldObj);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
|
||||
import { deprecated__whereRepoSchema } from "../../../src/data";
|
||||
import { type ObjectQuery, convert, validate } from "core/object/query/object-query";
|
||||
|
||||
describe("object-query", () => {
|
||||
const q: ObjectQuery = { name: "Michael" };
|
||||
@@ -8,22 +7,9 @@ describe("object-query", () => {
|
||||
const q3: ObjectQuery = { name: "Michael", age: { $gt: 18 } };
|
||||
const bag = { q, q2, q3 };
|
||||
|
||||
test("translates into legacy", async () => {
|
||||
for (const [key, value] of Object.entries(bag)) {
|
||||
const obj = convert(value);
|
||||
try {
|
||||
const parsed = deprecated__whereRepoSchema.parse(obj);
|
||||
expect(parsed).toBeDefined();
|
||||
} catch (e) {
|
||||
console.log("errored", { obj, value });
|
||||
console.error(key, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("validates", async () => {
|
||||
const converted = convert({
|
||||
name: { $eq: "ch" }
|
||||
name: { $eq: "ch" },
|
||||
});
|
||||
validate(converted, { name: "Michael" });
|
||||
});
|
||||
@@ -45,7 +31,7 @@ describe("object-query", () => {
|
||||
[{ val: { $notnull: 1 } }, { val: null }, false],
|
||||
[{ val: { $regex: ".*" } }, { val: "test" }, true],
|
||||
[{ val: { $regex: /^t.*/ } }, { val: "test" }, true],
|
||||
[{ val: { $regex: /^b.*/ } }, { val: "test" }, false]
|
||||
[{ val: { $regex: /^b.*/ } }, { val: "test" }, false],
|
||||
];
|
||||
|
||||
for (const [query, object, expected] of tests) {
|
||||
@@ -69,10 +55,10 @@ describe("object-query", () => {
|
||||
[
|
||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||
{ val1: "foo", val2: "bar" },
|
||||
true
|
||||
true,
|
||||
],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 1 }, true],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false]
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false],
|
||||
];
|
||||
|
||||
for (const [query, object, expected] of tests) {
|
||||
@@ -80,4 +66,14 @@ describe("object-query", () => {
|
||||
expect(result).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("paths", () => {
|
||||
const result = validate({ "user.age": { $lt: 18 } }, { user: { age: 17 } });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("empty filters", () => {
|
||||
const result = validate({}, { user: { age: 17 } });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
makeValidator,
|
||||
exp,
|
||||
Expression,
|
||||
isPrimitive,
|
||||
type Primitive,
|
||||
} from "../../../src/core/object/query/query";
|
||||
|
||||
describe("query", () => {
|
||||
test("isPrimitive", () => {
|
||||
expect(isPrimitive(1)).toBe(true);
|
||||
expect(isPrimitive("1")).toBe(true);
|
||||
expect(isPrimitive(true)).toBe(true);
|
||||
expect(isPrimitive(false)).toBe(true);
|
||||
|
||||
// not primitives
|
||||
expect(isPrimitive(null)).toBe(false);
|
||||
expect(isPrimitive(undefined)).toBe(false);
|
||||
expect(isPrimitive([])).toBe(false);
|
||||
expect(isPrimitive({})).toBe(false);
|
||||
expect(isPrimitive(Symbol("test"))).toBe(false);
|
||||
expect(isPrimitive(new Date())).toBe(false);
|
||||
expect(isPrimitive(new Error())).toBe(false);
|
||||
expect(isPrimitive(new Set())).toBe(false);
|
||||
expect(isPrimitive(new Map())).toBe(false);
|
||||
});
|
||||
|
||||
test("strict expression creation", () => {
|
||||
// @ts-expect-error
|
||||
expect(() => exp()).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("")).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("invalid")).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("$eq")).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("$eq", 1)).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("$eq", () => null)).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => exp("$eq", () => null, 1)).toThrow();
|
||||
expect(
|
||||
exp(
|
||||
"$eq",
|
||||
() => true,
|
||||
() => null,
|
||||
),
|
||||
).toBeInstanceOf(Expression);
|
||||
});
|
||||
|
||||
test("$eq is required", () => {
|
||||
expect(() => makeValidator([])).toThrow();
|
||||
expect(() =>
|
||||
makeValidator([
|
||||
exp(
|
||||
"$valid",
|
||||
() => true,
|
||||
() => null,
|
||||
),
|
||||
]),
|
||||
).toThrow();
|
||||
expect(
|
||||
makeValidator([
|
||||
exp(
|
||||
"$eq",
|
||||
() => true,
|
||||
() => null,
|
||||
),
|
||||
]),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
test("validates filter structure", () => {
|
||||
const validator = makeValidator([
|
||||
exp(
|
||||
"$eq",
|
||||
(v: Primitive) => isPrimitive(v),
|
||||
(e, a) => e === a,
|
||||
),
|
||||
exp(
|
||||
"$like",
|
||||
(v: string) => typeof v === "string",
|
||||
(e, a) => e === a,
|
||||
),
|
||||
]);
|
||||
|
||||
// @ts-expect-error intentionally typed as union of given expression keys
|
||||
expect(validator.expressionKeys).toEqual(["$eq", "$like"]);
|
||||
|
||||
// @ts-expect-error "$and" is not allowed
|
||||
expect(() => validator.convert({ $and: {} })).toThrow();
|
||||
|
||||
// @ts-expect-error "$or" must be an object
|
||||
expect(() => validator.convert({ $or: [] })).toThrow();
|
||||
|
||||
// @ts-expect-error "invalid" is not a valid expression key
|
||||
expect(() => validator.convert({ foo: { invalid: "bar" } })).toThrow();
|
||||
|
||||
// @ts-expect-error "invalid" is not a valid expression key
|
||||
expect(() => validator.convert({ foo: { $invalid: "bar" } })).toThrow();
|
||||
|
||||
// @ts-expect-error "null" is not a valid value
|
||||
expect(() => validator.convert({ foo: null })).toThrow();
|
||||
|
||||
// @ts-expect-error only primitives are allowed for $eq
|
||||
expect(() => validator.convert({ foo: { $eq: [] } })).toThrow();
|
||||
|
||||
// @ts-expect-error only strings are allowed for $like
|
||||
expect(() => validator.convert({ foo: { $like: 1 } })).toThrow();
|
||||
|
||||
// undefined values are ignored
|
||||
expect(validator.convert({ foo: undefined })).toEqual({});
|
||||
|
||||
expect(validator.convert({ foo: "bar" })).toEqual({ foo: { $eq: "bar" } });
|
||||
expect(validator.convert({ foo: { $eq: "bar" } })).toEqual({ foo: { $eq: "bar" } });
|
||||
expect(validator.convert({ foo: { $like: "bar" } })).toEqual({ foo: { $like: "bar" } });
|
||||
});
|
||||
});
|
||||
+387
-15
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Perf } from "../../src/core/utils";
|
||||
import * as reqres from "../../src/core/utils/reqres";
|
||||
import * as strings from "../../src/core/utils/strings";
|
||||
import { Perf, ucFirst } from "../../src/core/utils";
|
||||
import * as utils from "../../src/core/utils";
|
||||
import { assetsPath } from "../helper";
|
||||
|
||||
async function wait(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -13,33 +13,33 @@ describe("Core Utils", async () => {
|
||||
describe("[core] strings", async () => {
|
||||
test("objectToKeyValueArray", async () => {
|
||||
const obj = { a: 1, b: 2, c: 3 };
|
||||
const result = strings.objectToKeyValueArray(obj);
|
||||
const result = utils.objectToKeyValueArray(obj);
|
||||
expect(result).toEqual([
|
||||
{ key: "a", value: 1 },
|
||||
{ key: "b", value: 2 },
|
||||
{ key: "c", value: 3 }
|
||||
{ key: "c", value: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("snakeToPascalWithSpaces", async () => {
|
||||
const result = strings.snakeToPascalWithSpaces("snake_to_pascal");
|
||||
const result = utils.snakeToPascalWithSpaces("snake_to_pascal");
|
||||
expect(result).toBe("Snake To Pascal");
|
||||
});
|
||||
|
||||
test("randomString", async () => {
|
||||
const result = strings.randomString(10);
|
||||
const result = utils.randomString(10);
|
||||
expect(result).toHaveLength(10);
|
||||
});
|
||||
|
||||
test("pascalToKebab", async () => {
|
||||
const result = strings.pascalToKebab("PascalCase");
|
||||
const result = utils.pascalToKebab("PascalCase");
|
||||
expect(result).toBe("pascal-case");
|
||||
});
|
||||
|
||||
test("replaceSimplePlaceholders", async () => {
|
||||
const str = "Hello, {$name}!";
|
||||
const vars = { name: "John" };
|
||||
const result = strings.replaceSimplePlaceholders(str, vars);
|
||||
const result = utils.replaceSimplePlaceholders(str, vars);
|
||||
expect(result).toBe("Hello, John!");
|
||||
});
|
||||
});
|
||||
@@ -49,31 +49,31 @@ describe("Core Utils", async () => {
|
||||
const headers = new Headers();
|
||||
headers.append("Content-Type", "application/json");
|
||||
headers.append("Authorization", "Bearer 123");
|
||||
const obj = reqres.headersToObject(headers);
|
||||
const obj = utils.headersToObject(headers);
|
||||
expect(obj).toEqual({
|
||||
"content-type": "application/json",
|
||||
authorization: "Bearer 123"
|
||||
authorization: "Bearer 123",
|
||||
});
|
||||
});
|
||||
|
||||
test("replaceUrlParam", () => {
|
||||
const url = "/api/:id/:name";
|
||||
const params = { id: "123", name: "test" };
|
||||
const result = reqres.replaceUrlParam(url, params);
|
||||
const result = utils.replaceUrlParam(url, params);
|
||||
expect(result).toBe("/api/123/test");
|
||||
});
|
||||
|
||||
test("encode", () => {
|
||||
const obj = { id: "123", name: "test" };
|
||||
const result = reqres.encodeSearch(obj);
|
||||
const result = utils.encodeSearch(obj);
|
||||
expect(result).toBe("id=123&name=test");
|
||||
|
||||
const obj2 = { id: "123", name: ["test1", "test2"] };
|
||||
const result2 = reqres.encodeSearch(obj2);
|
||||
const result2 = utils.encodeSearch(obj2);
|
||||
expect(result2).toBe("id=123&name=test1&name=test2");
|
||||
|
||||
const obj3 = { id: "123", name: { test: "test" } };
|
||||
const result3 = reqres.encodeSearch(obj3, { encode: true });
|
||||
const result3 = utils.encodeSearch(obj3, { encode: true });
|
||||
expect(result3).toBe("id=123&name=%7B%22test%22%3A%22test%22%7D");
|
||||
});
|
||||
});
|
||||
@@ -108,4 +108,376 @@ describe("Core Utils", async () => {
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("objects", () => {
|
||||
test("omitKeys", () => {
|
||||
const objects = [
|
||||
[{ a: 1, b: 2, c: 3 }, ["a"], { b: 2, c: 3 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["b"], { a: 1, c: 3 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["c"], { a: 1, b: 2 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
|
||||
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
|
||||
] as [object, string[], object][];
|
||||
|
||||
for (const [obj, keys, expected] of objects) {
|
||||
const result = utils.omitKeys(obj, keys as any);
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("isEqual", () => {
|
||||
const objects = [
|
||||
[1, 1, true],
|
||||
[1, "1", false],
|
||||
[1, 2, false],
|
||||
["1", "1", true],
|
||||
["1", "2", false],
|
||||
[true, true, true],
|
||||
[true, false, false],
|
||||
[false, false, true],
|
||||
[1, Number.NaN, false],
|
||||
[Number.NaN, Number.NaN, true],
|
||||
[null, null, true],
|
||||
[null, undefined, false],
|
||||
[undefined, undefined, true],
|
||||
[new Map([["a", 1]]), new Map([["a", 1]]), true],
|
||||
[new Map([["a", 1]]), new Map([["a", 2]]), false],
|
||||
[new Map([["a", 1]]), new Map([["b", 1]]), false],
|
||||
[
|
||||
new Map([["a", 1]]),
|
||||
new Map([
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
]),
|
||||
false,
|
||||
],
|
||||
[{ a: 1 }, { a: 1 }, true],
|
||||
[{ a: 1 }, { a: 2 }, false],
|
||||
[{ a: 1 }, { b: 1 }, false],
|
||||
[{ a: "1" }, { a: "1" }, true],
|
||||
[{ a: "1" }, { a: "2" }, false],
|
||||
[{ a: "1" }, { b: "1" }, false],
|
||||
[{ a: 1 }, { a: 1, b: 2 }, false],
|
||||
[{ a: [1, 2, 3] }, { a: [1, 2, 3] }, true],
|
||||
[{ a: [1, 2, 3] }, { a: [1, 2, 4] }, false],
|
||||
[{ a: [1, 2, 3] }, { a: [1, 2, 3, 4] }, false],
|
||||
[{ a: { b: 1 } }, { a: { b: 1 } }, true],
|
||||
[{ a: { b: 1 } }, { a: { b: 2 } }, false],
|
||||
[{ a: { b: 1 } }, { a: { c: 1 } }, false],
|
||||
[{ a: { b: 1 } }, { a: { b: 1, c: 2 } }, false],
|
||||
[[1, 2, 3], [1, 2, 3], true],
|
||||
[[1, 2, 3], [1, 2, 4], false],
|
||||
[[1, 2, 3], [1, 2, 3, 4], false],
|
||||
[[{ a: 1 }], [{ a: 1 }], true],
|
||||
[[{ a: 1 }], [{ a: 2 }], false],
|
||||
[[{ a: 1 }], [{ b: 1 }], false],
|
||||
] as [any, any, boolean][];
|
||||
|
||||
for (const [a, b, expected] of objects) {
|
||||
const result = utils.isEqual(a, b);
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("getPath", () => {
|
||||
const tests = [
|
||||
[{ a: 1, b: 2, c: 3 }, "a", 1],
|
||||
[{ a: 1, b: 2, c: 3 }, "b", 2],
|
||||
[{ a: { b: 1 } }, "a.b", 1],
|
||||
[{ a: { b: 1 } }, "a.b.c", null, null],
|
||||
[{ a: { b: 1 } }, "a.b.c", 1, 1],
|
||||
[[[1]], "0.0", 1],
|
||||
] as [object, string, any, any][];
|
||||
|
||||
for (const [obj, path, expected, defaultValue] of tests) {
|
||||
const result = utils.getPath(obj, path, defaultValue);
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("recursivelyReplacePlaceholders", () => {
|
||||
// test basic replacement with simple pattern
|
||||
const obj1 = { a: "Hello, {$name}!", b: { c: "Hello, {$name}!" } };
|
||||
const variables1 = { name: "John" };
|
||||
const result1 = utils.recursivelyReplacePlaceholders(obj1, /\{\$(\w+)\}/g, variables1);
|
||||
expect(result1).toEqual({ a: "Hello, John!", b: { c: "Hello, John!" } });
|
||||
|
||||
// test the specific example from the user request
|
||||
const obj2 = { some: "value", here: "@auth.user" };
|
||||
const variables2 = { auth: { user: "what" } };
|
||||
const result2 = utils.recursivelyReplacePlaceholders(obj2, /^@([a-z\.]+)$/, variables2);
|
||||
expect(result2).toEqual({ some: "value", here: "what" });
|
||||
|
||||
// test with arrays
|
||||
const obj3 = { items: ["@config.name", "static", "@config.version"] };
|
||||
const variables3 = { config: { name: "MyApp", version: "1.0.0" } };
|
||||
const result3 = utils.recursivelyReplacePlaceholders(obj3, /^@([a-z\.]+)$/, variables3);
|
||||
expect(result3).toEqual({ items: ["MyApp", "static", "1.0.0"] });
|
||||
|
||||
// test with nested objects and deep paths
|
||||
const obj4 = {
|
||||
user: "@auth.user.name",
|
||||
settings: {
|
||||
theme: "@ui.theme",
|
||||
nested: {
|
||||
value: "@deep.nested.value",
|
||||
},
|
||||
},
|
||||
};
|
||||
const variables4 = {
|
||||
auth: { user: { name: "Alice" } },
|
||||
ui: { theme: "dark" },
|
||||
deep: { nested: { value: "found" } },
|
||||
};
|
||||
const result4 = utils.recursivelyReplacePlaceholders(obj4, /^@([a-z\.]+)$/, variables4);
|
||||
expect(result4).toEqual({
|
||||
user: "Alice",
|
||||
settings: {
|
||||
theme: "dark",
|
||||
nested: {
|
||||
value: "found",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// test with missing paths (should return original match)
|
||||
const obj5 = { value: "@missing.path" };
|
||||
const variables5 = { existing: "value" };
|
||||
const result5 = utils.recursivelyReplacePlaceholders(obj5, /^@([a-z\.]+)$/, variables5);
|
||||
expect(result5).toEqual({ value: "@missing.path" });
|
||||
|
||||
// test with non-matching strings (should remain unchanged)
|
||||
const obj6 = { value: "normal string", other: "not@matching" };
|
||||
const variables6 = { some: "value" };
|
||||
const result6 = utils.recursivelyReplacePlaceholders(obj6, /^@([a-z\.]+)$/, variables6);
|
||||
expect(result6).toEqual({ value: "normal string", other: "not@matching" });
|
||||
|
||||
// test with primitive values (should handle gracefully)
|
||||
expect(
|
||||
utils.recursivelyReplacePlaceholders("@test.value", /^@([a-z\.]+)$/, {
|
||||
test: { value: "replaced" },
|
||||
}),
|
||||
).toBe("replaced");
|
||||
expect(utils.recursivelyReplacePlaceholders(123, /^@([a-z\.]+)$/, {})).toBe(123);
|
||||
expect(utils.recursivelyReplacePlaceholders(null, /^@([a-z\.]+)$/, {})).toBe(null);
|
||||
|
||||
// test type preservation for full string matches
|
||||
const variables7 = { test: { value: 123, flag: true, data: null, arr: [1, 2, 3] } };
|
||||
const result7 = utils.recursivelyReplacePlaceholders(
|
||||
{
|
||||
number: "@test.value",
|
||||
boolean: "@test.flag",
|
||||
nullValue: "@test.data",
|
||||
array: "@test.arr",
|
||||
},
|
||||
/^@([a-z\.]+)$/,
|
||||
variables7,
|
||||
null,
|
||||
);
|
||||
expect(result7).toEqual({
|
||||
number: 123,
|
||||
boolean: true,
|
||||
nullValue: null,
|
||||
array: [1, 2, 3],
|
||||
});
|
||||
|
||||
// test partial string replacement (should convert to string)
|
||||
const result8 = utils.recursivelyReplacePlaceholders(
|
||||
{ message: "The value is @test.value!" },
|
||||
/@([a-z\.]+)/g,
|
||||
variables7,
|
||||
);
|
||||
expect(result8).toEqual({ message: "The value is 123!" });
|
||||
|
||||
// test with fallback parameter
|
||||
const obj9 = { user: "@user.id", config: "@config.theme" };
|
||||
const variables9 = {}; // empty context
|
||||
const result9 = utils.recursivelyReplacePlaceholders(
|
||||
obj9,
|
||||
/^@([a-z\.]+)$/,
|
||||
variables9,
|
||||
null,
|
||||
);
|
||||
expect(result9).toEqual({ user: null, config: null });
|
||||
|
||||
// test with fallback for partial matches
|
||||
const obj10 = { message: "Hello @user.name, welcome!" };
|
||||
const variables10 = {}; // empty context
|
||||
const result10 = utils.recursivelyReplacePlaceholders(
|
||||
obj10,
|
||||
/@([a-z\.]+)/g,
|
||||
variables10,
|
||||
"Guest",
|
||||
);
|
||||
expect(result10).toEqual({ message: "Hello Guest, welcome!" });
|
||||
|
||||
// test with different fallback types
|
||||
const obj11 = {
|
||||
stringFallback: "@missing.string",
|
||||
numberFallback: "@missing.number",
|
||||
booleanFallback: "@missing.boolean",
|
||||
objectFallback: "@missing.object",
|
||||
};
|
||||
const variables11 = {};
|
||||
const result11 = utils.recursivelyReplacePlaceholders(
|
||||
obj11,
|
||||
/^@([a-z\.]+)$/,
|
||||
variables11,
|
||||
"default",
|
||||
);
|
||||
expect(result11).toEqual({
|
||||
stringFallback: "default",
|
||||
numberFallback: "default",
|
||||
booleanFallback: "default",
|
||||
objectFallback: "default",
|
||||
});
|
||||
|
||||
// test fallback with arrays
|
||||
const obj12 = { items: ["@item1", "@item2", "static"] };
|
||||
const variables12 = { item1: "found" }; // item2 is missing
|
||||
const result12 = utils.recursivelyReplacePlaceholders(
|
||||
obj12,
|
||||
/^@([a-zA-Z0-9\.]+)$/,
|
||||
variables12,
|
||||
"missing",
|
||||
);
|
||||
expect(result12).toEqual({ items: ["found", "missing", "static"] });
|
||||
|
||||
// test fallback with nested objects
|
||||
const obj13 = {
|
||||
user: "@user.id",
|
||||
settings: {
|
||||
theme: "@theme.name",
|
||||
nested: {
|
||||
value: "@deep.value",
|
||||
},
|
||||
},
|
||||
};
|
||||
const variables13 = {}; // empty context
|
||||
const result13 = utils.recursivelyReplacePlaceholders(
|
||||
obj13,
|
||||
/^@([a-z\.]+)$/,
|
||||
variables13,
|
||||
null,
|
||||
);
|
||||
expect(result13).toEqual({
|
||||
user: null,
|
||||
settings: {
|
||||
theme: null,
|
||||
nested: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("file", async () => {
|
||||
describe("type guards", () => {
|
||||
const types = {
|
||||
blob: new Blob(),
|
||||
file: new File([""], "file.txt"),
|
||||
stream: new ReadableStream(),
|
||||
arrayBuffer: new ArrayBuffer(10),
|
||||
arrayBufferView: new Uint8Array(new ArrayBuffer(10)),
|
||||
};
|
||||
|
||||
const fns = [
|
||||
[utils.isReadableStream, "stream"],
|
||||
[utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]],
|
||||
[utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]],
|
||||
[utils.isArrayBuffer, "arrayBuffer"],
|
||||
[utils.isArrayBufferView, "arrayBufferView"],
|
||||
] as const;
|
||||
|
||||
const additional = [0, 0.0, "", null, undefined, {}, []];
|
||||
|
||||
for (const [fn, type, _to_test] of fns) {
|
||||
test(`is${ucFirst(type)}`, () => {
|
||||
const to_test = _to_test ?? (Object.keys(types) as string[]);
|
||||
for (const key of to_test) {
|
||||
const value = types[key as keyof typeof types];
|
||||
const result = fn(value);
|
||||
expect(result).toBe(key === type);
|
||||
}
|
||||
|
||||
for (const value of additional) {
|
||||
const result = fn(value);
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("getContentName", () => {
|
||||
const name = "test.json";
|
||||
const text = "attachment; filename=" + name;
|
||||
const headers = new Headers({
|
||||
"Content-Disposition": text,
|
||||
});
|
||||
const request = new Request("http://example.com", {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(utils.getContentName(text)).toBe(name);
|
||||
expect(utils.getContentName(headers)).toBe(name);
|
||||
expect(utils.getContentName(request)).toBe(name);
|
||||
});
|
||||
|
||||
test("detectImageDimensions", async () => {
|
||||
// wrong
|
||||
// @ts-expect-error
|
||||
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
||||
|
||||
// successful ones
|
||||
const getFile = (name: string): File => Bun.file(`${assetsPath}/${name}`) as any;
|
||||
expect(await utils.detectImageDimensions(getFile("image.png"))).toEqual({
|
||||
width: 362,
|
||||
height: 387,
|
||||
});
|
||||
expect(await utils.detectImageDimensions(getFile("image.jpg"))).toEqual({
|
||||
width: 453,
|
||||
height: 512,
|
||||
});
|
||||
});
|
||||
|
||||
test("isFileAccepted", () => {
|
||||
const file = new File([""], "file.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
expect(utils.isFileAccepted(file, "text/plain")).toBe(true);
|
||||
expect(utils.isFileAccepted(file, "text/plain,text/html")).toBe(true);
|
||||
expect(utils.isFileAccepted(file, "text/html")).toBe(false);
|
||||
|
||||
{
|
||||
const file = new File([""], "file.jpg", {
|
||||
type: "image/jpeg",
|
||||
});
|
||||
expect(utils.isFileAccepted(file, "image/jpeg")).toBe(true);
|
||||
expect(utils.isFileAccepted(file, "image/jpeg,image/png")).toBe(true);
|
||||
expect(utils.isFileAccepted(file, "image/png")).toBe(false);
|
||||
expect(utils.isFileAccepted(file, "image/*")).toBe(true);
|
||||
expect(utils.isFileAccepted(file, ".jpg")).toBe(true);
|
||||
expect(utils.isFileAccepted(file, ".jpg,.png")).toBe(true);
|
||||
expect(utils.isFileAccepted(file, ".png")).toBe(false);
|
||||
}
|
||||
|
||||
{
|
||||
const file = new File([""], "file.png");
|
||||
expect(utils.isFileAccepted(file, undefined as any)).toBe(true);
|
||||
}
|
||||
|
||||
expect(() => utils.isFileAccepted(null as any, "text/plain")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("dates", () => {
|
||||
test("formats local time", () => {
|
||||
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
|
||||
/*console.log(utils.datetimeStringUTC(new Date()));
|
||||
console.log(utils.datetimeStringUTC());
|
||||
console.log(new Date());
|
||||
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone); */
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,80 +1,32 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
|
||||
import { Guard } from "../../src/auth";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
type EntityData,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
type MutatorResponse,
|
||||
type RepositoryResponse,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
import { parse } from "core/utils/schema";
|
||||
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
||||
import { Entity, EntityManager, type EntityData } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
const dataConfig = parse(dataConfigSchema, {});
|
||||
describe("[data] DataController", async () => {
|
||||
test("repoResult", async () => {
|
||||
const em = new EntityManager<any>([], dummyConnection);
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
|
||||
const res = controller.repoResult({
|
||||
entity: null as any,
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any,
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0
|
||||
}
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0
|
||||
},
|
||||
data: []
|
||||
});
|
||||
});
|
||||
|
||||
test("mutatorResult", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
|
||||
const res = controller.mutatorResult({
|
||||
entity: null as any,
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
data: []
|
||||
});
|
||||
});
|
||||
|
||||
describe("getController", async () => {
|
||||
const users = new Entity("users", [
|
||||
new TextField("name", { required: true }),
|
||||
new TextField("bio")
|
||||
new TextField("bio"),
|
||||
]);
|
||||
const posts = new Entity("posts", [new TextField("content")]);
|
||||
const em = new EntityManager([users, posts], dummyConnection, [
|
||||
new ManyToOneRelation(posts, users)
|
||||
new ManyToOneRelation(posts, users),
|
||||
]);
|
||||
|
||||
await em.schema().sync({ force: true });
|
||||
@@ -83,12 +35,12 @@ describe("[data] DataController", async () => {
|
||||
users: [
|
||||
{ name: "foo", bio: "bar" },
|
||||
{ name: "bar", bio: null },
|
||||
{ name: "baz", bio: "!!!" }
|
||||
{ name: "baz", bio: "!!!" },
|
||||
],
|
||||
posts: [
|
||||
{ content: "post 1", users_id: 1 },
|
||||
{ content: "post 2", users_id: 2 }
|
||||
]
|
||||
{ content: "post 2", users_id: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
@@ -116,12 +68,11 @@ describe("[data] DataController", async () => {
|
||||
//console.log("app.routes", app.routes);
|
||||
// create users
|
||||
for await (const _user of fixtures.users) {
|
||||
const res = await app.request("/users", {
|
||||
const res = await app.request("/entity/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_user)
|
||||
body: JSON.stringify(_user),
|
||||
});
|
||||
//console.log("res", { _user }, res);
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const result = (await res.json()) as MutatorResultJSON;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
@@ -131,11 +82,11 @@ describe("[data] DataController", async () => {
|
||||
|
||||
// create posts
|
||||
for await (const _post of fixtures.posts) {
|
||||
const res = await app.request("/posts", {
|
||||
const res = await app.request("/entity/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_post)
|
||||
body: JSON.stringify(_post),
|
||||
});
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const result = (await res.json()) as MutatorResultJSON;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
@@ -145,91 +96,89 @@ describe("[data] DataController", async () => {
|
||||
});
|
||||
|
||||
test("/:entity (read many)", async () => {
|
||||
const res = await app.request("/users");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const res = await app.request("/entity/users");
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(3);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).toBe(3);
|
||||
expect(data.data.length).toBe(3);
|
||||
expect(data.data[0].name).toBe("foo");
|
||||
expect(data.data[0]?.name).toBe("foo");
|
||||
});
|
||||
|
||||
test("/:entity/query (func query)", async () => {
|
||||
const res = await app.request("/users/query", {
|
||||
const res = await app.request("/entity/users/query", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
where: { bio: { $isnull: 1 } }
|
||||
})
|
||||
where: { bio: { $isnull: 1 } },
|
||||
}),
|
||||
});
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].name).toBe("bar");
|
||||
expect(data.data[0]?.name).toBe("bar");
|
||||
});
|
||||
|
||||
test("/:entity (read many, paginated)", async () => {
|
||||
const res = await app.request("/users?limit=1&offset=2");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const res = await app.request("/entity/users?limit=1&offset=2");
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(3);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].name).toBe("baz");
|
||||
expect(data.data[0]?.name).toBe("baz");
|
||||
});
|
||||
|
||||
test("/:entity/:id (read one)", async () => {
|
||||
const res = await app.request("/users/3");
|
||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
console.log("data", data);
|
||||
const res = await app.request("/entity/users/3");
|
||||
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||
});
|
||||
|
||||
test("/:entity (update one)", async () => {
|
||||
const res = await app.request("/users/3", {
|
||||
const res = await app.request("/entity/users/3", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ name: "new name" })
|
||||
body: JSON.stringify({ name: "new name" }),
|
||||
});
|
||||
const { data } = (await res.json()) as MutatorResponse;
|
||||
const { data } = (await res.json()) as MutatorResultJSON;
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||
});
|
||||
|
||||
test("/:entity/:id/:reference (read references)", async () => {
|
||||
const res = await app.request("/users/1/posts");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
console.log("data", data);
|
||||
const res = await app.request("/entity/users/1/posts");
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(2);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(2);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].content).toBe("post 1");
|
||||
expect(data.data[0]?.content).toBe("post 1");
|
||||
});
|
||||
|
||||
test("/:entity/:id (delete one)", async () => {
|
||||
const res = await app.request("/posts/2", {
|
||||
method: "DELETE"
|
||||
const res = await app.request("/entity/posts/2", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
const { data } = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||
|
||||
// verify
|
||||
const res2 = await app.request("/posts");
|
||||
const data2 = (await res2.json()) as RepositoryResponse;
|
||||
expect(data2.meta.total).toBe(1);
|
||||
const res2 = await app.request("/entity/posts");
|
||||
const data2 = (await res2.json()) as RepositoryResultJSON;
|
||||
//expect(data2.meta.total).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
NumberField,
|
||||
PrimaryField,
|
||||
Repository,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, PrimaryField, NumberField } from "data/fields";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -18,14 +12,14 @@ describe("some tests", async () => {
|
||||
|
||||
const users = new Entity("users", [
|
||||
new TextField("username", { required: true, default_value: "nobody" }),
|
||||
new TextField("email", { max_length: 3 })
|
||||
new TextField("email", { maxLength: 3 }),
|
||||
]);
|
||||
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title"),
|
||||
new TextField("content"),
|
||||
new TextField("created_at"),
|
||||
new NumberField("likes", { default_value: 0 })
|
||||
new NumberField("likes", { default_value: 0 }),
|
||||
]);
|
||||
|
||||
const em = new EntityManager([users, posts], connection);
|
||||
@@ -34,39 +28,32 @@ describe("some tests", async () => {
|
||||
|
||||
test("findId", async () => {
|
||||
const query = await em.repository(users).findId(1);
|
||||
/*const { result, total, count, time } = query;
|
||||
console.log("query", query.result, {
|
||||
result,
|
||||
total,
|
||||
count,
|
||||
time,
|
||||
});*/
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?'
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? order by "users"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([1, 1]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.parameters).toEqual([1, 1, 0]);
|
||||
expect(query.data).toBeUndefined();
|
||||
});
|
||||
|
||||
test("findMany", async () => {
|
||||
const query = await em.repository(users).findMany();
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?'
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("findMany with number", async () => {
|
||||
const query = await em.repository(posts).findMany();
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?'
|
||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("try adding an existing field name", async () => {
|
||||
@@ -74,7 +61,7 @@ describe("some tests", async () => {
|
||||
new Entity("users", [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("email") // not throwing, it's just being ignored
|
||||
new TextField("email"), // not throwing, it's just being ignored
|
||||
]);
|
||||
}).toBeDefined();
|
||||
|
||||
@@ -83,7 +70,7 @@ describe("some tests", async () => {
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
// field config differs, will throw
|
||||
new TextField("email", { required: true })
|
||||
new TextField("email", { required: true }),
|
||||
]);
|
||||
}).toThrow();
|
||||
|
||||
@@ -91,7 +78,7 @@ describe("some tests", async () => {
|
||||
new Entity("users", [
|
||||
new PrimaryField(),
|
||||
new TextField("username"),
|
||||
new TextField("email")
|
||||
new TextField("email"),
|
||||
]);
|
||||
}).toBeDefined();
|
||||
});
|
||||
@@ -110,4 +97,18 @@ describe("some tests", async () => {
|
||||
new EntityManager([entity, entity2], connection);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("primary uuid", async () => {
|
||||
const entity = new Entity("users", [
|
||||
new PrimaryField("id", { format: "uuid" }),
|
||||
new TextField("username"),
|
||||
]);
|
||||
const em = new EntityManager([entity], getDummyConnection().dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const mutator = em.mutator(entity);
|
||||
const data = await mutator.insertOne({ username: "test" });
|
||||
expect(data.data.id).toBeDefined();
|
||||
expect(data.data.id).toBeString();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
|
||||
import type { SqliteDatabase } from "kysely";
|
||||
// @ts-ignore
|
||||
import Database from "libsql";
|
||||
import { SqliteLocalConnection } from "../../src/data";
|
||||
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
||||
afterAllCleanup: async () => {
|
||||
if (!memory) await unlink(DB_NAME);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
|
||||
|
||||
return {
|
||||
dummyConnection,
|
||||
afterAllCleanup
|
||||
afterAllCleanup,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
SchemaManager,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -21,7 +18,7 @@ describe("Mutator relation", async () => {
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title"),
|
||||
new TextField("content", { default_value: "..." }),
|
||||
new NumberField("count", { default_value: 0 })
|
||||
new NumberField("count", { default_value: 0 }),
|
||||
]);
|
||||
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
@@ -44,7 +41,7 @@ describe("Mutator relation", async () => {
|
||||
expect(em.mutator(posts).insertOne({ title: "post2", users_id: 10 })).rejects.toThrow();
|
||||
|
||||
expect(
|
||||
em.mutator(posts).insertOne({ title: "post2", users_id: data.id })
|
||||
em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
||||
import { TransformPersistFailedException } from "../../src/data/errors";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { TransformPersistFailedException } from "data/errors";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -14,9 +15,9 @@ describe("Mutator simple", async () => {
|
||||
|
||||
const items = new Entity("items", [
|
||||
new TextField("label", { required: true, minLength: 1 }),
|
||||
new NumberField("count", { default_value: 0 })
|
||||
new NumberField("count", { default_value: 0 }),
|
||||
]);
|
||||
const em = new EntityManager([items], connection);
|
||||
const em = new EntityManager<any>([items], connection);
|
||||
|
||||
await em.connection.kysely.schema
|
||||
.createTable("items")
|
||||
@@ -29,11 +30,11 @@ describe("Mutator simple", async () => {
|
||||
test("insert single row", async () => {
|
||||
const mutation = await em.mutator(items).insertOne({
|
||||
label: "test",
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"'
|
||||
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
|
||||
|
||||
@@ -41,11 +42,11 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||
expect(query.data).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||
});
|
||||
|
||||
test("update inserted row", async () => {
|
||||
@@ -53,18 +54,18 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
const id = query.data![0].id as number;
|
||||
|
||||
const mutation = await em.mutator(items).updateOne(id, {
|
||||
label: "new label",
|
||||
count: 100
|
||||
count: 100,
|
||||
});
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"'
|
||||
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
});
|
||||
@@ -74,27 +75,27 @@ describe("Mutator simple", async () => {
|
||||
limit: 1,
|
||||
sort: {
|
||||
by: "id",
|
||||
dir: "desc"
|
||||
}
|
||||
dir: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const id = query.data![0].id as number;
|
||||
const mutation = await em.mutator(items).deleteOne(id);
|
||||
|
||||
expect(mutation.sql).toBe(
|
||||
'delete from "items" where "id" = ? returning "id", "label", "count"'
|
||||
'delete from "items" where "id" = ? returning "id", "label", "count"',
|
||||
);
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
|
||||
const query2 = await em.repository(items).findId(id);
|
||||
expect(query2.result.length).toBe(0);
|
||||
expect(query2.data).toBeUndefined();
|
||||
});
|
||||
|
||||
test("validation: insert incomplete row", async () => {
|
||||
const incompleteCreate = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
//label: "test",
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(incompleteCreate()).rejects.toThrow();
|
||||
@@ -104,7 +105,7 @@ describe("Mutator simple", async () => {
|
||||
const invalidCreate1 = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
label: 111, // this should work
|
||||
count: "1" // this should fail
|
||||
count: "1", // this should fail
|
||||
});
|
||||
|
||||
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
|
||||
@@ -112,7 +113,7 @@ describe("Mutator simple", async () => {
|
||||
const invalidCreate2 = async () =>
|
||||
await em.mutator(items).insertOne({
|
||||
label: "", // this should fail
|
||||
count: 1
|
||||
count: 1,
|
||||
});
|
||||
|
||||
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
|
||||
@@ -132,14 +133,61 @@ describe("Mutator simple", async () => {
|
||||
const data = (await em.repository(items).findMany()).data;
|
||||
//console.log(data);
|
||||
|
||||
await em.mutator(items).deleteMany({ label: "delete" });
|
||||
await em.mutator(items).deleteWhere({ label: "delete" });
|
||||
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||
//console.log((await em.repository(items).findMany()).data);
|
||||
|
||||
await em.mutator(items).deleteMany();
|
||||
await em.mutator(items).deleteWhere({ id: { $isnull: 0 } });
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
||||
|
||||
//expect(res.data.count).toBe(0);
|
||||
});
|
||||
|
||||
test("updateMany", async () => {
|
||||
await em.mutator(items).insertOne({ label: "update", count: 1 });
|
||||
await em.mutator(items).insertOne({ label: "update too", count: 1 });
|
||||
await em.mutator(items).insertOne({ label: "keep" });
|
||||
|
||||
// expect no update
|
||||
await em.mutator(items).updateWhere(
|
||||
{ count: 2 },
|
||||
{
|
||||
count: 10,
|
||||
},
|
||||
);
|
||||
expect((await em.repository(items).findMany()).data).toEqual([
|
||||
{ id: 6, label: "update", count: 1 },
|
||||
{ id: 7, label: "update too", count: 1 },
|
||||
{ id: 8, label: "keep", count: 0 },
|
||||
]);
|
||||
|
||||
// expect 2 to be updated
|
||||
await em.mutator(items).updateWhere(
|
||||
{ count: 2 },
|
||||
{
|
||||
count: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect((await em.repository(items).findMany()).data).toEqual([
|
||||
{ id: 6, label: "update", count: 2 },
|
||||
{ id: 7, label: "update too", count: 2 },
|
||||
{ id: 8, label: "keep", count: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("insertMany", async () => {
|
||||
const oldCount = (await em.repo(items).count()).data.count;
|
||||
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||
const { data } = await em.mutator(items).insertMany(inserts);
|
||||
|
||||
expect(data.length).toBe(2);
|
||||
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
||||
const newCount = (await em.repo(items).count()).data.count;
|
||||
expect(newCount).toBe(oldCount + inserts.length);
|
||||
|
||||
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||
expect(data2).toEqual(data);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||
import { stripMark } from "../../src/core/utils";
|
||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { PolymorphicRelation } from "data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -23,23 +25,23 @@ describe("Polymorphic", async () => {
|
||||
source: "categories",
|
||||
target: "media",
|
||||
config: {
|
||||
mappedBy: "image"
|
||||
}
|
||||
mappedBy: "image",
|
||||
},
|
||||
});
|
||||
// media should not see categories
|
||||
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
||||
|
||||
// it's important that media cannot access categories
|
||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
||||
[]
|
||||
[],
|
||||
);
|
||||
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||
"media"
|
||||
"media",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||
"image"
|
||||
"image",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||
|
||||
@@ -48,7 +50,7 @@ describe("Polymorphic", async () => {
|
||||
"id",
|
||||
"path",
|
||||
"reference",
|
||||
"entity_id"
|
||||
"entity_id",
|
||||
]);
|
||||
expect(media.getSelect()).toEqual(["id", "path"]);
|
||||
});
|
||||
@@ -60,7 +62,7 @@ describe("Polymorphic", async () => {
|
||||
const entities = [media, categories];
|
||||
const single = new PolymorphicRelation(categories, media, {
|
||||
mappedBy: "single",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
});
|
||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||
|
||||
@@ -71,17 +73,17 @@ describe("Polymorphic", async () => {
|
||||
|
||||
// it's important that media cannot access categories
|
||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
||||
[]
|
||||
[],
|
||||
);
|
||||
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||
"media",
|
||||
"media"
|
||||
"media",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||
"single",
|
||||
"multiple"
|
||||
"multiple",
|
||||
]);
|
||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||
|
||||
@@ -90,7 +92,7 @@ describe("Polymorphic", async () => {
|
||||
"id",
|
||||
"path",
|
||||
"reference",
|
||||
"entity_id"
|
||||
"entity_id",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { MediaField } from "../../src";
|
||||
import {
|
||||
BooleanField,
|
||||
DateField,
|
||||
Entity,
|
||||
EnumField,
|
||||
JsonField,
|
||||
NumberField,
|
||||
TextField,
|
||||
EntityIndex,
|
||||
} from "data/fields";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField
|
||||
} from "../../src/data";
|
||||
} from "data/relations";
|
||||
import { DummyConnection } from "data/connection/DummyConnection";
|
||||
import {
|
||||
FieldPrototype,
|
||||
type FieldSchema,
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
boolean,
|
||||
date,
|
||||
datetime,
|
||||
em,
|
||||
entity,
|
||||
enumm,
|
||||
json,
|
||||
@@ -28,8 +32,9 @@ import {
|
||||
medium,
|
||||
number,
|
||||
relation,
|
||||
text
|
||||
} from "../../src/data/prototype";
|
||||
text,
|
||||
} from "data/prototype";
|
||||
import { MediaField } from "media/MediaField";
|
||||
|
||||
describe("prototype", () => {
|
||||
test("...", () => {
|
||||
@@ -46,12 +51,17 @@ describe("prototype", () => {
|
||||
});
|
||||
|
||||
test("...2", async () => {
|
||||
const user = entity("users", {
|
||||
name: text().required(),
|
||||
const users = entity("users", {
|
||||
name: text(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number().required()
|
||||
some: number(),
|
||||
});
|
||||
type db = {
|
||||
users: Schema<typeof users>;
|
||||
};
|
||||
|
||||
const obj: Schema<typeof users> = {} as any;
|
||||
|
||||
//console.log("user", user.toJSON());
|
||||
});
|
||||
@@ -61,7 +71,7 @@ describe("prototype", () => {
|
||||
name: text({ default_value: "hello" }).required(),
|
||||
bio: text(),
|
||||
age: number(),
|
||||
some: number().required()
|
||||
some: number().required(),
|
||||
});
|
||||
|
||||
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
|
||||
@@ -74,10 +84,12 @@ describe("prototype", () => {
|
||||
new TextField("title", { required: true }),
|
||||
new TextField("content"),
|
||||
new DateField("created_at", {
|
||||
type: "datetime"
|
||||
type: "datetime",
|
||||
}),
|
||||
// @ts-ignore
|
||||
new MediaField("images", { entity: "posts" }),
|
||||
new MediaField("cover", { entity: "posts", max_items: 1 })
|
||||
// @ts-ignore
|
||||
new MediaField("cover", { entity: "posts", max_items: 1 }),
|
||||
]);
|
||||
|
||||
const posts2 = entity("posts", {
|
||||
@@ -85,12 +97,13 @@ describe("prototype", () => {
|
||||
content: text(),
|
||||
created_at: datetime(),
|
||||
images: media(),
|
||||
cover: medium()
|
||||
cover: medium(),
|
||||
});
|
||||
|
||||
type Posts = Schema<typeof posts2>;
|
||||
|
||||
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
// @todo: check
|
||||
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
});
|
||||
|
||||
test("test example", async () => {
|
||||
@@ -106,11 +119,11 @@ describe("prototype", () => {
|
||||
type: "objects",
|
||||
values: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
}
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
new JsonField("json")
|
||||
new JsonField("json"),
|
||||
]);
|
||||
|
||||
const test2 = entity("test", {
|
||||
@@ -123,10 +136,10 @@ describe("prototype", () => {
|
||||
status: enumm<"active" | "inactive">({
|
||||
enum: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
}),
|
||||
json: json<{ some: number }>()
|
||||
json: json<{ some: number }>(),
|
||||
});
|
||||
|
||||
expect(test.toJSON()).toEqual(test2.toJSON());
|
||||
@@ -150,12 +163,12 @@ describe("prototype", () => {
|
||||
// category has single image
|
||||
new PolymorphicRelation(categories, _media, {
|
||||
mappedBy: "image",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
}),
|
||||
|
||||
// post has multiple images
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "images" }),
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 })
|
||||
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 }),
|
||||
];
|
||||
|
||||
const relations2 = [
|
||||
@@ -169,7 +182,7 @@ describe("prototype", () => {
|
||||
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
||||
|
||||
relation(posts).polyToMany(_media, { mappedBy: "images" }),
|
||||
relation(posts).polyToOne(_media, { mappedBy: "cover" })
|
||||
relation(posts).polyToOne(_media, { mappedBy: "cover" }),
|
||||
];
|
||||
|
||||
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
|
||||
@@ -183,21 +196,21 @@ describe("prototype", () => {
|
||||
posts,
|
||||
categories,
|
||||
{
|
||||
connectionTableMappedName: "custom"
|
||||
connectionTableMappedName: "custom",
|
||||
},
|
||||
[new TextField("description")]
|
||||
[new TextField("description")],
|
||||
);
|
||||
|
||||
const fields = {
|
||||
description: text()
|
||||
description: text(),
|
||||
};
|
||||
let o: FieldSchema<typeof fields>;
|
||||
const rel2 = relation(posts).manyToMany(
|
||||
categories,
|
||||
{
|
||||
connectionTableMappedName: "custom"
|
||||
connectionTableMappedName: "custom",
|
||||
},
|
||||
fields
|
||||
fields,
|
||||
);
|
||||
|
||||
expect(rel.toJSON()).toEqual(rel2.toJSON());
|
||||
@@ -205,11 +218,11 @@ describe("prototype", () => {
|
||||
|
||||
test("devexample", async () => {
|
||||
const users = entity("users", {
|
||||
username: text()
|
||||
username: text(),
|
||||
});
|
||||
|
||||
const comments = entity("comments", {
|
||||
content: text()
|
||||
content: text(),
|
||||
});
|
||||
|
||||
const posts = entity("posts", {
|
||||
@@ -217,17 +230,17 @@ describe("prototype", () => {
|
||||
content: text(),
|
||||
created_at: datetime(),
|
||||
images: media(),
|
||||
cover: medium()
|
||||
cover: medium(),
|
||||
});
|
||||
|
||||
const categories = entity("categories", {
|
||||
name: text(),
|
||||
description: text(),
|
||||
image: medium()
|
||||
image: medium(),
|
||||
});
|
||||
|
||||
const settings = entity("settings", {
|
||||
theme: text()
|
||||
theme: text(),
|
||||
});
|
||||
|
||||
const test = entity("test", {
|
||||
@@ -240,10 +253,10 @@ describe("prototype", () => {
|
||||
status: enumm<"active" | "inactive">({
|
||||
enum: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Not active" }
|
||||
]
|
||||
{ value: "inactive", label: "Not active" },
|
||||
],
|
||||
}),
|
||||
json: json<{ some: number }>()
|
||||
json: json<{ some: number }>(),
|
||||
});
|
||||
|
||||
const _media = entity("media", {});
|
||||
@@ -259,9 +272,43 @@ describe("prototype", () => {
|
||||
relation(users).oneToOne(settings),
|
||||
|
||||
relation(comments).manyToOne(users, { required: true }),
|
||||
relation(comments).manyToOne(posts, { required: true })
|
||||
relation(comments).manyToOne(posts, { required: true }),
|
||||
];
|
||||
|
||||
const obj: Schema<typeof test> = {} as any;
|
||||
});
|
||||
|
||||
test("schema", async () => {
|
||||
const _em = em(
|
||||
{
|
||||
posts: entity("posts", { name: text(), slug: text().required() }),
|
||||
comments: entity("comments", { some: text() }),
|
||||
users: entity("users", { email: text() }),
|
||||
},
|
||||
({ relation, index }, { posts, comments, users }) => {
|
||||
relation(posts).manyToOne(comments).manyToOne(users);
|
||||
index(posts).on(["name"]).on(["slug"], true);
|
||||
},
|
||||
);
|
||||
|
||||
type LocalDb = (typeof _em)["DB"];
|
||||
|
||||
const es = [
|
||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||
new Entity("comments", [new TextField("some")]),
|
||||
new Entity("users", [new TextField("email")]),
|
||||
] as const;
|
||||
const _em2 = new EntityManager(
|
||||
[...es],
|
||||
new DummyConnection(),
|
||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||
[
|
||||
new EntityIndex(es[0], [es[0].field("name")!]),
|
||||
new EntityIndex(es[0], [es[0].field("slug")!], true),
|
||||
],
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
expect(_em2.toJSON()).toEqual(_em.toJSON());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, TextField } from "../../src/data";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
RelationField
|
||||
} from "../../src/data/relations";
|
||||
RelationField,
|
||||
} from "data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -22,32 +23,32 @@ describe("Relations", async () => {
|
||||
const r1 = new RelationField("users_id", {
|
||||
reference: "users",
|
||||
target: "users",
|
||||
target_field: "id"
|
||||
target_field: "id",
|
||||
});
|
||||
|
||||
const sql1 = schema
|
||||
.createTable("posts")
|
||||
.addColumn(...r1.schema()!)
|
||||
.addColumn(...em.connection.getFieldSchema(r1.schema())!)
|
||||
.compile().sql;
|
||||
|
||||
expect(sql1).toBe(
|
||||
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)'
|
||||
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)',
|
||||
);
|
||||
|
||||
//const r2 = new RelationField(new Entity("users"), "author");
|
||||
const r2 = new RelationField("author_id", {
|
||||
reference: "author",
|
||||
target: "users",
|
||||
target_field: "id"
|
||||
target_field: "id",
|
||||
});
|
||||
|
||||
const sql2 = schema
|
||||
.createTable("posts")
|
||||
.addColumn(...r2.schema()!)
|
||||
.addColumn(...em.connection.getFieldSchema(r2.schema())!)
|
||||
.compile().sql;
|
||||
|
||||
expect(sql2).toBe(
|
||||
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)'
|
||||
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -57,7 +58,7 @@ describe("Relations", async () => {
|
||||
reference: "users",
|
||||
target: "users",
|
||||
target_field: "id",
|
||||
required: true
|
||||
required: true,
|
||||
});
|
||||
expect(r1.isRequired()).toBeTrue();
|
||||
});
|
||||
@@ -66,8 +67,8 @@ describe("Relations", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const posts = new Entity("posts", [
|
||||
new TextField("title", {
|
||||
maxLength: 2
|
||||
})
|
||||
maxLength: 2,
|
||||
}),
|
||||
]);
|
||||
|
||||
const entities = [users, posts];
|
||||
@@ -77,7 +78,7 @@ describe("Relations", async () => {
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// verify naming
|
||||
const rel = em.relations.all[0];
|
||||
const rel = em.relations.all[0]!;
|
||||
expect(rel.source.entity.name).toBe(posts.name);
|
||||
expect(rel.source.reference).toBe(posts.name);
|
||||
expect(rel.target.entity.name).toBe(users.name);
|
||||
@@ -89,11 +90,11 @@ describe("Relations", async () => {
|
||||
// verify low level relation
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
||||
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userPostsRel = em.relationOf(users.name, "posts");
|
||||
@@ -106,7 +107,6 @@ describe("Relations", async () => {
|
||||
expect(postAuthorRel?.other(posts).entity).toBe(users);
|
||||
|
||||
const kysely = em.connection.kysely;
|
||||
const jsonFrom = (e) => e;
|
||||
/**
|
||||
* Relation Helper
|
||||
*/
|
||||
@@ -119,14 +119,11 @@ describe("Relations", async () => {
|
||||
- select: users.*
|
||||
- cardinality: 1
|
||||
*/
|
||||
const selectPostsFromUsers = postAuthorRel.buildWith(
|
||||
users,
|
||||
kysely.selectFrom(users.name),
|
||||
jsonFrom,
|
||||
"posts"
|
||||
);
|
||||
const selectPostsFromUsers = kysely
|
||||
.selectFrom(users.name)
|
||||
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
|
||||
expect(selectPostsFromUsers.compile().sql).toBe(
|
||||
'select (select "posts"."id" as "id", "posts"."title" as "title", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" limit ?) as "posts" from "users"'
|
||||
'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"',
|
||||
);
|
||||
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
|
||||
const userObj = { id: 1, username: "test" };
|
||||
@@ -141,15 +138,12 @@ describe("Relations", async () => {
|
||||
- select: posts.*
|
||||
- cardinality:
|
||||
*/
|
||||
const selectUsersFromPosts = postAuthorRel.buildWith(
|
||||
posts,
|
||||
kysely.selectFrom(posts.name),
|
||||
jsonFrom,
|
||||
"author"
|
||||
);
|
||||
const selectUsersFromPosts = kysely
|
||||
.selectFrom(posts.name)
|
||||
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
|
||||
|
||||
expect(selectUsersFromPosts.compile().sql).toBe(
|
||||
'select (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"'
|
||||
'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"',
|
||||
);
|
||||
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
|
||||
const postObj = { id: 1, title: "test" };
|
||||
@@ -165,7 +159,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: undefined,
|
||||
relation_type: "n:1"
|
||||
relation_type: "n:1",
|
||||
});
|
||||
|
||||
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
@@ -177,7 +171,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: "id",
|
||||
cardinality: 1,
|
||||
relation_type: "n:1"
|
||||
relation_type: "n:1",
|
||||
});
|
||||
|
||||
/*console.log("ManyToOne (source=posts, target=users)");
|
||||
@@ -198,7 +192,7 @@ describe("Relations", async () => {
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// verify naming
|
||||
const rel = em.relations.all[0];
|
||||
const rel = em.relations.all[0]!;
|
||||
expect(rel.source.entity.name).toBe(users.name);
|
||||
expect(rel.source.reference).toBe(users.name);
|
||||
expect(rel.target.entity.name).toBe(settings.name);
|
||||
@@ -209,8 +203,8 @@ describe("Relations", async () => {
|
||||
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||
@@ -232,7 +226,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: "id",
|
||||
cardinality: 1,
|
||||
relation_type: "1:1"
|
||||
relation_type: "1:1",
|
||||
});
|
||||
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
|
||||
reference: "users",
|
||||
@@ -243,7 +237,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: 1,
|
||||
relation_type: "1:1"
|
||||
relation_type: "1:1",
|
||||
});
|
||||
|
||||
/*console.log("");
|
||||
@@ -315,26 +309,22 @@ describe("Relations", async () => {
|
||||
- select: users.*
|
||||
- cardinality: 1
|
||||
*/
|
||||
const selectCategoriesFromPosts = postCategoriesRel.buildWith(
|
||||
posts,
|
||||
kysely.selectFrom(posts.name),
|
||||
jsonFrom
|
||||
);
|
||||
const selectCategoriesFromPosts = kysely
|
||||
.selectFrom(posts.name)
|
||||
.select((eb) => postCategoriesRel.buildWith(posts)(eb).select("id").as("categories"));
|
||||
expect(selectCategoriesFromPosts.compile().sql).toBe(
|
||||
'select (select "categories"."id" as "id", "categories"."label" as "label" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"'
|
||||
'select (select "id" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"',
|
||||
);
|
||||
|
||||
const selectPostsFromCategories = postCategoriesRel.buildWith(
|
||||
categories,
|
||||
kysely.selectFrom(categories.name),
|
||||
jsonFrom
|
||||
);
|
||||
const selectPostsFromCategories = kysely
|
||||
.selectFrom(categories.name)
|
||||
.select((eb) => postCategoriesRel.buildWith(categories)(eb).select("id").as("posts"));
|
||||
expect(selectPostsFromCategories.compile().sql).toBe(
|
||||
'select (select "posts"."id" as "id", "posts"."title" as "title" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"'
|
||||
'select (select "id" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"',
|
||||
);
|
||||
|
||||
// mutation info
|
||||
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
reference: "categories",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
@@ -343,9 +333,9 @@ describe("Relations", async () => {
|
||||
$detach: true,
|
||||
primary: "id",
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n"
|
||||
relation_type: "m:n",
|
||||
});
|
||||
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
reference: "posts",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
@@ -354,7 +344,7 @@ describe("Relations", async () => {
|
||||
$detach: false,
|
||||
primary: undefined,
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n"
|
||||
relation_type: "m:n",
|
||||
});
|
||||
|
||||
/*console.log("");
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Entity, NumberField, TextField } from "../../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
|
||||
describe("[data] Entity", async () => {
|
||||
const entity = new Entity("test", [
|
||||
new TextField("name", { required: true }),
|
||||
new TextField("description"),
|
||||
new NumberField("age", { fillable: false, default_value: 18 }),
|
||||
new TextField("hidden", { hidden: true, default_value: "secret" })
|
||||
new TextField("hidden", { hidden: true, default_value: "secret" }),
|
||||
]);
|
||||
|
||||
test("getSelect", async () => {
|
||||
@@ -17,7 +18,7 @@ describe("[data] Entity", async () => {
|
||||
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
|
||||
"name",
|
||||
"description",
|
||||
"hidden"
|
||||
"hidden",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,7 +29,7 @@ describe("[data] Entity", async () => {
|
||||
test("getDefaultObject", async () => {
|
||||
expect(entity.getDefaultObject()).toEqual({
|
||||
age: 18,
|
||||
hidden: "secret"
|
||||
hidden: "secret",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,15 +47,4 @@ describe("[data] Entity", async () => {
|
||||
entity.addField(field);
|
||||
expect(entity.getField("new_field")).toBe(field);
|
||||
});
|
||||
|
||||
// @todo: move this to ClientApp
|
||||
/*test("serialize and deserialize", async () => {
|
||||
const json = entity.toJSON();
|
||||
//sconsole.log("json", json.fields);
|
||||
const newEntity = Entity.deserialize(json);
|
||||
//console.log("newEntity", newEntity.toJSON().fields);
|
||||
expect(newEntity).toBeInstanceOf(Entity);
|
||||
expect(json).toEqual(newEntity.toJSON());
|
||||
expect(json.fields).toEqual(newEntity.toJSON().fields);
|
||||
});*/
|
||||
});
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
SchemaManager
|
||||
} from "../../../src/data";
|
||||
import { UnableToConnectException } from "../../../src/data/errors";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { UnableToConnectException } from "data/errors";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -25,7 +21,7 @@ describe("[data] EntityManager", async () => {
|
||||
expect(await em.ping()).toBe(true);
|
||||
expect(() => em.entity("...")).toThrow();
|
||||
expect(() =>
|
||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2")))
|
||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
|
||||
).toThrow();
|
||||
expect(em.schema()).toBeInstanceOf(SchemaManager);
|
||||
|
||||
@@ -47,8 +43,8 @@ describe("[data] EntityManager", async () => {
|
||||
em.addRelation(new ManyToOneRelation(posts, users));
|
||||
expect(em.relations.all.length).toBe(1);
|
||||
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
|
||||
expect(em.relationsOf("users")).toEqual([em.relations.all[0]]);
|
||||
expect(em.relationsOf("posts")).toEqual([em.relations.all[0]]);
|
||||
expect(em.relationsOf("users")).toEqual([em.relations.all[0]!]);
|
||||
expect(em.relationsOf("posts")).toEqual([em.relations.all[0]!]);
|
||||
expect(em.hasRelations("users")).toBe(true);
|
||||
expect(em.hasRelations("posts")).toBe(true);
|
||||
expect(em.relatedEntitiesOf("users")).toEqual([posts]);
|
||||
@@ -98,7 +94,7 @@ describe("[data] EntityManager", async () => {
|
||||
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
|
||||
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
|
||||
"comments",
|
||||
"categories"
|
||||
"categories",
|
||||
]);
|
||||
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
|
||||
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
||||
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { TextField } from "data/fields";
|
||||
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("[data] JoinBuilder", async () => {
|
||||
test("missing relation", async () => {
|
||||
@@ -12,7 +17,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
const em = new EntityManager([users], dummyConnection);
|
||||
|
||||
expect(() =>
|
||||
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"])
|
||||
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]),
|
||||
).toThrow('Relation "posts" not found');
|
||||
});
|
||||
|
||||
@@ -23,7 +28,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
|
||||
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
||||
"posts"
|
||||
"posts",
|
||||
]);
|
||||
|
||||
const res = qb.compile();
|
||||
@@ -34,7 +39,7 @@ describe("[data] JoinBuilder", async () => {
|
||||
);*/
|
||||
|
||||
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
|
||||
"author"
|
||||
"author",
|
||||
]);
|
||||
|
||||
const res2 = qb2.compile();
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import type { EventManager } from "../../../src/core/events";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
MutatorEvents,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
type RelationField,
|
||||
RelationField,
|
||||
RelationMutator,
|
||||
TextField
|
||||
} from "../../../src/data";
|
||||
import { getDummyConnection } from "../helper";
|
||||
} from "data/relations";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import * as proto from "data/prototype";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { MutatorEvents } from "data/events";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("[data] Mutator (base)", async () => {
|
||||
const entity = new Entity("items", [
|
||||
new TextField("label", { required: true }),
|
||||
new NumberField("count"),
|
||||
new TextField("hidden", { hidden: true }),
|
||||
new TextField("not_fillable", { fillable: false })
|
||||
new TextField("not_fillable", { fillable: false }),
|
||||
]);
|
||||
const em = new EntityManager([entity], dummyConnection);
|
||||
const em = new EntityManager<any>([entity], dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = { label: "item 1", count: 1 };
|
||||
@@ -42,7 +46,7 @@ describe("[data] Mutator (base)", async () => {
|
||||
test("updateOne", async () => {
|
||||
const { data } = await em.mutator(entity).insertOne(payload);
|
||||
const updated = await em.mutator(entity).updateOne(data.id, {
|
||||
count: 2
|
||||
count: 2,
|
||||
});
|
||||
|
||||
expect(updated.parameters).toEqual([2, data.id]);
|
||||
@@ -61,7 +65,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const posts = new Entity("posts", [new TextField("title")]);
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const relations = [new ManyToOneRelation(posts, users)];
|
||||
const em = new EntityManager([posts, users], dummyConnection, relations);
|
||||
const em = new EntityManager<any>([posts, users], dummyConnection, relations);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
test("RelationMutator", async () => {
|
||||
@@ -75,7 +79,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
|
||||
// persisting relational field should just return key value to be added
|
||||
expect(
|
||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id)
|
||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
|
||||
).resolves.toEqual(["users_id", userData.data.id]);
|
||||
|
||||
// persisting invalid value should throw
|
||||
@@ -83,14 +87,12 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
|
||||
// persisting reference should ...
|
||||
expect(
|
||||
postRelMutator.persistReference(relations[0], "users", {
|
||||
$set: { id: userData.data.id }
|
||||
})
|
||||
postRelMutator.persistReference(relations[0]!, "users", {
|
||||
$set: { id: userData.data.id },
|
||||
}),
|
||||
).resolves.toEqual(["users_id", userData.data.id]);
|
||||
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
|
||||
|
||||
process.exit(0);
|
||||
|
||||
const userRelMutator = new RelationMutator(users, em);
|
||||
expect(userRelMutator.getRelationalKeys()).toEqual(["posts"]);
|
||||
});
|
||||
@@ -99,8 +101,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: 1 // user does not exist yet
|
||||
})
|
||||
users_id: 100, // user does not exist yet
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -111,7 +113,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const em = new EntityManager([items, cats], dummyConnection, relations);
|
||||
|
||||
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
|
||||
'Field "cats_id" is required'
|
||||
'Field "cats_id" is required',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -119,14 +121,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: data.id
|
||||
users_id: data.id,
|
||||
});
|
||||
expect(res.data.users_id).toBe(data.id);
|
||||
|
||||
// setting "null" should be allowed
|
||||
const res2 = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users_id: null
|
||||
users_id: null,
|
||||
});
|
||||
expect(res2.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -135,14 +137,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||
const res = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users: { $set: { id: data.id } }
|
||||
users: { $set: { id: data.id } },
|
||||
});
|
||||
expect(res.data.users_id).toBe(data.id);
|
||||
|
||||
// setting "null" should be allowed
|
||||
const res2 = await em.mutator(posts).insertOne({
|
||||
title: "post1",
|
||||
users: { $set: { id: null } }
|
||||
users: { $set: { id: null } },
|
||||
});
|
||||
expect(res2.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -151,8 +153,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(posts).insertOne({
|
||||
title: "test",
|
||||
users: { $create: { username: "test" } }
|
||||
})
|
||||
users: { $create: { username: "test" } },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -162,27 +164,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
||||
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
|
||||
|
||||
const up1 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users: { $set: { id: res1.data.id } }
|
||||
users: { $set: { id: res1.data.id } },
|
||||
});
|
||||
expect(up1.data.users_id).toBe(res1.data.id);
|
||||
|
||||
const up2 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users: { $set: { id: res1_1.data.id } }
|
||||
users: { $set: { id: res1_1.data.id } },
|
||||
});
|
||||
expect(up2.data.users_id).toBe(res1_1.data.id);
|
||||
|
||||
const up3_1 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: res1.data.id
|
||||
users_id: res1.data.id,
|
||||
});
|
||||
expect(up3_1.data.users_id).toBe(res1.data.id);
|
||||
|
||||
const up3_2 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: res1_1.data.id
|
||||
users_id: res1_1.data.id,
|
||||
});
|
||||
expect(up3_2.data.users_id).toBe(res1_1.data.id);
|
||||
|
||||
const up4 = await em.mutator(posts).updateOne(res2.data.id, {
|
||||
users_id: null
|
||||
users_id: null,
|
||||
});
|
||||
expect(up4.data.users_id).toBe(null);
|
||||
});
|
||||
@@ -192,15 +194,15 @@ describe("[data] Mutator (OneToOne)", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const settings = new Entity("settings", [new TextField("theme")]);
|
||||
const relations = [new OneToOneRelation(users, settings)];
|
||||
const em = new EntityManager([users, settings], dummyConnection, relations);
|
||||
const em = new EntityManager<any>([users, settings], dummyConnection, relations);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
test("insertOne: missing ref", async () => {
|
||||
expect(
|
||||
em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings_id: 1 // todo: throws because it doesn't exist, but it shouldn't be allowed
|
||||
})
|
||||
settings_id: 1, // todo: throws because it doesn't exist, but it shouldn't be allowed
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -210,15 +212,15 @@ describe("[data] Mutator (OneToOne)", async () => {
|
||||
expect(
|
||||
em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings: { $set: { id: data.id } }
|
||||
})
|
||||
settings: { $set: { id: data.id } },
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("insertOne: using $create", async () => {
|
||||
const res = await em.mutator(users).insertOne({
|
||||
username: "test",
|
||||
settings: { $create: { theme: "dark" } }
|
||||
settings: { $create: { theme: "dark" } },
|
||||
});
|
||||
expect(res.data.settings_id).toBeDefined();
|
||||
});
|
||||
@@ -276,7 +278,7 @@ describe("[data] Mutator (ManyToMany)", async () => {
|
||||
|
||||
describe("[data] Mutator (Events)", async () => {
|
||||
const entity = new Entity("test", [new TextField("label")]);
|
||||
const em = new EntityManager([entity], dummyConnection);
|
||||
const em = new EntityManager<any>([entity], dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
const events = new Map<string, any>();
|
||||
|
||||
@@ -288,15 +290,85 @@ describe("[data] Mutator (Events)", async () => {
|
||||
|
||||
test("events were fired", async () => {
|
||||
const { data } = await mutator.insertOne({ label: "test" });
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
||||
|
||||
await mutator.updateOne(data.id, { label: "test2" });
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
||||
|
||||
await mutator.deleteOne(data.id);
|
||||
await mutator.emgr.executeAsyncs();
|
||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||
});
|
||||
|
||||
test("insertOne event return is respected", async () => {
|
||||
const posts = proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
views: proto.number(),
|
||||
});
|
||||
|
||||
const conn = getDummyConnection();
|
||||
const em = new EntityManager([posts], conn.dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const emgr = em.emgr as EventManager<any>;
|
||||
|
||||
emgr.onEvent(
|
||||
// @ts-ignore
|
||||
EntityManager.Events.MutatorInsertBefore,
|
||||
async (event) => {
|
||||
return {
|
||||
...event.params.data,
|
||||
views: 2,
|
||||
};
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
|
||||
const mutator = em.mutator("posts");
|
||||
const result = await mutator.insertOne({ title: "test", views: 1 });
|
||||
expect(result.data).toEqual({
|
||||
id: 1,
|
||||
title: "test",
|
||||
views: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("updateOne event return is respected", async () => {
|
||||
const posts = proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
views: proto.number(),
|
||||
});
|
||||
|
||||
const conn = getDummyConnection();
|
||||
const em = new EntityManager([posts], conn.dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const emgr = em.emgr as EventManager<any>;
|
||||
|
||||
emgr.onEvent(
|
||||
// @ts-ignore
|
||||
EntityManager.Events.MutatorUpdateBefore,
|
||||
async (event) => {
|
||||
return {
|
||||
...event.params.data,
|
||||
views: event.params.data.views + 1,
|
||||
};
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
|
||||
const mutator = em.mutator("posts");
|
||||
const created = await mutator.insertOne({ title: "test", views: 1 });
|
||||
const result = await mutator.updateOne(created.data.id, { views: 2 });
|
||||
expect(result.data).toEqual({
|
||||
id: 1,
|
||||
title: "test",
|
||||
views: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
// @ts-ignore
|
||||
import { Perf } from "@bknd/core/utils";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import type { Kysely, Transaction } from "kysely";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
LibsqlConnection,
|
||||
ManyToOneRelation,
|
||||
RepositoryEvents,
|
||||
TextField
|
||||
} from "../../../src/data";
|
||||
import { TextField } from "data/fields";
|
||||
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { RepositoryEvents } from "data/events";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
type E = Kysely<any> | Transaction<any>;
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -24,120 +21,6 @@ async function sleep(ms: number) {
|
||||
}
|
||||
|
||||
describe("[Repository]", async () => {
|
||||
test("bulk", async () => {
|
||||
//const connection = dummyConnection;
|
||||
//const connection = getLocalLibsqlConnection();
|
||||
const credentials = null as any; // @todo: determine what to do here
|
||||
const connection = new LibsqlConnection(credentials);
|
||||
|
||||
const em = new EntityManager([], connection);
|
||||
/*const emLibsql = new EntityManager([], {
|
||||
url: connection.url.replace("https", "libsql"),
|
||||
authToken: connection.authToken,
|
||||
});*/
|
||||
const table = "posts";
|
||||
|
||||
const client = connection.getClient();
|
||||
if (!client) {
|
||||
console.log("Cannot perform test without libsql connection");
|
||||
return;
|
||||
}
|
||||
|
||||
const conn = em.connection.kysely;
|
||||
const selectQ = (e: E) => e.selectFrom(table).selectAll().limit(2);
|
||||
const countQ = (e: E) => e.selectFrom(table).select(e.fn.count("*").as("count"));
|
||||
|
||||
async function executeTransaction(em: EntityManager<any>) {
|
||||
return await em.connection.kysely.transaction().execute(async (e) => {
|
||||
const res = await selectQ(e).execute();
|
||||
const count = await countQ(e).execute();
|
||||
|
||||
return [res, count];
|
||||
});
|
||||
}
|
||||
|
||||
async function executeBatch(em: EntityManager<any>) {
|
||||
const queries = [selectQ(conn), countQ(conn)];
|
||||
return await em.connection.batchQuery(queries);
|
||||
}
|
||||
|
||||
async function executeSingleKysely(em: EntityManager<any>) {
|
||||
const res = await selectQ(conn).execute();
|
||||
const count = await countQ(conn).execute();
|
||||
return [res, count];
|
||||
}
|
||||
|
||||
async function executeSingleClient(em: EntityManager<any>) {
|
||||
const q1 = selectQ(conn).compile();
|
||||
const res = await client.execute({
|
||||
sql: q1.sql,
|
||||
args: q1.parameters as any
|
||||
});
|
||||
|
||||
const q2 = countQ(conn).compile();
|
||||
const count = await client.execute({
|
||||
sql: q2.sql,
|
||||
args: q2.parameters as any
|
||||
});
|
||||
return [res, count];
|
||||
}
|
||||
|
||||
const transaction = await executeTransaction(em);
|
||||
const batch = await executeBatch(em);
|
||||
|
||||
expect(batch).toEqual(transaction as any);
|
||||
|
||||
const testperf = false;
|
||||
if (testperf) {
|
||||
const times = 5;
|
||||
|
||||
const exec = async (
|
||||
name: string,
|
||||
fn: (em: EntityManager<any>) => Promise<any>,
|
||||
em: EntityManager<any>
|
||||
) => {
|
||||
const res = await Perf.execute(() => fn(em), times);
|
||||
await sleep(1000);
|
||||
const info = {
|
||||
name,
|
||||
total: res.total.toFixed(2),
|
||||
avg: (res.total / times).toFixed(2),
|
||||
first: res.marks[0].time.toFixed(2),
|
||||
last: res.marks[res.marks.length - 1].time.toFixed(2)
|
||||
};
|
||||
console.log(info.name, info, res.marks);
|
||||
return info;
|
||||
};
|
||||
|
||||
const data: any[] = [];
|
||||
data.push(await exec("transaction.http", executeTransaction, em));
|
||||
data.push(await exec("bulk.http", executeBatch, em));
|
||||
data.push(await exec("singleKy.http", executeSingleKysely, em));
|
||||
data.push(await exec("singleCl.http", executeSingleClient, em));
|
||||
|
||||
/*data.push(await exec("transaction.libsql", executeTransaction, emLibsql));
|
||||
data.push(await exec("bulk.libsql", executeBatch, emLibsql));
|
||||
data.push(await exec("singleKy.libsql", executeSingleKysely, emLibsql));
|
||||
data.push(await exec("singleCl.libsql", executeSingleClient, emLibsql));*/
|
||||
|
||||
console.table(data);
|
||||
/**
|
||||
* ┌───┬────────────────────┬────────┬────────┬────────┬────────┐
|
||||
* │ │ name │ total │ avg │ first │ last │
|
||||
* ├───┼────────────────────┼────────┼────────┼────────┼────────┤
|
||||
* │ 0 │ transaction.http │ 681.29 │ 136.26 │ 136.46 │ 396.09 │
|
||||
* │ 1 │ bulk.http │ 164.82 │ 32.96 │ 32.95 │ 99.91 │
|
||||
* │ 2 │ singleKy.http │ 330.01 │ 66.00 │ 65.86 │ 195.41 │
|
||||
* │ 3 │ singleCl.http │ 326.17 │ 65.23 │ 61.32 │ 198.08 │
|
||||
* │ 4 │ transaction.libsql │ 856.79 │ 171.36 │ 132.31 │ 595.24 │
|
||||
* │ 5 │ bulk.libsql │ 180.63 │ 36.13 │ 35.39 │ 107.71 │
|
||||
* │ 6 │ singleKy.libsql │ 347.11 │ 69.42 │ 65.00 │ 207.14 │
|
||||
* │ 7 │ singleCl.libsql │ 328.60 │ 65.72 │ 62.19 │ 195.04 │
|
||||
* └───┴────────────────────┴────────┴────────┴────────┴────────┘
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
test("count & exists", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const em = new EntityManager([items], dummyConnection);
|
||||
@@ -158,25 +41,88 @@ describe("[Repository]", async () => {
|
||||
// count all
|
||||
const res = await em.repository(items).count();
|
||||
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
||||
expect(res.count).toBe(3);
|
||||
expect(res.data.count).toBe(3);
|
||||
|
||||
//
|
||||
{
|
||||
const res = await em.repository(items).findMany();
|
||||
expect(res.count).toBe(3);
|
||||
}
|
||||
|
||||
{
|
||||
const res = await em
|
||||
.repository(items, {
|
||||
includeCounts: true,
|
||||
})
|
||||
.findMany();
|
||||
expect(res.count).toBe(3);
|
||||
}
|
||||
|
||||
// count filtered
|
||||
const res2 = await em.repository(items).count({ label: { $in: ["a", "b"] } });
|
||||
const res2 = await em
|
||||
.repository(items, {
|
||||
includeCounts: true,
|
||||
})
|
||||
.count({ label: { $in: ["a", "b"] } });
|
||||
|
||||
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
||||
expect(res2.parameters).toEqual(["a", "b"]);
|
||||
expect(res2.count).toBe(2);
|
||||
expect(res2.data.count).toBe(2);
|
||||
|
||||
// check exists
|
||||
const res3 = await em.repository(items).exists({ label: "a" });
|
||||
expect(res3.exists).toBe(true);
|
||||
expect(res3.data.exists).toBe(true);
|
||||
|
||||
const res4 = await em.repository(items).exists({ label: "d" });
|
||||
expect(res4.exists).toBe(false);
|
||||
expect(res4.data.exists).toBe(false);
|
||||
|
||||
// for now, allow empty filter
|
||||
const res5 = await em.repository(items).exists({});
|
||||
expect(res5.exists).toBe(true);
|
||||
expect(res5.data.exists).toBe(true);
|
||||
});
|
||||
|
||||
test("option: silent", async () => {
|
||||
const em = $em({
|
||||
items: $entity("items", {
|
||||
label: $text(),
|
||||
}),
|
||||
}).proto.withConnection(getDummyConnection().dummyConnection);
|
||||
|
||||
// should throw because table doesn't exist
|
||||
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
||||
// should silently return empty result
|
||||
em.repo("items", { silent: true })
|
||||
.findMany({})
|
||||
.then((r) => r.data);
|
||||
expect(
|
||||
em
|
||||
.repo("items", { silent: true })
|
||||
.findMany({})
|
||||
.then((r) => r.data),
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
test("option: includeCounts", async () => {
|
||||
const em = $em({
|
||||
items: $entity("items", {
|
||||
label: $text(),
|
||||
}),
|
||||
}).proto.withConnection(getDummyConnection().dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
expect(
|
||||
em
|
||||
.repo("items", { includeCounts: true })
|
||||
.findMany({})
|
||||
.then((r) => [r.count, r.total]),
|
||||
).resolves.toEqual([0, 0]);
|
||||
|
||||
expect(
|
||||
em
|
||||
.repo("items", { includeCounts: false })
|
||||
.findMany({})
|
||||
.then((r) => [r.count, r.total]),
|
||||
).resolves.toEqual([undefined, undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,7 +130,7 @@ describe("[data] Repository (Events)", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const categories = new Entity("categories", [new TextField("label")]);
|
||||
const em = new EntityManager([items, categories], dummyConnection, [
|
||||
new ManyToOneRelation(categories, items)
|
||||
new ManyToOneRelation(categories, items),
|
||||
]);
|
||||
await em.schema().sync({ force: true });
|
||||
const events = new Map<string, any>();
|
||||
@@ -199,24 +145,36 @@ describe("[data] Repository (Events)", async () => {
|
||||
});
|
||||
|
||||
test("events were fired", async () => {
|
||||
await em.repository(items).findId(1);
|
||||
const repo = em.repository(items);
|
||||
await repo.findId(1);
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findOne({ id: 1 });
|
||||
await repo.findOne({ id: 1 });
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findMany({ where: { id: 1 } });
|
||||
await repo.findMany({ where: { id: 1 } });
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
await em.repository(items).findManyByReference(1, "categories");
|
||||
await repo.findManyByReference(1, "categories");
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
|
||||
// check find one on findMany with limit 1
|
||||
await repo.findMany({ where: { id: 1 }, limit: 1 });
|
||||
await repo.emgr.executeAsyncs();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||
events.clear();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
||||
import { afterAll, describe, expect, spyOn, test } from "bun:test";
|
||||
import { randomString } from "core/utils";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, EntityIndex } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -15,7 +17,7 @@ describe("SchemaManager tests", async () => {
|
||||
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
||||
const schema = new SchemaManager(em);
|
||||
|
||||
const introspection = schema.getIntrospectionFromEntity(em.entities[0]);
|
||||
const introspection = schema.getIntrospectionFromEntity(em.entities[0]!);
|
||||
expect(introspection).toEqual({
|
||||
name: "test",
|
||||
isView: false,
|
||||
@@ -26,7 +28,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
@@ -34,7 +36,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
@@ -42,7 +44,7 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "bio",
|
||||
@@ -50,8 +52,8 @@ describe("SchemaManager tests", async () => {
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined
|
||||
}
|
||||
comment: undefined,
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
@@ -61,11 +63,11 @@ describe("SchemaManager tests", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "email",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,10 +79,10 @@ describe("SchemaManager tests", async () => {
|
||||
new Entity(table, [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("bio")
|
||||
])
|
||||
new TextField("bio"),
|
||||
]),
|
||||
],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -101,15 +103,15 @@ describe("SchemaManager tests", async () => {
|
||||
name: table,
|
||||
isNew: false,
|
||||
columns: { add: ["bio"], drop: [], change: [] },
|
||||
indices: { add: [], drop: [index] }
|
||||
}
|
||||
indices: { add: [], drop: [index] },
|
||||
},
|
||||
]);
|
||||
|
||||
// now sync
|
||||
await schema.sync({ force: true, drop: true });
|
||||
const diffAfter = await schema.getDiff();
|
||||
|
||||
console.log("diffAfter", diffAfter);
|
||||
//console.log("diffAfter", diffAfter);
|
||||
expect(diffAfter.length).toBe(0);
|
||||
|
||||
await kysely.schema.dropTable(table).execute();
|
||||
@@ -119,7 +121,7 @@ describe("SchemaManager tests", async () => {
|
||||
const table = "drop_column";
|
||||
const em = new EntityManager(
|
||||
[new Entity(table, [new TextField("username")])],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -141,10 +143,10 @@ describe("SchemaManager tests", async () => {
|
||||
columns: {
|
||||
add: [],
|
||||
drop: ["email"],
|
||||
change: []
|
||||
change: [],
|
||||
},
|
||||
indices: { add: [], drop: [] }
|
||||
}
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// now sync
|
||||
@@ -165,15 +167,15 @@ describe("SchemaManager tests", async () => {
|
||||
new Entity(usersTable, [
|
||||
new TextField("username"),
|
||||
new TextField("email"),
|
||||
new TextField("bio")
|
||||
new TextField("bio"),
|
||||
]),
|
||||
new Entity(postsTable, [
|
||||
new TextField("title"),
|
||||
new TextField("content"),
|
||||
new TextField("created_at")
|
||||
])
|
||||
new TextField("created_at"),
|
||||
]),
|
||||
],
|
||||
dummyConnection
|
||||
dummyConnection,
|
||||
);
|
||||
const kysely = em.connection.kysely;
|
||||
|
||||
@@ -192,7 +194,7 @@ describe("SchemaManager tests", async () => {
|
||||
name: usersTable,
|
||||
isNew: false,
|
||||
columns: { add: ["bio"], drop: [], change: [] },
|
||||
indices: { add: [], drop: [] }
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
{
|
||||
name: postsTable,
|
||||
@@ -200,10 +202,10 @@ describe("SchemaManager tests", async () => {
|
||||
columns: {
|
||||
add: ["id", "title", "content", "created_at"],
|
||||
drop: [],
|
||||
change: []
|
||||
change: [],
|
||||
},
|
||||
indices: { add: [], drop: [] }
|
||||
}
|
||||
indices: { add: [], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// now sync
|
||||
@@ -228,8 +230,8 @@ describe("SchemaManager tests", async () => {
|
||||
name: entity.name,
|
||||
isNew: true,
|
||||
columns: { add: ["id", "email"], drop: [], change: [] },
|
||||
indices: { add: [index.name!], drop: [] }
|
||||
}
|
||||
indices: { add: [index.name!], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// sync and then check again
|
||||
@@ -256,8 +258,8 @@ describe("SchemaManager tests", async () => {
|
||||
name: entity.name,
|
||||
isNew: false,
|
||||
columns: { add: [], drop: [], change: [] },
|
||||
indices: { add: [index.name!], drop: [] }
|
||||
}
|
||||
indices: { add: [index.name!], drop: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
// sync and then check again
|
||||
@@ -266,4 +268,39 @@ describe("SchemaManager tests", async () => {
|
||||
const diffAfter = await em.schema().getDiff();
|
||||
expect(diffAfter.length).toBe(0);
|
||||
});
|
||||
|
||||
test("returns statements", async () => {
|
||||
const amount = 5;
|
||||
const entities = new Array(amount)
|
||||
.fill(0)
|
||||
.map(() => new Entity(randomString(16), [new TextField("text")]));
|
||||
const em = new EntityManager(entities, dummyConnection);
|
||||
const statements = await em.schema().sync({ force: true });
|
||||
expect(statements.length).toBe(amount);
|
||||
expect(statements.every((stmt) => Object.keys(stmt).join(",") === "sql,parameters")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("batches statements", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
const entities = new Array(20)
|
||||
.fill(0)
|
||||
.map(() => new Entity(randomString(16), [new TextField("text")]));
|
||||
const em = new EntityManager(entities, dummyConnection);
|
||||
const spy = spyOn(em.connection, "executeQueries");
|
||||
const statements = await em.schema().sync();
|
||||
expect(statements.length).toBe(entities.length);
|
||||
expect(statements.every((stmt) => Object.keys(stmt).join(",") === "sql,parameters")).toBe(
|
||||
true,
|
||||
);
|
||||
await em.schema().sync({ force: true });
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const tables = await em.connection.kysely
|
||||
.selectFrom("sqlite_master")
|
||||
.where("type", "=", "table")
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(tables.length).toBe(entities.length + 1); /* 1+ for sqlite_sequence */
|
||||
});
|
||||
});
|
||||
|
||||
+19
-21
@@ -1,20 +1,18 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { QueryObject } from "ufo";
|
||||
import { WhereBuilder, type WhereQuery } from "../../src/data/entities/query/WhereBuilder";
|
||||
import { getDummyConnection } from "./helper";
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
||||
|
||||
const t = "t";
|
||||
describe("data-query-impl", () => {
|
||||
function qb() {
|
||||
const c = getDummyConnection();
|
||||
const kysely = c.dummyConnection.kysely;
|
||||
return kysely.selectFrom(t).selectAll();
|
||||
}
|
||||
function compile(q: QueryObject) {
|
||||
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
|
||||
return { sql, parameters };
|
||||
}
|
||||
function qb() {
|
||||
const c = getDummyConnection();
|
||||
const kysely = c.dummyConnection.kysely;
|
||||
return kysely.selectFrom("t").selectAll();
|
||||
}
|
||||
function compile(q: WhereQuery) {
|
||||
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
|
||||
return { sql, parameters };
|
||||
}
|
||||
|
||||
describe("WhereBuilder", () => {
|
||||
test("single validation", () => {
|
||||
const tests: [WhereQuery, string, any[]][] = [
|
||||
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
||||
@@ -25,7 +23,7 @@ describe("data-query-impl", () => {
|
||||
[{ val: { $isnull: 0 } }, '"val" is not null', []],
|
||||
[{ val: { $isnull: false } }, '"val" is not null', []],
|
||||
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
|
||||
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]]
|
||||
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]],
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
@@ -46,22 +44,22 @@ describe("data-query-impl", () => {
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
["foo", "bar"],
|
||||
],
|
||||
[
|
||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||
'("val1" = ? and "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
["foo", "bar"],
|
||||
],
|
||||
|
||||
// or constructs
|
||||
[
|
||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||
'("val1" = ? or "val2" = ?)',
|
||||
["foo", "bar"]
|
||||
["foo", "bar"],
|
||||
],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]]
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
|
||||
];
|
||||
|
||||
for (const [query, expectedSql, expectedParams] of tests) {
|
||||
@@ -81,7 +79,7 @@ describe("data-query-impl", () => {
|
||||
|
||||
// or constructs
|
||||
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]]
|
||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]],
|
||||
];
|
||||
|
||||
for (const [query, expectedKeys] of tests) {
|
||||
@@ -1,26 +1,62 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField,
|
||||
WithBuilder
|
||||
} from "../../../src/data";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
||||
import { TextField } from "data/fields";
|
||||
import * as proto from "data/prototype";
|
||||
import { WithBuilder } from "data/entities/query/WithBuilder";
|
||||
import { schemaToEm } from "../../helper";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
describe("[data] WithBuilder", async () => {
|
||||
test("validate withs", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {}),
|
||||
users: proto.entity("users", {}),
|
||||
media: proto.entity("media", {}),
|
||||
},
|
||||
({ relation }, { posts, users, media }) => {
|
||||
relation(posts).manyToOne(users);
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
expect(WithBuilder.validateWiths(em, "posts", undefined as any)).toBe(0);
|
||||
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
|
||||
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
||||
expect(
|
||||
WithBuilder.validateWiths(em, "posts", {
|
||||
users: {
|
||||
with: { avatar: {} },
|
||||
},
|
||||
}),
|
||||
).toBe(2);
|
||||
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
|
||||
expect(() =>
|
||||
WithBuilder.validateWiths(em, "posts", {
|
||||
users: {
|
||||
with: { glibberish: {} },
|
||||
},
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("missing relation", async () => {
|
||||
const users = new Entity("users", [new TextField("username")]);
|
||||
const em = new EntityManager([users], dummyConnection);
|
||||
|
||||
expect(() =>
|
||||
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"])
|
||||
).toThrow('Relation "posts" not found');
|
||||
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||
posts: {},
|
||||
}),
|
||||
).toThrow('Relation "users<>posts" not found');
|
||||
});
|
||||
|
||||
test("addClause: ManyToOne", async () => {
|
||||
@@ -29,36 +65,39 @@ describe("[data] WithBuilder", async () => {
|
||||
const relations = [new ManyToOneRelation(posts, users, { mappedBy: "author" })];
|
||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||
|
||||
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
||||
"posts"
|
||||
]);
|
||||
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||
posts: {},
|
||||
});
|
||||
|
||||
const res = qb.compile();
|
||||
|
||||
expect(res.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" where "users"."id" = "posts"."author_id" limit ?) as agg) as "posts" from "users"'
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"',
|
||||
);
|
||||
expect(res.parameters).toEqual([5]);
|
||||
expect(res.parameters).toEqual([10, 0]);
|
||||
|
||||
const qb2 = WithBuilder.addClause(
|
||||
em,
|
||||
em.connection.kysely.selectFrom("posts"),
|
||||
posts, // @todo: try with "users", it gives output!
|
||||
["author"]
|
||||
{
|
||||
author: {},
|
||||
},
|
||||
);
|
||||
|
||||
const res2 = qb2.compile();
|
||||
|
||||
expect(res2.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" where "posts"."author_id" = "users"."id" limit ?) as obj) as "author" from "posts"'
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ?) as obj) as "author" from "posts"',
|
||||
);
|
||||
expect(res2.parameters).toEqual([1]);
|
||||
});
|
||||
|
||||
test("test with empty join", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const qb = { qb: 1 } as any;
|
||||
|
||||
expect(WithBuilder.addClause(null as any, qb, null as any, [])).toBe(qb);
|
||||
expect(WithBuilder.addClause(em, qb, null as any, {})).toBe(qb);
|
||||
});
|
||||
|
||||
test("test manytomany", async () => {
|
||||
@@ -83,13 +122,13 @@ describe("[data] WithBuilder", async () => {
|
||||
.values([
|
||||
{ posts_id: 1, categories_id: 1 },
|
||||
{ posts_id: 2, categories_id: 2 },
|
||||
{ posts_id: 1, categories_id: 2 }
|
||||
{ posts_id: 1, categories_id: 2 },
|
||||
])
|
||||
.execute();
|
||||
|
||||
//console.log((await em.repository().findMany("posts_categories")).result);
|
||||
|
||||
const res = await em.repository(posts).findMany({ with: ["categories"] });
|
||||
const res = await em.repository(posts).findMany({ with: { categories: {} } });
|
||||
|
||||
expect(res.data).toEqual([
|
||||
{
|
||||
@@ -97,17 +136,17 @@ describe("[data] WithBuilder", async () => {
|
||||
title: "fashion post",
|
||||
categories: [
|
||||
{ id: 1, label: "fashion" },
|
||||
{ id: 2, label: "beauty" }
|
||||
]
|
||||
{ id: 2, label: "beauty" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "beauty post",
|
||||
categories: [{ id: 2, label: "beauty" }]
|
||||
}
|
||||
categories: [{ id: 2, label: "beauty" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const res2 = await em.repository(categories).findMany({ with: ["posts"] });
|
||||
const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
|
||||
|
||||
//console.log(res2.sql, res2.data);
|
||||
|
||||
@@ -115,21 +154,21 @@ describe("[data] WithBuilder", async () => {
|
||||
{
|
||||
id: 1,
|
||||
label: "fashion",
|
||||
posts: [{ id: 1, title: "fashion post" }]
|
||||
posts: [{ id: 1, title: "fashion post" }],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: "beauty",
|
||||
posts: [
|
||||
{ id: 1, title: "fashion post" },
|
||||
{ id: 2, title: "beauty post" },
|
||||
{ id: 1, title: "fashion post" }
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "tech",
|
||||
posts: []
|
||||
}
|
||||
posts: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -140,7 +179,7 @@ describe("[data] WithBuilder", async () => {
|
||||
const entities = [media, categories];
|
||||
const single = new PolymorphicRelation(categories, media, {
|
||||
mappedBy: "single",
|
||||
targetCardinality: 1
|
||||
targetCardinality: 1,
|
||||
});
|
||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||
|
||||
@@ -150,11 +189,11 @@ describe("[data] WithBuilder", async () => {
|
||||
em,
|
||||
em.connection.kysely.selectFrom("categories"),
|
||||
categories,
|
||||
["single"]
|
||||
{ single: {} },
|
||||
);
|
||||
const res = qb.compile();
|
||||
expect(res.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" limit ?) as obj) as "single" from "categories"'
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "single" from "categories"',
|
||||
);
|
||||
expect(res.parameters).toEqual(["categories.single", 1]);
|
||||
|
||||
@@ -162,13 +201,13 @@ describe("[data] WithBuilder", async () => {
|
||||
em,
|
||||
em.connection.kysely.selectFrom("categories"),
|
||||
categories,
|
||||
["multiple"]
|
||||
{ multiple: {} },
|
||||
);
|
||||
const res2 = qb2.compile();
|
||||
expect(res2.sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" limit ?) as agg) as "multiple" from "categories"'
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"',
|
||||
);
|
||||
expect(res2.parameters).toEqual(["categories.multiple", 5]);
|
||||
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
|
||||
});
|
||||
|
||||
/*test("test manytoone", async () => {
|
||||
@@ -192,4 +231,205 @@ describe("[data] WithBuilder", async () => {
|
||||
const res = await em.repository().findMany("posts", { join: ["author"] });
|
||||
console.log(res.sql, res.parameters, res.result);
|
||||
});*/
|
||||
|
||||
describe("recursive", () => {
|
||||
test("compiles with singles", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, users, media }) => {
|
||||
relation(posts).manyToOne(users);
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
const qb = WithBuilder.addClause(
|
||||
em,
|
||||
em.connection.kysely.selectFrom("posts"),
|
||||
schema.entities.posts,
|
||||
{
|
||||
users: {
|
||||
limit: 5, // ignored
|
||||
select: ["id", "username"],
|
||||
sort: { by: "username", dir: "asc" },
|
||||
with: {
|
||||
avatar: {
|
||||
select: ["id", "path"],
|
||||
limit: 2, // ignored
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
//prettyPrintQb(qb);
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts"',
|
||||
);
|
||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
|
||||
});
|
||||
|
||||
test("compiles with many", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {}),
|
||||
comments: proto.entity("comments", {}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, comments, users, media }) => {
|
||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
relation(comments).manyToOne(posts).manyToOne(users);
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
|
||||
const qb = WithBuilder.addClause(
|
||||
em,
|
||||
em.connection.kysely.selectFrom("posts"),
|
||||
schema.entities.posts,
|
||||
{
|
||||
comments: {
|
||||
limit: 12,
|
||||
with: {
|
||||
users: {
|
||||
select: ["username"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||
);
|
||||
expect(qb.compile().parameters).toEqual([1, 12, 0]);
|
||||
});
|
||||
|
||||
test("returns correct result", async () => {
|
||||
const schema = proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text(),
|
||||
}),
|
||||
users: proto.entity("users", {
|
||||
username: proto.text(),
|
||||
}),
|
||||
media: proto.entity("media", {
|
||||
path: proto.text(),
|
||||
}),
|
||||
},
|
||||
({ relation }, { posts, comments, users, media }) => {
|
||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||
relation(comments).manyToOne(posts).manyToOne(users);
|
||||
},
|
||||
);
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
// add data
|
||||
await em.mutator("users").insertMany([{ username: "user1" }, { username: "user2" }]);
|
||||
await em.mutator("posts").insertMany([
|
||||
{ title: "post1", users_id: 1 },
|
||||
{ title: "post2", users_id: 1 },
|
||||
{ title: "post3", users_id: 2 },
|
||||
]);
|
||||
await em.mutator("comments").insertMany([
|
||||
{ content: "comment1", posts_id: 1, users_id: 1 },
|
||||
{ content: "comment1-1", posts_id: 1, users_id: 1 },
|
||||
{ content: "comment2", posts_id: 1, users_id: 2 },
|
||||
{ content: "comment3", posts_id: 2, users_id: 1 },
|
||||
{ content: "comment4", posts_id: 2, users_id: 2 },
|
||||
{ content: "comment5", posts_id: 3, users_id: 1 },
|
||||
{ content: "comment6", posts_id: 3, users_id: 2 },
|
||||
]);
|
||||
|
||||
const result = await em.repo("posts").findMany({
|
||||
select: ["title"],
|
||||
with: {
|
||||
comments: {
|
||||
limit: 2,
|
||||
select: ["content"],
|
||||
with: {
|
||||
users: {
|
||||
select: ["username"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.data).toEqual([
|
||||
{
|
||||
title: "post1",
|
||||
comments: [
|
||||
{
|
||||
content: "comment1",
|
||||
users: {
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment1-1",
|
||||
users: {
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "post2",
|
||||
comments: [
|
||||
{
|
||||
content: "comment3",
|
||||
users: {
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment4",
|
||||
users: {
|
||||
username: "user2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "post3",
|
||||
comments: [
|
||||
{
|
||||
content: "comment5",
|
||||
users: {
|
||||
username: "user1",
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "comment6",
|
||||
users: {
|
||||
username: "user2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
//console.log(_jsonp(result.data));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { EntityManager } from "../../../../src/data";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -22,10 +22,10 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -54,14 +54,14 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
order: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -83,10 +83,10 @@ describe("Connection", async () => {
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
order: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { SqliteIntrospector } from "data/connection";
|
||||
import { getDummyDatabase } from "../../helper";
|
||||
import { Kysely, SqliteDialect } from "kysely";
|
||||
|
||||
function create() {
|
||||
const database = getDummyDatabase().dummyDb;
|
||||
return new Kysely({
|
||||
dialect: new SqliteDialect({ database }),
|
||||
});
|
||||
}
|
||||
|
||||
function createLibsql() {
|
||||
const database = getDummyDatabase().dummyDb;
|
||||
return new Kysely({
|
||||
dialect: new SqliteDialect({ database }),
|
||||
});
|
||||
}
|
||||
|
||||
describe("SqliteIntrospector", () => {
|
||||
test("asdf", async () => {
|
||||
const kysely = create();
|
||||
|
||||
await kysely.schema
|
||||
.createTable("test")
|
||||
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||
.addColumn("string", "text", (col) => col.notNull())
|
||||
.addColumn("number", "integer")
|
||||
.execute();
|
||||
|
||||
await kysely.schema
|
||||
.createIndex("idx_test_string")
|
||||
.on("test")
|
||||
.columns(["string"])
|
||||
.unique()
|
||||
.execute();
|
||||
|
||||
await kysely.schema
|
||||
.createTable("test2")
|
||||
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
|
||||
.addColumn("number", "integer")
|
||||
.execute();
|
||||
|
||||
await kysely.schema.createIndex("idx_test2_number").on("test2").columns(["number"]).execute();
|
||||
|
||||
const introspector = new SqliteIntrospector(kysely, {});
|
||||
|
||||
const result = await introspector.getTables();
|
||||
|
||||
//console.log(_jsonp(result));
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: "test",
|
||||
isView: false,
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
dataType: "INTEGER",
|
||||
isNullable: false,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
dataType: "TEXT",
|
||||
isNullable: false,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
comment: undefined,
|
||||
dataType: "INTEGER",
|
||||
hasDefaultValue: false,
|
||||
isAutoIncrementing: false,
|
||||
isNullable: true,
|
||||
name: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
isView: false,
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
dataType: "INTEGER",
|
||||
isNullable: false,
|
||||
isAutoIncrementing: true,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined,
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
dataType: "INTEGER",
|
||||
isNullable: true,
|
||||
isAutoIncrementing: false,
|
||||
hasDefaultValue: false,
|
||||
comment: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { BooleanField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { BooleanField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] BooleanField", async () => {
|
||||
runBaseFieldTests(BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new BooleanField("test");
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { DateField } from "../../../../src/data";
|
||||
import { runBaseFieldTests } from "./inc";
|
||||
import { describe, test } from "bun:test";
|
||||
import { DateField } from "data/fields";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
describe("[data] DateField", async () => {
|
||||
runBaseFieldTests(DateField, { defaultValue: new Date(), schemaType: "date" });
|
||||
fieldTestSuite(
|
||||
bunTestRunner,
|
||||
DateField,
|
||||
{ defaultValue: new Date(), schemaType: "date" },
|
||||
{ type: "date" },
|
||||
);
|
||||
|
||||
// @todo: add datefield tests
|
||||
test("week", async () => {
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { EnumField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { EnumField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
function options(strings: string[]) {
|
||||
return { type: "strings", values: strings };
|
||||
}
|
||||
|
||||
describe("[data] EnumField", async () => {
|
||||
runBaseFieldTests(
|
||||
fieldTestSuite(
|
||||
bunTestRunner,
|
||||
// @ts-ignore
|
||||
EnumField,
|
||||
{ defaultValue: "a", schemaType: "text" },
|
||||
{ options: options(["a", "b", "c"]) }
|
||||
{ options: options(["a", "b", "c"]) },
|
||||
);
|
||||
|
||||
test("yields if no options", async () => {
|
||||
expect(() => new EnumField("test", { options: options([]) })).toThrow();
|
||||
});
|
||||
|
||||
test("yields if default value is not a valid option", async () => {
|
||||
expect(
|
||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
|
||||
// @ts-ignore
|
||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("transformPersist (config)", async () => {
|
||||
// @ts-ignore
|
||||
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
||||
|
||||
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
||||
@@ -33,9 +34,10 @@ describe("[data] EnumField", async () => {
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new EnumField("test", {
|
||||
// @ts-ignore
|
||||
options: options(["a", "b", "c"]),
|
||||
default_value: "a",
|
||||
required: true
|
||||
required: true,
|
||||
});
|
||||
|
||||
expect(field.transformRetrieve(null)).toBe("a");
|
||||
|
||||
@@ -1,45 +1,50 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Default, parse, stripMark } from "../../../../src/core/utils";
|
||||
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
|
||||
describe("[data] Field", async () => {
|
||||
class FieldSpec extends Field {
|
||||
schema(): SchemaResponse {
|
||||
return this.useSchemaHelper("text");
|
||||
}
|
||||
getSchema() {
|
||||
return baseFieldConfigSchema;
|
||||
}
|
||||
}
|
||||
|
||||
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||
test("fieldSpec", () => {
|
||||
expect(new FieldSpec("test").schema()).toEqual({
|
||||
name: "test",
|
||||
type: "text",
|
||||
nullable: true, // always true
|
||||
dflt: undefined, // never using default value
|
||||
});
|
||||
});
|
||||
|
||||
test.only("default config", async () => {
|
||||
const field = new FieldSpec("test");
|
||||
const config = Default(baseFieldConfigSchema, {});
|
||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
|
||||
console.log("config", new TextField("test", { required: true }).toJSON());
|
||||
fieldTestSuite(bunTestRunner, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||
|
||||
test("default config", async () => {
|
||||
const config = baseFieldConfigSchema.template({});
|
||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||
});
|
||||
|
||||
test("transformPersist (specific)", async () => {
|
||||
const required = new FieldSpec("test", { required: true });
|
||||
const requiredDefault = new FieldSpec("test", {
|
||||
required: true,
|
||||
default_value: "test"
|
||||
default_value: "test",
|
||||
});
|
||||
|
||||
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
|
||||
expect(
|
||||
required.transformPersist(undefined, undefined as any, undefined as any)
|
||||
required.transformPersist(undefined, undefined as any, undefined as any),
|
||||
).rejects.toThrow();
|
||||
|
||||
// works because it has a default value
|
||||
expect(
|
||||
requiredDefault.transformPersist(null, undefined as any, undefined as any)
|
||||
requiredDefault.transformPersist(null, undefined as any, undefined as any),
|
||||
).resolves.toBeDefined();
|
||||
expect(
|
||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any)
|
||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "../../../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityIndex,
|
||||
type EntityManager,
|
||||
Field,
|
||||
type SchemaResponse
|
||||
} from "../../../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { Field, EntityIndex } from "data/fields";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
class TestField extends Field {
|
||||
protected getSchema(): any {
|
||||
return Type.Any();
|
||||
return s.any();
|
||||
}
|
||||
|
||||
schema(em: EntityManager<any>): SchemaResponse {
|
||||
override schema() {
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
@@ -28,11 +23,4 @@ describe("FieldIndex", async () => {
|
||||
expect(index.name).toEqual("idx_test_name");
|
||||
expect(index.unique).toEqual(false);
|
||||
});
|
||||
|
||||
test("it fails on non-unique", async () => {
|
||||
const field = new TestField("name", { required: false });
|
||||
|
||||
expect(() => new EntityIndex(entity, [field], true)).toThrowError();
|
||||
expect(() => new EntityIndex(entity, [field])).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { JsonField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonField", async () => {
|
||||
const field = new JsonField("test");
|
||||
runBaseFieldTests(JsonField, {
|
||||
fieldTestSuite(bunTestRunner, JsonField, {
|
||||
defaultValue: { a: 1 },
|
||||
sampleValues: ["string", { test: 1 }, 1],
|
||||
schemaType: "text"
|
||||
//sampleValues: ["string", { test: 1 }, 1],
|
||||
schemaType: "text",
|
||||
});
|
||||
|
||||
test("transformPersist (no config)", async () => {
|
||||
@@ -32,9 +33,9 @@ describe("[data] JsonField", async () => {
|
||||
});
|
||||
|
||||
test("getValue", async () => {
|
||||
expect(field.getValue({ test: 1 }, "form")).toBe('{"test":1}');
|
||||
expect(field.getValue("string", "form")).toBe('"string"');
|
||||
expect(field.getValue(1, "form")).toBe("1");
|
||||
expect(field.getValue({ test: 1 }, "form")).toEqual({ test: 1 });
|
||||
expect(field.getValue("string", "form")).toBe("string");
|
||||
expect(field.getValue(1, "form")).toBe(1);
|
||||
|
||||
expect(field.getValue('{"test":1}', "submit")).toEqual({ test: 1 });
|
||||
expect(field.getValue('"string"', "submit")).toBe("string");
|
||||
@@ -42,6 +43,5 @@ describe("[data] JsonField", async () => {
|
||||
|
||||
expect(field.getValue({ test: 1 }, "table")).toBe('{"test":1}');
|
||||
expect(field.getValue("string", "table")).toBe('"string"');
|
||||
expect(field.getValue(1, "form")).toBe("1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonSchemaField } from "../../../../src/data";
|
||||
import { runBaseFieldTests } from "./inc";
|
||||
import { JsonSchemaField } from "data/fields";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonSchemaField", async () => {
|
||||
runBaseFieldTests(JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||
// @ts-ignore
|
||||
fieldTestSuite({ expect, test }, JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||
|
||||
// @todo: add JsonSchemaField tests
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { NumberField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { NumberField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] NumberField", async () => {
|
||||
test("transformPersist (config)", async () => {
|
||||
@@ -15,5 +16,5 @@ describe("[data] NumberField", async () => {
|
||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||
});
|
||||
|
||||
runBaseFieldTests(NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
fieldTestSuite(bunTestRunner, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { PrimaryField } from "../../../../src/data";
|
||||
import { PrimaryField } from "data/fields";
|
||||
|
||||
describe("[data] PrimaryField", async () => {
|
||||
const field = new PrimaryField("primary");
|
||||
@@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => {
|
||||
|
||||
test("schema", () => {
|
||||
expect(field.name).toBe("primary");
|
||||
expect(field.schema()).toEqual(["primary", "integer", expect.any(Function)]);
|
||||
expect(field.schema()).toEqual({
|
||||
name: "primary",
|
||||
type: "integer" as const,
|
||||
nullable: false,
|
||||
primary: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("hasDefault", async () => {
|
||||
@@ -34,4 +39,28 @@ describe("[data] PrimaryField", async () => {
|
||||
expect(field.transformPersist(1)).rejects.toThrow();
|
||||
expect(field.transformRetrieve(1)).toBe(1);
|
||||
});
|
||||
|
||||
test("format", () => {
|
||||
const uuid = new PrimaryField("uuid", { format: "uuid" });
|
||||
expect(uuid.format).toBe("uuid");
|
||||
expect(uuid.fieldType).toBe("text");
|
||||
expect(uuid.getNewValue()).toBeString();
|
||||
expect(uuid.toType()).toEqual({
|
||||
required: true,
|
||||
comment: undefined,
|
||||
type: "Generated<string>",
|
||||
import: [{ package: "kysely", name: "Generated" }],
|
||||
});
|
||||
|
||||
const integer = new PrimaryField("integer", { format: "integer" });
|
||||
expect(integer.format).toBe("integer");
|
||||
expect(integer.fieldType).toBe("integer");
|
||||
expect(integer.getNewValue()).toBeUndefined();
|
||||
expect(integer.toType()).toEqual({
|
||||
required: true,
|
||||
comment: undefined,
|
||||
type: "Generated<number>",
|
||||
import: [{ package: "kysely", name: "Generated" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { TextField } from "../../../../src/data";
|
||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
||||
import { TextField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
describe("[data] TextField", async () => {
|
||||
test("transformPersist (config)", async () => {
|
||||
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
|
||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
||||
});
|
||||
|
||||
runBaseFieldTests(TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
fieldTestSuite(bunTestRunner, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
});
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { Entity, type EntityManager } from "../../../../src/data";
|
||||
import { Entity, type EntityManager } from "data/entities";
|
||||
import {
|
||||
type BaseRelationConfig,
|
||||
EntityRelation,
|
||||
EntityRelationAnchor,
|
||||
RelationTypes
|
||||
} from "../../../../src/data/relations";
|
||||
ManyToManyRelation,
|
||||
RelationTypes,
|
||||
} from "data/relations";
|
||||
import * as proto from "data/prototype";
|
||||
|
||||
class TestEntityRelation extends EntityRelation {
|
||||
constructor(config?: BaseRelationConfig) {
|
||||
super(
|
||||
new EntityRelationAnchor(new Entity("source"), "source"),
|
||||
new EntityRelationAnchor(new Entity("target"), "target"),
|
||||
config
|
||||
config,
|
||||
);
|
||||
}
|
||||
initialize(em: EntityManager<any>) {}
|
||||
@@ -24,11 +26,11 @@ class TestEntityRelation extends EntityRelation {
|
||||
return this;
|
||||
}
|
||||
|
||||
buildWith(a: any, b: any, c: any): any {
|
||||
buildWith(): any {
|
||||
return;
|
||||
}
|
||||
|
||||
buildJoin(a: any, b: any): any {
|
||||
buildJoin(): any {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -70,9 +72,20 @@ describe("[data] EntityRelation", async () => {
|
||||
|
||||
it("required", async () => {
|
||||
const relation1 = new TestEntityRelation();
|
||||
expect(relation1.config.required).toBe(false);
|
||||
expect(relation1.required).toBe(false);
|
||||
|
||||
const relation2 = new TestEntityRelation({ required: true });
|
||||
expect(relation2.config.required).toBe(true);
|
||||
expect(relation2.required).toBe(true);
|
||||
});
|
||||
|
||||
it("correctly produces the relation name", async () => {
|
||||
const relation = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"));
|
||||
expect(relation.getName()).not.toContain(",");
|
||||
expect(relation.getName()).toBe("mn_apps_organizations");
|
||||
|
||||
const relation2 = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"), {
|
||||
connectionTableMappedName: "appOrganizations",
|
||||
});
|
||||
expect(relation2.getName()).toBe("mn_apps_organizations_appOrganizations");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import * as sDirect from "jsonv-ts";
|
||||
import { s as sFromBknd } from "bknd/utils";
|
||||
|
||||
describe("jsonv-ts resolution", () => {
|
||||
it("should resolve to a single instance", () => {
|
||||
const sameNamespace = sDirect === (sFromBknd as unknown as typeof sDirect);
|
||||
// If this fails, two instances are being loaded via different specifiers/paths
|
||||
expect(sameNamespace).toBe(true);
|
||||
});
|
||||
|
||||
it("should resolve specifiers to a single package path", async () => {
|
||||
const base = await import.meta.resolve("jsonv-ts");
|
||||
const hono = await import.meta.resolve("jsonv-ts/hono");
|
||||
const mcp = await import.meta.resolve("jsonv-ts/mcp");
|
||||
expect(typeof base).toBe("string");
|
||||
expect(typeof hono).toBe("string");
|
||||
expect(typeof mcp).toBe("string");
|
||||
// They can be different files (subpath exports), but they should share the same package root
|
||||
const pkgRoot = (p: string) => p.slice(0, p.lastIndexOf("jsonv-ts") + "jsonv-ts".length);
|
||||
expect(pkgRoot(base)).toBe(pkgRoot(hono));
|
||||
expect(pkgRoot(base)).toBe(pkgRoot(mcp));
|
||||
});
|
||||
});
|
||||
@@ -30,23 +30,23 @@ beforeAll(() =>
|
||||
method: init?.method ?? "GET",
|
||||
// @ts-ignore
|
||||
headers: Object.fromEntries(init?.headers?.entries() ?? []),
|
||||
body: init?.body
|
||||
body: init?.body,
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify({ todos: [1, 2], request }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
afterAll(unmockFetch);
|
||||
|
||||
describe("FetchTask", async () => {
|
||||
describe.skip("FetchTask", async () => {
|
||||
test("Simple fetch", async () => {
|
||||
const task = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
method: "GET",
|
||||
headers: [{ key: "Content-Type", value: "application/json" }]
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
});
|
||||
|
||||
const result = await task.run();
|
||||
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
|
||||
|
||||
expect(
|
||||
// // @ts-expect-error
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 })
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
|
||||
).toThrow();
|
||||
|
||||
expect(
|
||||
new FetchTask("", {
|
||||
url: "https://jsonplaceholder.typicode.com",
|
||||
method: "invalid"
|
||||
}).execute()
|
||||
method: "invalid",
|
||||
}).execute(),
|
||||
).rejects.toThrow(/^Invalid method/);
|
||||
|
||||
expect(
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" })
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
|
||||
).toBeDefined();
|
||||
|
||||
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
|
||||
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
|
||||
method: "{{ flow.output.method }}",
|
||||
headers: [
|
||||
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
|
||||
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" }
|
||||
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" },
|
||||
],
|
||||
body: JSON.stringify({
|
||||
email: "{{ flow.output.email }}"
|
||||
})
|
||||
email: "{{ flow.output.email }}",
|
||||
}),
|
||||
});
|
||||
const inputs = {
|
||||
headerKey: "Type",
|
||||
apiKey: 123,
|
||||
email: "what@else.com",
|
||||
method: "PATCH"
|
||||
method: "PATCH",
|
||||
};
|
||||
|
||||
const flow = new Flow("", [task]);
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows";
|
||||
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
describe("SubFlowTask", async () => {
|
||||
export class StringifyTask<Output extends string> extends Task<
|
||||
typeof StringifyTask.schema,
|
||||
Output
|
||||
> {
|
||||
type = "stringify";
|
||||
|
||||
static override schema = s.object({
|
||||
input: s.string().optional(),
|
||||
});
|
||||
|
||||
async execute() {
|
||||
return JSON.stringify(this.params.input) as Output;
|
||||
}
|
||||
}
|
||||
|
||||
describe.skip("SubFlowTask", async () => {
|
||||
test("Simple Subflow", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "subflow"
|
||||
render: "subflow",
|
||||
});
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
const task = new LogTask("log");
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow
|
||||
flow: subflow,
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: "Subflow output: {{ sub.output }}"
|
||||
render: "Subflow output: {{ sub.output }}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
@@ -22,15 +38,13 @@ describe("SubFlowTask", async () => {
|
||||
|
||||
const execution = flow.createExecution();
|
||||
await execution.start();
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: subflow");
|
||||
});
|
||||
|
||||
test("Simple loop", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "run {{ flow.output }}"
|
||||
render: "run {{ flow.output }}",
|
||||
});
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
@@ -38,10 +52,10 @@ describe("SubFlowTask", async () => {
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow,
|
||||
loop: true,
|
||||
input: [1, 2, 3]
|
||||
input: [1, 2, 3],
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
||||
const task3 = new StringifyTask("stringify", {
|
||||
input: "{{ sub.output }}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
@@ -51,41 +65,6 @@ describe("SubFlowTask", async () => {
|
||||
const execution = flow.createExecution();
|
||||
await execution.start();
|
||||
|
||||
console.log("errors", execution.getErrors());
|
||||
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: run 1, run 2, run 3");
|
||||
});
|
||||
|
||||
test("Simple loop from flow input", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "run {{ flow.output }}"
|
||||
});
|
||||
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
const task = new LogTask("log");
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow,
|
||||
loop: true,
|
||||
input: "{{ flow.output | json }}"
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
flow.task(task).asInputFor(task2);
|
||||
flow.task(task2).asInputFor(task3);
|
||||
|
||||
const execution = flow.createExecution();
|
||||
await execution.start([4, 5, 6]);
|
||||
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: run 4, run 5, run 6");
|
||||
expect(execution.getResponse()).toEqual('"run 1,run 2,run 3"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "../../src/core/utils";
|
||||
import { Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
describe("Task", async () => {
|
||||
describe.skip("Task", async () => {
|
||||
test("resolveParams: template with parse", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Number()) }),
|
||||
s.object({ test: dynamic(s.number()) }),
|
||||
{
|
||||
test: "{{ some.path }}"
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: 1
|
||||
}
|
||||
}
|
||||
path: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toBe(1);
|
||||
@@ -22,15 +22,15 @@ describe("Task", async () => {
|
||||
|
||||
test("resolveParams: with string", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: Type.String() }),
|
||||
s.object({ test: s.string() }),
|
||||
{
|
||||
test: "{{ some.path }}"
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: "1/1"
|
||||
}
|
||||
}
|
||||
path: "1/1",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.test).toBe("1/1");
|
||||
@@ -38,75 +38,17 @@ describe("Task", async () => {
|
||||
|
||||
test("resolveParams: with object", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
|
||||
s.object({ test: dynamic(s.object({ key: s.string(), value: s.string() })) }),
|
||||
{
|
||||
test: { key: "path", value: "{{ some.path }}" }
|
||||
test: { key: "path", value: "{{ some.path }}" },
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: "1/1"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||
});
|
||||
|
||||
test("resolveParams: with json", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() }))
|
||||
}),
|
||||
{
|
||||
test: "{{ some | json }}"
|
||||
path: "1/1",
|
||||
},
|
||||
},
|
||||
{
|
||||
some: {
|
||||
key: "path",
|
||||
value: "1/1"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||
});
|
||||
|
||||
test("resolveParams: with array", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Array(Type.String()))
|
||||
}),
|
||||
{
|
||||
test: '{{ "1,2,3" | split: "," | json }}'
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
|
||||
test("resolveParams: boolean", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Boolean())
|
||||
}),
|
||||
{
|
||||
test: "{{ true }}"
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual(true);
|
||||
});
|
||||
|
||||
test("resolveParams: float", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Number(), Number.parseFloat)
|
||||
}),
|
||||
{
|
||||
test: "{{ 3.14 }}"
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual(3.14);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,11 +7,11 @@ const first = getNamedTask(
|
||||
//throw new Error("Error");
|
||||
return {
|
||||
inner: {
|
||||
result: 2
|
||||
}
|
||||
result: 2,
|
||||
},
|
||||
};
|
||||
},
|
||||
1000
|
||||
1000,
|
||||
);
|
||||
const second = getNamedTask("second (if match)");
|
||||
const third = getNamedTask("third (if error)");
|
||||
|
||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
||||
constructor(
|
||||
name: string,
|
||||
params: any,
|
||||
private fn: () => any
|
||||
private fn: () => any,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: n
|
||||
return new ExecTask(
|
||||
name,
|
||||
{
|
||||
delay
|
||||
delay,
|
||||
},
|
||||
func
|
||||
func,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const first = new LogTask("First", { delay: 1000 });
|
||||
const second = new LogTask("Second", { delay: 1000 });
|
||||
const third = new LogTask("Long Third", { delay: 2500 });
|
||||
const fourth = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1"
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
});
|
||||
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { Event, EventManager } from "../../src/core/events";
|
||||
import { type Static, type StaticDecode, Type, parse } from "../../src/core/utils";
|
||||
import { s, parse } from "core/utils/schema";
|
||||
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
|
||||
@@ -14,19 +14,19 @@ class Passthrough extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
type OutputIn = Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
||||
type OutputIn = s.Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
|
||||
|
||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||
type = "output-param";
|
||||
|
||||
static override schema = Type.Object({
|
||||
static override schema = s.strictObject({
|
||||
number: dynamic(
|
||||
Type.Number({
|
||||
title: "Output number"
|
||||
s.number({
|
||||
title: "Output number",
|
||||
}),
|
||||
Number.parseInt
|
||||
)
|
||||
Number.parseInt,
|
||||
),
|
||||
});
|
||||
|
||||
async execute(inputs: InputsMap) {
|
||||
@@ -43,7 +43,7 @@ class PassthroughFlowInput extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow task inputs", async () => {
|
||||
describe.skip("Flow task inputs", async () => {
|
||||
test("types", async () => {
|
||||
const schema = OutputParamTask.schema;
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
|
||||
test("output/input", async () => {
|
||||
const task = new OutputParamTask("task1", { number: 111 });
|
||||
const task2 = new OutputParamTask("task2", {
|
||||
number: "{{ task1.output }}"
|
||||
number: "{{ task1.output }}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
|
||||
|
||||
test("input from flow", async () => {
|
||||
const task = new OutputParamTask("task1", {
|
||||
number: "{{flow.output.someFancyParam}}"
|
||||
number: "{{flow.output.someFancyParam}}",
|
||||
});
|
||||
const task2 = new OutputParamTask("task2", {
|
||||
number: "{{task1.output}}"
|
||||
number: "{{task1.output}}",
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
|
||||
const emgr = new EventManager({ EventTriggerClass });
|
||||
|
||||
const task = new OutputParamTask("event", {
|
||||
number: "{{flow.output.number}}"
|
||||
number: "{{flow.output.number}}",
|
||||
});
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
|
||||
[],
|
||||
new EventTrigger({
|
||||
event: "test-event",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
flow.trigger.register(flow, emgr);
|
||||
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const flows = {
|
||||
back,
|
||||
fanout,
|
||||
parallel,
|
||||
simpleFetch
|
||||
simpleFetch,
|
||||
};
|
||||
|
||||
const arg = process.argv[2];
|
||||
@@ -32,7 +32,7 @@ const colors = [
|
||||
"#F78F1E", // Saffron
|
||||
"#BD10E0", // Vivid Purple
|
||||
"#50E3C2", // Turquoise
|
||||
"#9013FE" // Grape
|
||||
"#9013FE", // Grape
|
||||
];
|
||||
|
||||
const colorsCache: Record<string, string> = {};
|
||||
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
||||
}
|
||||
|
||||
return t;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
||||
console.log("done", response ? response : "(no response)");
|
||||
console.log(
|
||||
"Executed tasks:",
|
||||
execution.logs.map((l) => l.task.name)
|
||||
execution.logs.map((l) => l.task.name),
|
||||
);
|
||||
console.log("Executed count:", execution.logs.length);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
||||
constructor(
|
||||
name: string,
|
||||
params: any,
|
||||
private fn: () => any
|
||||
private fn: () => any,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class ExecTask extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow trigger", async () => {
|
||||
describe.skip("Flow trigger", async () => {
|
||||
test("manual trigger", async () => {
|
||||
let called = false;
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new EventTrigger({ event: "test-event", mode: "sync" })
|
||||
new EventTrigger({ event: "test-event", mode: "sync" }),
|
||||
);
|
||||
|
||||
flow.trigger.register(flow, emgr);
|
||||
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
|
||||
const hono = new Hono();
|
||||
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
|
||||
|
||||
test("http trigger with response", async () => {
|
||||
const task = ExecTask.create("http", () => ({
|
||||
called: true
|
||||
called: true,
|
||||
}));
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
mode: "sync",
|
||||
}),
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { type Static, Type, _jsonp } from "../../src/core/utils";
|
||||
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
||||
import { s } from "core/utils/schema";
|
||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||
|
||||
/*beforeAll(disableConsoleLog);
|
||||
@@ -10,19 +11,19 @@ afterAll(enableConsoleLog);*/
|
||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||
type = "exec";
|
||||
|
||||
static override schema = Type.Object({
|
||||
delay: Type.Number({ default: 10 })
|
||||
static override schema = s.object({
|
||||
delay: s.number({ default: 10 }),
|
||||
});
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
params: Static<typeof ExecTask.schema>,
|
||||
private func: () => Promise<any>
|
||||
params: s.Static<typeof ExecTask.schema>,
|
||||
private func: () => Promise<any>,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
|
||||
override clone(name: string, params: Static<typeof ExecTask.schema>) {
|
||||
override clone(name: string, params: s.Static<typeof ExecTask.schema>) {
|
||||
return new ExecTask(name, params, this.func);
|
||||
}
|
||||
|
||||
@@ -36,12 +37,12 @@ function getTask(num: number = 0, delay: number = 5) {
|
||||
return new ExecTask(
|
||||
`Task ${num}`,
|
||||
{
|
||||
delay
|
||||
delay,
|
||||
},
|
||||
async () => {
|
||||
//console.log(`[DONE] Task: ${num}`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
//return new LogTask(`Log ${num}`, { delay });
|
||||
}
|
||||
@@ -56,9 +57,9 @@ function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number)
|
||||
return new ExecTask(
|
||||
name,
|
||||
{
|
||||
delay: delay ?? 0
|
||||
delay: delay ?? 0,
|
||||
},
|
||||
func
|
||||
func,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
describe("Flow tests", async () => {
|
||||
describe.skip("Flow tests", async () => {
|
||||
test("Simple single task", async () => {
|
||||
const simple = getTask(0);
|
||||
|
||||
@@ -228,11 +229,13 @@ describe("Flow tests", async () => {
|
||||
back
|
||||
.task(third)
|
||||
.getOutTasks()
|
||||
.map((t) => t.name)
|
||||
.map((t) => t.name),
|
||||
).toEqual(["second", "fourth"]);
|
||||
|
||||
const execution = back.createExecution();
|
||||
expect(execution.start()).rejects.toThrow();
|
||||
withDisabledConsole(async () => {
|
||||
expect(execution.start()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test("Flow with back step: enough retries", async () => {
|
||||
@@ -261,7 +264,7 @@ describe("Flow tests", async () => {
|
||||
back
|
||||
.task(third)
|
||||
.getOutTasks()
|
||||
.map((t) => t.name)
|
||||
.map((t) => t.name),
|
||||
).toEqual(["second", "fourth"]);
|
||||
|
||||
const execution = back.createExecution();
|
||||
@@ -322,8 +325,8 @@ describe("Flow tests", async () => {
|
||||
const first = getNamedTask("first", async () => {
|
||||
return {
|
||||
inner: {
|
||||
result: 2
|
||||
}
|
||||
result: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
const second = getNamedTask("second");
|
||||
@@ -359,7 +362,7 @@ describe("Flow tests", async () => {
|
||||
"[event]",
|
||||
event.isStart() ? "start" : "end",
|
||||
event.task().name,
|
||||
event.isStart() ? undefined : event.succeeded()
|
||||
event.isStart() ? undefined : event.succeeded(),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -387,7 +390,7 @@ describe("Flow tests", async () => {
|
||||
const second = new LogTask("Task 1");
|
||||
const third = new LogTask("Task 2", { delay: 50 });
|
||||
const fourth = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1"
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
});
|
||||
const fifth = new LogTask("Task 4"); // without connection
|
||||
|
||||
@@ -403,7 +406,7 @@ describe("Flow tests", async () => {
|
||||
// @todo: fix
|
||||
const deserialized = Flow.fromObject("", original, {
|
||||
fetch: { cls: FetchTask },
|
||||
log: { cls: LogTask }
|
||||
log: { cls: LogTask },
|
||||
} as any);
|
||||
|
||||
const diffdeep = getObjectDiff(original, deserialized.toJSON());
|
||||
@@ -412,7 +415,7 @@ describe("Flow tests", async () => {
|
||||
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
|
||||
expect(flow.respondingTask?.name).toEqual(
|
||||
// @ts-ignore
|
||||
deserialized.respondingTask?.name
|
||||
deserialized.respondingTask?.name,
|
||||
);
|
||||
|
||||
//console.log("--- creating original sequence");
|
||||
|
||||
+68
-19
@@ -1,7 +1,13 @@
|
||||
import { unlink } from "node:fs/promises";
|
||||
import type { SqliteDatabase } from "kysely";
|
||||
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
||||
import Database from "libsql";
|
||||
import { SqliteLocalConnection } from "../src/data";
|
||||
import { format as sqlFormat } from "sql-formatter";
|
||||
import type { em as protoEm } from "../src/data/prototype";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { slugify } from "bknd/utils";
|
||||
import { type Connection, SqliteLocalConnection } from "data/connection";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
@@ -15,7 +21,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
||||
afterAllCleanup: async () => {
|
||||
if (!memory) await unlink(DB_NAME);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +31,7 @@ export function getDummyConnection(memory: boolean = true) {
|
||||
|
||||
return {
|
||||
dummyConnection,
|
||||
afterAllCleanup
|
||||
afterAllCleanup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,21 +39,64 @@ export function getLocalLibsqlConnection() {
|
||||
return { url: "http://127.0.0.1:8080" };
|
||||
}
|
||||
|
||||
type ConsoleSeverity = "log" | "warn" | "error";
|
||||
const _oldConsoles = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error
|
||||
};
|
||||
|
||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log"]) {
|
||||
severities.forEach((severity) => {
|
||||
console[severity] = () => null;
|
||||
});
|
||||
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||
const { sql, parameters } = qb.compile();
|
||||
return { sql, parameters };
|
||||
}
|
||||
|
||||
export function enableConsoleLog() {
|
||||
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||
console[severity as ConsoleSeverity] = fn;
|
||||
});
|
||||
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||
const { sql, parameters } = qb.compile();
|
||||
console.info("$", sqlFormat(sql), "\n[params]", parameters);
|
||||
}
|
||||
|
||||
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
||||
const connection = conn ? conn : getDummyConnection().dummyConnection;
|
||||
return new EntityManager(Object.values(s.entities), connection, s.relations, s.indices);
|
||||
}
|
||||
|
||||
export const assetsPath = `${import.meta.dir}/_assets`;
|
||||
export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
|
||||
|
||||
export async function enableFetchLogging() {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
// @ts-ignore
|
||||
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const response = await originalFetch(input, init);
|
||||
const url = input instanceof URL || typeof input === "string" ? input : input.url;
|
||||
|
||||
// Only clone if it's a supported content type
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const isSupported =
|
||||
contentType.includes("json") ||
|
||||
contentType.includes("text") ||
|
||||
contentType.includes("xml");
|
||||
|
||||
if (isSupported) {
|
||||
const clonedResponse = response.clone();
|
||||
let extension = "txt";
|
||||
let body: string;
|
||||
|
||||
if (contentType.includes("json")) {
|
||||
body = JSON.stringify(await clonedResponse.json(), null, 2);
|
||||
extension = "json";
|
||||
} else if (contentType.includes("xml")) {
|
||||
body = await clonedResponse.text();
|
||||
extension = "xml";
|
||||
} else {
|
||||
body = await clonedResponse.text();
|
||||
}
|
||||
|
||||
const fileName = `${new Date().getTime()}_${init?.method ?? "GET"}_${slugify(String(url))}.${extension}`;
|
||||
const filePath = join(assetsTmpPath, fileName);
|
||||
|
||||
await writeFile(filePath, body);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return () => {
|
||||
global.fetch = originalFetch;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { App, createApp, type AuthResponse } from "../../src";
|
||||
import { auth } from "../../src/modules/middlewares";
|
||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
const roles = {
|
||||
sloppy: {
|
||||
guest: {
|
||||
permissions: [
|
||||
"system.access.admin",
|
||||
"system.schema.read",
|
||||
"system.access.api",
|
||||
"system.config.read",
|
||||
"data.entity.read",
|
||||
],
|
||||
is_default: true,
|
||||
},
|
||||
admin: {
|
||||
is_default: true,
|
||||
implicit_allow: true,
|
||||
},
|
||||
},
|
||||
strict: {
|
||||
guest: {
|
||||
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
|
||||
is_default: true,
|
||||
},
|
||||
admin: {
|
||||
is_default: true,
|
||||
implicit_allow: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const configs = {
|
||||
auth: {
|
||||
enabled: true,
|
||||
entity_name: "users",
|
||||
jwt: {
|
||||
secret: secureRandomString(20),
|
||||
issuer: randomString(10),
|
||||
},
|
||||
roles: roles.strict,
|
||||
guard: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
normal: {
|
||||
email: "normal@bknd.io",
|
||||
password: "12345678",
|
||||
},
|
||||
admin: {
|
||||
email: "admin@bknd.io",
|
||||
password: "12345678",
|
||||
role: "admin",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function createAuthApp() {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
const app = createApp({
|
||||
connection: dummyConnection,
|
||||
config: {
|
||||
auth: configs.auth,
|
||||
},
|
||||
});
|
||||
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppFirstBoot,
|
||||
async () => {
|
||||
await app.createUser(configs.users.normal);
|
||||
await app.createUser(configs.users.admin);
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function getCookie(r: Response, name: string) {
|
||||
const cookies = r.headers.get("cookie") ?? r.headers.get("set-cookie");
|
||||
if (!cookies) return;
|
||||
const cookie = cookies.split(";").find((c) => c.trim().startsWith(name));
|
||||
if (!cookie) return;
|
||||
return cookie.split("=")[1];
|
||||
}
|
||||
|
||||
const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) => {
|
||||
function headers(token?: string, additional?: Record<string, string>) {
|
||||
if (mode === "cookie") {
|
||||
return {
|
||||
cookie: `auth=${token};`,
|
||||
...additional,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
"Content-Type": "application/json",
|
||||
...additional,
|
||||
};
|
||||
}
|
||||
function body(obj?: Record<string, any>) {
|
||||
if (mode === "cookie") {
|
||||
const formData = new FormData();
|
||||
for (const key in obj) {
|
||||
formData.append(key, obj[key]);
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
return {
|
||||
login: async (
|
||||
user: any,
|
||||
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
|
||||
const res = (await app.server.request("/api/auth/password/login", {
|
||||
method: "POST",
|
||||
headers: headers(),
|
||||
body: body(user),
|
||||
})) as Response;
|
||||
|
||||
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
||||
|
||||
return { res, data };
|
||||
},
|
||||
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
||||
const res = (await app.server.request("/api/auth/me", {
|
||||
method: "GET",
|
||||
headers: headers(token),
|
||||
})) as Response;
|
||||
return await res.json();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe("integration auth", () => {
|
||||
it("should create users on boot", async () => {
|
||||
const app = createAuthApp();
|
||||
await app.build();
|
||||
|
||||
const { data: users } = await app.em.repository("users").findMany();
|
||||
expect(users.length).toBe(2);
|
||||
expect(users[0]?.email).toBe(configs.users.normal.email);
|
||||
expect(users[1]?.email).toBe(configs.users.admin.email);
|
||||
});
|
||||
|
||||
it("should log you in with API", async () => {
|
||||
const app = createAuthApp();
|
||||
await app.build();
|
||||
const $fns = fns(app);
|
||||
|
||||
// login api
|
||||
const { data } = await $fns.login(configs.users.normal);
|
||||
const me = await $fns.me(data.token);
|
||||
|
||||
expect(data.user.email).toBe(me.user.email);
|
||||
expect(me.user.email).toBe(configs.users.normal.email);
|
||||
|
||||
// expect no user with no token
|
||||
expect(await $fns.me()).toEqual({ user: null as any });
|
||||
|
||||
// expect no user with invalid token
|
||||
expect(await $fns.me("invalid")).toEqual({ user: null as any });
|
||||
expect(await $fns.me()).toEqual({ user: null as any });
|
||||
});
|
||||
|
||||
it("should log you in with form and cookie", async () => {
|
||||
const app = createAuthApp();
|
||||
await app.build();
|
||||
const $fns = fns(app, "cookie");
|
||||
|
||||
const { res, data: token } = await $fns.login(configs.users.normal);
|
||||
expect(token).toBeDefined();
|
||||
expect(res.status).toBe(302); // because it redirects
|
||||
|
||||
// test cookie jwt interchangability
|
||||
{
|
||||
// expect token to not work as-is for api endpoints
|
||||
expect(await fns(app).me(token)).toEqual({ user: null as any });
|
||||
// hono adds an additional segment to cookies
|
||||
const apified_token = token.split(".").slice(0, -1).join(".");
|
||||
// now it should work
|
||||
// @todo: maybe add a config to don't allow re-use?
|
||||
expect((await fns(app).me(apified_token)).user.email).toBe(configs.users.normal.email);
|
||||
}
|
||||
|
||||
// test cookie with me endpoint
|
||||
{
|
||||
const me = await $fns.me(token);
|
||||
expect(me.user.email).toBe(configs.users.normal.email);
|
||||
|
||||
// check with invalid & empty
|
||||
expect(await $fns.me("invalid")).toEqual({ user: null as any });
|
||||
expect(await $fns.me()).toEqual({ user: null as any });
|
||||
}
|
||||
});
|
||||
|
||||
it("should check for permissions", async () => {
|
||||
const app = createAuthApp();
|
||||
await app.build();
|
||||
|
||||
await withDisabledConsole(async () => {
|
||||
const res = await app.server.request("/api/system/schema");
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
it("context is exclusive", async () => {
|
||||
const app = createAuthApp();
|
||||
await app.build();
|
||||
const $fns = fns(app);
|
||||
|
||||
app.server.get("/get", auth(), async (c) => {
|
||||
return c.json({
|
||||
user: c.get("auth")?.user ?? null,
|
||||
});
|
||||
});
|
||||
app.server.get("/wait", auth(), async (c) => {
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
const { data } = await $fns.login(configs.users.normal);
|
||||
const me = await $fns.me(data.token);
|
||||
expect(me.user.email).toBe(configs.users.normal.email);
|
||||
|
||||
app.server.request("/wait", {
|
||||
headers: { Authorization: `Bearer ${data.token}` },
|
||||
});
|
||||
|
||||
{
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const res = await app.server.request("/get");
|
||||
const data = (await res.json()) as any;
|
||||
expect(data.user).toBe(null);
|
||||
expect(await $fns.me()).toEqual({ user: null as any });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { Api } from "../../src/Api";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("integration config", () => {
|
||||
it("should create an entity", async () => {
|
||||
const app = createApp();
|
||||
await app.build();
|
||||
const api = new Api({
|
||||
host: "http://localhost",
|
||||
fetcher: app.server.request as typeof fetch,
|
||||
});
|
||||
|
||||
// create entity
|
||||
await api.system.addConfig("data", "entities.posts", {
|
||||
config: { sort_field: "id", sort_dir: "asc" },
|
||||
fields: { id: { type: "primary" }, asdf: { type: "text" } },
|
||||
type: "regular",
|
||||
});
|
||||
|
||||
expect(app.em.entities.map((e) => e.name)).toContain("posts");
|
||||
});
|
||||
});
|
||||
@@ -1,56 +1,131 @@
|
||||
import { describe, test } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { Guard } from "../../src/auth";
|
||||
import { EventManager } from "../../src/core/events";
|
||||
import { EntityManager } from "../../src/data";
|
||||
import { AppMedia } from "../../src/media/AppMedia";
|
||||
import { MediaController } from "../../src/media/api/MediaController";
|
||||
import { getDummyConnection } from "../helper";
|
||||
/// <reference types="@types/bun" />
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { registries } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { mergeObject, randomString } from "../../src/core/utils";
|
||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { assetsPath, assetsTmpPath } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
|
||||
/**
|
||||
* R2
|
||||
* value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null,
|
||||
* Node writefile
|
||||
* data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView> | Stream,
|
||||
*/
|
||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||
describe.skipIf(ALL_TESTS)("MediaController", () => {
|
||||
test("..", async () => {
|
||||
const ctx: any = {
|
||||
em: new EntityManager([], dummyConnection, []),
|
||||
guard: new Guard(),
|
||||
emgr: new EventManager(),
|
||||
server: new Hono()
|
||||
};
|
||||
beforeAll(() => {
|
||||
//disableConsoleLog();
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
});
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
const media = new AppMedia(
|
||||
// @ts-ignore
|
||||
{
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "s3",
|
||||
config: {
|
||||
access_key: process.env.R2_ACCESS_KEY as string,
|
||||
secret_access_key: process.env.R2_SECRET_ACCESS_KEY as string,
|
||||
url: process.env.R2_URL as string
|
||||
}
|
||||
}
|
||||
},
|
||||
ctx
|
||||
);
|
||||
await media.build();
|
||||
const app = new MediaController(media).getController();
|
||||
const path = `${assetsPath}/image.png`;
|
||||
|
||||
const file = Bun.file(`${import.meta.dir}/adapters/icon.png`);
|
||||
console.log("file", file);
|
||||
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||
const app = createApp({
|
||||
config: {
|
||||
media: mergeObject(
|
||||
{
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: assetsTmpPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
mediaOverride,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
return app;
|
||||
}
|
||||
|
||||
function makeName(ext: string) {
|
||||
return randomString(10) + "." + ext;
|
||||
}
|
||||
|
||||
describe("MediaController", () => {
|
||||
test("accepts direct", async () => {
|
||||
const app = await makeApp();
|
||||
|
||||
const file = Bun.file(path);
|
||||
const name = makeName("png");
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
expect(result.name).toBe(name);
|
||||
|
||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||
expect(destFile.exists()).resolves.toBe(true);
|
||||
await destFile.delete();
|
||||
});
|
||||
|
||||
test("accepts form data", async () => {
|
||||
const app = await makeApp();
|
||||
|
||||
const file = Bun.file(path);
|
||||
const name = makeName("png");
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
|
||||
await app.request("/upload/test.png", {
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: file
|
||||
body: form,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
expect(result.name).toBe(name);
|
||||
|
||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||
expect(destFile.exists()).resolves.toBe(true);
|
||||
await destFile.delete();
|
||||
});
|
||||
|
||||
test("limits body", async () => {
|
||||
const app = await makeApp({ storage: { body_max_size: 1 } });
|
||||
|
||||
const file = await Bun.file(path);
|
||||
const name = makeName("png");
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
});
|
||||
|
||||
expect(res.status).toBe(413);
|
||||
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
||||
});
|
||||
|
||||
test("audio files", async () => {
|
||||
const app = await makeApp();
|
||||
const file = Bun.file(`${assetsPath}/test.mp3`);
|
||||
const name = makeName("mp3");
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
expect(result.data.mime_type).toStartWith("audio/mpeg");
|
||||
expect(result.name).toBe(name);
|
||||
|
||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||
expect(destFile.exists()).resolves.toBe(true);
|
||||
await destFile.delete();
|
||||
});
|
||||
|
||||
test("text files", async () => {
|
||||
const app = await makeApp();
|
||||
const file = Bun.file(`${assetsPath}/test.txt`);
|
||||
const name = makeName("txt");
|
||||
const res = await app.server.request("/api/media/upload/" + name, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
expect(result.data.mime_type).toStartWith("text/plain");
|
||||
expect(result.name).toBe(name);
|
||||
|
||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||
expect(destFile.exists()).resolves.toBe(true);
|
||||
await destFile.delete();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type FileBody, Storage, type StorageAdapter } from "../../src/media/storage/Storage";
|
||||
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||
import * as StorageEvents from "../../src/media/storage/events";
|
||||
import { StorageAdapter } from "media/storage/StorageAdapter";
|
||||
|
||||
class TestAdapter implements StorageAdapter {
|
||||
class TestAdapter extends StorageAdapter {
|
||||
files: Record<string, FileBody> = {};
|
||||
|
||||
getName() {
|
||||
@@ -60,8 +61,8 @@ describe("Storage", async () => {
|
||||
|
||||
test("uploads a file", async () => {
|
||||
const {
|
||||
meta: { type, size }
|
||||
} = await storage.uploadFile("hello", "world.txt");
|
||||
meta: { type, size },
|
||||
} = await storage.uploadFile("hello" as any, "world.txt");
|
||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||
});
|
||||
|
||||
@@ -71,6 +72,7 @@ describe("Storage", async () => {
|
||||
});
|
||||
|
||||
test("events were fired", async () => {
|
||||
await storage.emgr.executeAsyncs();
|
||||
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
||||
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
||||
// @todo: file access must be tested in controllers
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import * as assert from "node:assert/strict";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import { Miniflare } from "miniflare";
|
||||
|
||||
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
|
||||
console.log = async (message: any) => {
|
||||
const tty = createWriteStream("/dev/tty");
|
||||
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
|
||||
return tty.write(`${msg}\n`);
|
||||
};
|
||||
|
||||
test("what", async () => {
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
r2Buckets: ["BUCKET"]
|
||||
});
|
||||
|
||||
const bucket = await mf.getR2Bucket("BUCKET");
|
||||
console.log(await bucket.put("count", "1"));
|
||||
|
||||
const object = await bucket.get("count");
|
||||
if (object) {
|
||||
/*const headers = new Headers();
|
||||
object.writeHttpMetadata(headers);
|
||||
headers.set("etag", object.httpEtag);*/
|
||||
console.log("yo -->", await object.text());
|
||||
|
||||
assert.strictEqual(await object.text(), "1");
|
||||
}
|
||||
|
||||
await mf.dispose();
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user