mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
730 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c596781ff | |||
| 43cbdb3010 | |||
| b3c9f258de | |||
| feb3911d46 | |||
| ab545d3ca2 | |||
| 06c3a6f89b | |||
| 898a825f6e | |||
| a5ddf6aa93 | |||
| 5aa286298c | |||
| 55cb2bacef | |||
| e6ea3f64d7 | |||
| a5f5ea2084 | |||
| 6fb8e9a0e7 | |||
| abc6de6ad3 | |||
| 71b9a0f546 | |||
| 038ec0d672 | |||
| 7832328a50 | |||
| e5bfc602e1 | |||
| 0318e595f2 | |||
| 80b0716a4a | |||
| 3894271b17 | |||
| 8bf8b22e57 | |||
| 367d5878df | |||
| fb429e57e2 | |||
| 9008f9c6a3 | |||
| 6321a9935a | |||
| eb22289460 | |||
| c3198394de | |||
| 51237f5e1a | |||
| 01483b912f | |||
| 6288faef33 | |||
| 384e337bbd | |||
| cc1e1976f7 | |||
| 224d98879a | |||
| d1522c97ea | |||
| 8805f017a1 | |||
| 0ef06375c0 | |||
| 3d5c2cd3a8 | |||
| fc60dfd042 | |||
| c8ee30b3da | |||
| b85c3a8a9b | |||
| e20c2ef92c | |||
| 5873baad3c | |||
| aca162f9c8 | |||
| 9f0997c28a | |||
| 1c114d9533 | |||
| 68063986d1 | |||
| 53f8609e9d | |||
| 833bfa5049 | |||
| defcdfef0b | |||
| 348ac7f8b0 | |||
| 16b1cde8ea | |||
| 436b195242 | |||
| ccd504440e | |||
| 023f5391e4 | |||
| 11e3488917 | |||
| 9685c5d4e1 | |||
| 9b008be512 | |||
| 5352331464 | |||
| d9bce049ef | |||
| 0e4f25cf11 | |||
| 4a5bdade8d | |||
| 047bb80a97 | |||
| d826cb23a3 | |||
| 23a9e7c278 | |||
| a9fe43130e | |||
| 3b7ec06703 | |||
| 55a124b519 | |||
| dc3fe330d7 | |||
| d541167fb3 | |||
| 7e8b357bbb | |||
| 521d8bc6ff | |||
| 19f1941544 | |||
| f9373bd0e6 | |||
| 5384738b67 | |||
| d82e28e6b1 | |||
| c38bc95b1d | |||
| 6651fce099 | |||
| 91a1fc8a0c | |||
| d00e5fe68c | |||
| 6faf0d421f | |||
| 19e51fae63 | |||
| 7e01cc6101 | |||
| 103cbd11df | |||
| 03d3b8e407 | |||
| a97a79f11e | |||
| 90b4de7093 | |||
| 81f3914e7f | |||
| 5163e2142e | |||
| 15ed075de3 | |||
| 09b9a5e857 | |||
| 4f7925dfd8 | |||
| 21e970cd8c | |||
| 40b70e7a20 | |||
| 9958dd7308 | |||
| fef77eea61 | |||
| ed41887d74 | |||
| 8ba4793160 | |||
| 65e17db2a2 | |||
| e21b5e5b55 | |||
| 959dee013e | |||
| a39e76a440 | |||
| acc10377ca | |||
| 506c7d84cc | |||
| 7ba542c040 | |||
| b784d8611a | |||
| a21d4ad6a7 | |||
| 731b7a7e8d | |||
| e56fc9c368 | |||
| d1aa2da5b1 | |||
| 52fe9aa085 | |||
| 065821d9a5 | |||
| 0fc817382a | |||
| 2e6b95e484 | |||
| 4e31f48917 | |||
| b2872eb196 | |||
| 3d804f5ac5 | |||
| da025efee1 | |||
| ab1fa4c895 | |||
| ebe3bc2ff5 | |||
| f792d8b93e | |||
| 319469f44b | |||
| 1359741e5f | |||
| 8f4de33a76 | |||
| 43dbc856ce | |||
| 5a8f2b4894 | |||
| 2627213de7 | |||
| 36e1bb1867 | |||
| 0cf1d86213 | |||
| 3fac740771 | |||
| 5e5dc62304 | |||
| c1d6384271 | |||
| f2bd04770a | |||
| 41d43126cb | |||
| a39407c9a3 | |||
| 5d6046b149 | |||
| 000f8d20a8 | |||
| 9e7547e787 | |||
| c3ae4a3999 | |||
| 5c3eeb7642 | |||
| a2fa11ccd0 | |||
| fdee39da62 | |||
| 16a3a67634 | |||
| 2b5e1771de | |||
| a16e017e39 | |||
| 079d613b2e | |||
| ff56d616d9 | |||
| 5b3d36c527 | |||
| 2885fea077 | |||
| 5ce22162c3 | |||
| 3f55c0f2c5 | |||
| 21f642161d | |||
| 7912729159 | |||
| d3e87be604 | |||
| ba3d11edab | |||
| 9ec31373e1 | |||
| 70b25a9f9b | |||
| b6717f0237 | |||
| c57f3e8070 | |||
| c2f4f92d1a | |||
| 6eb8525656 | |||
| e2ebb57564 | |||
| 2b7bbc5df0 | |||
| 8766b742c6 | |||
| 3befa43a9e | |||
| a842808464 | |||
| bd1ef8ed57 | |||
| a0019e5500 | |||
| 8ec67a5492 | |||
| 960f7c3fb2 | |||
| 793c214e6d | |||
| 4094004b83 | |||
| 300d86ff7c | |||
| 4c9b662f6f | |||
| a862cfdcf1 | |||
| de62f4e729 | |||
| 7e399830e5 | |||
| ee2ab982df | |||
| 5be55a6fa6 | |||
| 341eb13425 | |||
| 68fbb6e933 | |||
| 8b36985252 | |||
| ff86240b0e | |||
| 40bbdb904f | |||
| a333d537b0 | |||
| 108c108d82 | |||
| 4575d89cfe | |||
| 0ca0828581 | |||
| 07a57ebf67 | |||
| e9f1241ec3 | |||
| 80903d4ffa | |||
| c8290802e9 | |||
| ac6cd4a900 | |||
| a91ddb1ec3 | |||
| ed47c5bf51 | |||
| dbb19a27f4 | |||
| 71a17696eb | |||
| 2dbafebcea | |||
| 84d4635ec3 | |||
| e9c92b6086 | |||
| 5fa7601ad5 | |||
| 2c7054c317 | |||
| 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 | |||
| 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 | |||
| ac891a4b61 | |||
| 412eab6653 | |||
| 2fb07698d5 | |||
| 64e400c4f1 | |||
| 86d6e891a8 | |||
| 5e5f0ef70f | |||
| 104885ea5c | |||
| ffbb61d58a | |||
| bd48bb7a18 | |||
| a298b65abf | |||
| 7b86611f35 | |||
| 9e316a92f6 | |||
| 12ef72d595 | |||
| 75c29ae5e2 | |||
| f0551f50d1 | |||
| 03aac3437b | |||
| f07f3d1007 | |||
| c833b2b539 | |||
| 6070394cca | |||
| 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 | |||
| 6f776aeff7 |
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"bknd": {
|
||||||
|
"url": "http://localhost:28623/api/system/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: bknd
|
||||||
|
ports:
|
||||||
|
- 5430:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
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.3.3"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./app
|
||||||
|
run: bun install #--linker=hoisted
|
||||||
|
|
||||||
|
- 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
|
||||||
+7
-2
@@ -27,6 +27,11 @@ packages/media/.env
|
|||||||
.npmrc
|
.npmrc
|
||||||
/.verdaccio
|
/.verdaccio
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
.git_old
|
.git_old
|
||||||
docker/tmp
|
docker/tmp
|
||||||
|
.debug
|
||||||
|
.history
|
||||||
|
.aider*
|
||||||
|
.vercel
|
||||||
|
|||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"biome.enabled": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
//"source.organizeImports.biome": "explicit",
|
||||||
|
"source.fixAll.biome": "explicit"
|
||||||
|
},
|
||||||
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
|
"typescript.preferences.autoImportFileExcludePatterns": [
|
||||||
|
"**/dist/**",
|
||||||
|
"**/node_modules/**/dist/**",
|
||||||
|
"**/node_modules/**/!(src|lib|esm)/**" // optional, stricter
|
||||||
|
],
|
||||||
|
"typescript.preferences.includePackageJsonAutoImports": "on",
|
||||||
|
"typescript.tsserver.watchOptions": {
|
||||||
|
"excludeDirectories": ["**/dist", "**/node_modules/**/dist"]
|
||||||
|
}
|
||||||
|
}
|
||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
# Contributing to bknd
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to bknd. This guide will help you get started, understand the codebase, and submit contributions that align with the project's direction.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Before You Start](#before-you-start)
|
||||||
|
- [Important Resources](#important-resources)
|
||||||
|
- [Development Setup](#development-setup)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Understanding the Codebase](#understanding-the-codebase)
|
||||||
|
- [Running Tests](#running-tests)
|
||||||
|
- [Code Style](#code-style)
|
||||||
|
- [Submitting Changes](#submitting-changes)
|
||||||
|
- [AI-Generated Code](#ai-generated-code)
|
||||||
|
- [Contributing to Documentation](#contributing-to-documentation)
|
||||||
|
- [Reporting Bugs](#reporting-bugs)
|
||||||
|
- [Getting Help](#getting-help)
|
||||||
|
- [Contributors](#contributors)
|
||||||
|
- [Maintainers](#maintainers)
|
||||||
|
|
||||||
|
## Before You Start
|
||||||
|
|
||||||
|
**Open a GitHub Issue before writing code.** This is the preferred way to propose any change. It lets maintainers and the community align on the approach before time is spent on implementation. If you have discussed a contribution in the [Discord server](https://discord.com/invite/952SFk8Tb8) instead, include a link to the relevant Discord thread in your pull request. Pull requests submitted without a corresponding issue or Discord discussion may be closed.
|
||||||
|
|
||||||
|
**Unsolicited architectural changes will be closed.** The internal architecture of bknd is intentional. Refactors, restructuring, or changes to core patterns must be discussed and approved before any code is written.
|
||||||
|
|
||||||
|
Contributions that are generally welcome (after opening an issue):
|
||||||
|
|
||||||
|
- Bug fixes
|
||||||
|
- New adapters, strategies, or storage backends
|
||||||
|
- Documentation improvements
|
||||||
|
- New examples
|
||||||
|
- Test coverage improvements
|
||||||
|
|
||||||
|
**A note on versioning**: bknd is pre-1.0 and under active development. Full backward compatibility is not guaranteed before v1.0.0. Contributors should be aware that APIs and internal interfaces may change between releases.
|
||||||
|
|
||||||
|
## Important Resources
|
||||||
|
|
||||||
|
- **Documentation**: https://docs.bknd.io
|
||||||
|
- **Issue Tracker**: https://github.com/bknd-io/bknd/issues
|
||||||
|
- **Discord**: https://discord.com/invite/952SFk8Tb8
|
||||||
|
- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 22.13 or higher
|
||||||
|
- Bun 1.3.3
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
1. Fork the repository and clone your fork.
|
||||||
|
|
||||||
|
2. Install dependencies from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Navigate to the main app package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Copy the example environment file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
The default `.env.example` includes a file-based SQLite database (`file:.db/dev.db`). You can change `VITE_DB_URL` to `:memory:` for an in-memory database instead.
|
||||||
|
|
||||||
|
5. Start the dev server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The dev server runs on `http://localhost:28623` using Vite with Hono.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
This is a Bun monorepo using workspaces. The vast majority of the code lives in `app/`.
|
||||||
|
|
||||||
|
```
|
||||||
|
bknd/
|
||||||
|
app/ # Main "bknd" npm package (this is where most work happens)
|
||||||
|
src/
|
||||||
|
adapter/ # Runtime/framework adapters (node, bun, cloudflare, nextjs, astro, etc.)
|
||||||
|
auth/ # Authentication module (strategies, sessions, user pool)
|
||||||
|
cli/ # CLI commands
|
||||||
|
core/ # Shared utilities, event system, drivers, server helpers
|
||||||
|
data/ # Data module (entities, fields, relations, queries, schema)
|
||||||
|
flows/ # Workflow engine and tasks
|
||||||
|
media/ # Media module (storage adapters, uploads)
|
||||||
|
modes/ # Configuration modes (code, hybrid, db)
|
||||||
|
modules/ # Module system, MCP server, permissions framework
|
||||||
|
plugins/ # Built-in plugins (auth, data, cloudflare, dev)
|
||||||
|
ui/ # All frontend code
|
||||||
|
client/ # TypeScript SDK and React hooks (exported as bknd/client)
|
||||||
|
elements/ # React components for auth/media (exported as bknd/elements)
|
||||||
|
(everything else) # Admin UI (exported as bknd/ui)
|
||||||
|
App.ts # Central application orchestrator
|
||||||
|
index.ts # Main package exports
|
||||||
|
__test__/ # Unit tests (mirrors src/ structure)
|
||||||
|
e2e/ # End-to-end tests (Playwright)
|
||||||
|
build.ts # Build script (tsup/esbuild)
|
||||||
|
build.cli.ts # CLI build script
|
||||||
|
packages/ # Small satellite packages
|
||||||
|
cli/ # Standalone CLI package (bknd-cli)
|
||||||
|
plasmic/ # Plasmic integration
|
||||||
|
postgres/ # Postgres helper
|
||||||
|
sqlocal/ # SQLocal helper
|
||||||
|
docs/ # Documentation site (Next.js + Fumadocs, deployed to docs.bknd.io)
|
||||||
|
examples/ # Example projects across runtimes and frameworks
|
||||||
|
docker/ # Docker configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Understanding the Codebase
|
||||||
|
|
||||||
|
### Path Aliases
|
||||||
|
|
||||||
|
The project uses TypeScript path aliases defined in `app/tsconfig.json`. Imports like `core/utils`, `data/connection`, or `auth/authenticate` resolve to directories under `app/src/`. When reading source code, keep this in mind -- an import like `import { Connection } from "data/connection"` refers to `app/src/data/connection`, not an external package.
|
||||||
|
|
||||||
|
### Module System
|
||||||
|
|
||||||
|
bknd is built around four core modules, each with its own schema, API routes, and permissions:
|
||||||
|
|
||||||
|
- **Data** -- entity definitions, field types, relations, queries (backed by Kysely)
|
||||||
|
- **Auth** -- authentication strategies, sessions, user management
|
||||||
|
- **Media** -- file storage with pluggable adapters (S3, Cloudinary, local filesystem, etc.)
|
||||||
|
- **Flows** -- workflow automation and task execution
|
||||||
|
|
||||||
|
These modules are managed by the `ModuleManager` (in `app/src/modules/`), which handles configuration, building, and lifecycle.
|
||||||
|
|
||||||
|
### Adapter Pattern
|
||||||
|
|
||||||
|
Adapters in `app/src/adapter/` allow bknd to run on different runtimes and frameworks. Each adapter provides the glue between bknd's Hono-based server and a specific environment (Node, Bun, Cloudflare Workers, Next.js, Astro, etc.).
|
||||||
|
|
||||||
|
### Plugin System
|
||||||
|
|
||||||
|
Plugins (in `app/src/plugins/`) hook into the app lifecycle via callbacks like `beforeBuild`, `onBuilt`, `onServerInit`, and `schema`. They can add entities, register routes, and extend behavior.
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
The build is handled by a custom `app/build.ts` script using tsup (esbuild under the hood). It builds four targets in parallel:
|
||||||
|
|
||||||
|
- **API** -- the core backend
|
||||||
|
- **UI** -- the admin interface
|
||||||
|
- **Elements** -- standalone React components
|
||||||
|
- **Adapters** -- all runtime/framework adapters
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
All test commands run from the `app/` directory.
|
||||||
|
|
||||||
|
| Command | Runner | What it runs |
|
||||||
|
|---|---|---|
|
||||||
|
| `bun run test` | Bun test | Unit tests (`*.spec.ts` files) |
|
||||||
|
| `bun run test:node` | Vitest | Node-specific tests (`*.vi-test.ts`, `*.vitest.ts`) |
|
||||||
|
| `bun run test:e2e` | Playwright | End-to-end tests (`*.e2e-spec.ts` in `e2e/`) |
|
||||||
|
| `bun run types` | TypeScript | Type checking (no emit) |
|
||||||
|
|
||||||
|
CI runs both Bun and Node tests, with a Postgres 17 service for database integration tests.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
The project uses **Biome** for formatting and linting.
|
||||||
|
|
||||||
|
- 3-space indentation for JavaScript/TypeScript
|
||||||
|
- 100-character line width
|
||||||
|
- Spaces (not tabs)
|
||||||
|
|
||||||
|
To format and lint, run from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bunx biome format --write ./app
|
||||||
|
bunx biome lint --changed --write ./app
|
||||||
|
```
|
||||||
|
|
||||||
|
Run both before submitting a PR. CI will catch style issues, but it is better to fix them locally first.
|
||||||
|
|
||||||
|
## Submitting Changes
|
||||||
|
|
||||||
|
1. Open a GitHub Issue describing your proposed change, or link to a Discord thread where it was discussed. Wait for feedback before writing code.
|
||||||
|
2. Fork the repo and create a branch from `main`.
|
||||||
|
3. Keep changes focused and minimal. One PR per issue.
|
||||||
|
4. Add or update tests if applicable.
|
||||||
|
5. Run the test suite and linter before pushing.
|
||||||
|
6. Open a pull request against `main`. Reference the issue number in the PR description.
|
||||||
|
|
||||||
|
Expect a response from maintainers, but be patient -- this is an actively developed project and review may take some time.
|
||||||
|
|
||||||
|
## AI-Generated Code
|
||||||
|
|
||||||
|
If you use AI tools to write or assist with your code, you must:
|
||||||
|
|
||||||
|
- **Fully review all AI-generated code yourself** before submitting. You are responsible for understanding and standing behind every line in your PR.
|
||||||
|
- **Include the prompts used** in the PR description. This gives maintainers additional context for reviewing the code.
|
||||||
|
|
||||||
|
Pull requests with unreviewed AI-generated code will be closed.
|
||||||
|
|
||||||
|
## Contributing to Documentation
|
||||||
|
|
||||||
|
The documentation site lives in the `docs/` directory and is a **separate Next.js application** built with [Fumadocs](https://fumadocs.dev). It uses **npm** (not Bun) as its package manager.
|
||||||
|
|
||||||
|
### Docs Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docs
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The docs dev server runs on `http://localhost:3000`.
|
||||||
|
|
||||||
|
### Where Documentation Lives
|
||||||
|
|
||||||
|
Documentation content is written in MDX and located in `docs/content/docs/`. The directory is organized into:
|
||||||
|
|
||||||
|
- `(documentation)/` -- core documentation pages
|
||||||
|
- `guide/` -- guides and tutorials
|
||||||
|
- `api-reference/` -- API reference (partially auto-generated from OpenAPI)
|
||||||
|
|
||||||
|
To add or edit a page, create or modify an `.mdx` file in the appropriate directory. Page metadata is defined via frontmatter at the top of each file. Navigation order is controlled by `meta.json` files in each directory.
|
||||||
|
|
||||||
|
### Building the Docs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This generates the OpenAPI and MCP reference pages before building the Next.js site. Make sure the build succeeds locally before submitting a docs PR.
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
|
||||||
|
Open a GitHub Issue with:
|
||||||
|
|
||||||
|
- A clear title describing the problem.
|
||||||
|
- Steps to reproduce the bug.
|
||||||
|
- Expected behavior vs. actual behavior.
|
||||||
|
- Your environment (runtime, database, adapter, bknd version).
|
||||||
|
- Any relevant error messages or logs.
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- **Discord**: https://discord.com/invite/952SFk8Tb8
|
||||||
|
- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
|
||||||
|
- **GitHub Issues**: https://github.com/bknd-io/bknd/issues
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thank you to everyone who has contributed to bknd.
|
||||||
|
|
||||||
|
<a href="https://github.com/bknd-io/bknd/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=bknd-io/bknd" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Made with [contrib.rocks](https://contrib.rocks).
|
||||||
|
|
||||||
|
## Maintainers
|
||||||
|
|
||||||
|
- **Dennis** ([@dswbx](https://github.com/dswbx)) -- Creator and maintainer of bknd
|
||||||
|
- **Cameron Pak** ([@cameronapak](https://github.com/cameronapak)) -- Maintainer of bknd docs
|
||||||
+169
-77
@@ -1,110 +1,202 @@
|
|||||||
# Functional Source License, Version 1.1, MIT Future License
|
|
||||||
|
|
||||||
## Abbreviation
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
FSL-1.1-MIT
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
## Notice
|
1. Definitions.
|
||||||
|
|
||||||
Copyright 2025 Dennis Senn
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
## Terms and Conditions
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
### Licensor ("We")
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
The party offering the Software under these Terms and Conditions.
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
### The Software
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
The "Software" is each version of the software that we make available under
|
"Object" form shall mean any form resulting from mechanical
|
||||||
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
transformation or translation of a Source form, including but
|
||||||
Conditions with the Software.
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
### License Grant
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
Subject to your compliance with this License Grant and the Patents,
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
Redistribution and Trademark clauses below, we hereby grant you the right to
|
form, that is based on (or derived from) the Work and for which the
|
||||||
use, copy, modify, create derivative works, publicly perform, publicly display
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
and redistribute the Software for any Permitted Purpose identified below.
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
### Permitted Purpose
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
means making the Software available to others in a commercial product or
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
service that:
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
1. substitutes for the Software;
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
2. substitutes for any other product or service we offer using the Software
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
that exists as of the date we make the Software available; or
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
3. offers the same or substantially similar functionality as the Software.
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
Permitted Purposes specifically include using the Software:
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
1. for your internal use and access;
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
2. for non-commercial education;
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
3. for non-commercial research; and
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
4. in connection with professional services that you provide to a licensee
|
You may add Your own copyright statement to Your modifications and
|
||||||
using the Software in accordance with these Terms and Conditions.
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
### Patents
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
To the extent your use for a Permitted Purpose would necessarily infringe our
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
patents, the license grant above includes a license under our patents. If you
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
make a claim against any party that the Software infringes or contributes to
|
except as required for reasonable and customary use in describing the
|
||||||
the infringement of any patent, then your patent license to the Software ends
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
immediately.
|
|
||||||
|
|
||||||
### Redistribution
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
The Terms and Conditions apply to all copies, modifications and derivatives of
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
the Software.
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
If you redistribute any copies, modifications or derivatives of the Software,
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
you must include a copy of or a link to these Terms and Conditions and not
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
remove any copyright notices provided in or with the Software.
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
### Disclaimer
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
||||||
|
|
||||||
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
To apply the Apache License to your work, attach the following
|
||||||
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
### Trademarks
|
Copyright 2026 Dennis Senn
|
||||||
|
|
||||||
Except for displaying the License Details and identifying us as the origin of
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
the Software, you have no right under these Terms and Conditions to use our
|
you may not use this file except in compliance with the License.
|
||||||
trademarks, trade names, service marks or product names.
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
## Grant of Future License
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
We hereby irrevocably grant you an additional license to use the Software under
|
Unless required by applicable law or agreed to in writing, software
|
||||||
the MIT license that is effective on the second anniversary of the date we make
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
the Software available. On or after that date, you may use the Software under
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
the MIT license, in which case the following will apply:
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,30 +1,55 @@
|
|||||||
[](https://npmjs.org/package/bknd)
|
[](https://npmjs.org/package/bknd)
|
||||||
[](https://www.npmjs.com/package/bknd)
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<p align="center" width="100%">
|
<p align="center" width="100%">
|
||||||
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema">
|
<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>
|
<strong>⭐ Live Demo</strong>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
bknd simplifies app development by providing a fully functional 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!
|
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, Origin Private File System (OPFS)
|
||||||
|
* **Deployment**: Standalone, Docker, Cloudflare Workers, Vercel, Netlify, Deno Deploy, AWS Lambda, Valtown etc.
|
||||||
|
|
||||||
**For documentation and examples, please visit https://docs.bknd.io.**
|
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||||
|
|
||||||
> [!WARNING]
|
> [!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
|
> Please keep in mind that **bknd** is still under active development
|
||||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
|
||||||
## Size
|
## 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.
|
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
|
## 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.
|
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.
|
||||||
|
|
||||||
@@ -37,10 +62,11 @@ Creating digital products always requires developing both the backend (the logic
|
|||||||
* **Media**: Effortlessly manage and serve all your media files.
|
* **Media**: Effortlessly manage and serve all your media files.
|
||||||
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
|
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
|
||||||
* 🌐 Built on Web Standards for maximum compatibility
|
* 🌐 Built on Web Standards for maximum compatibility
|
||||||
|
* 🛠️ MCP server, client and UI built-in to control your backend
|
||||||
* 🏃♂️ Multiple run modes
|
* 🏃♂️ Multiple run modes
|
||||||
* standalone using the CLI
|
* standalone using the CLI
|
||||||
* using a JavaScript runtime (Node, Bun, workerd)
|
* using a JavaScript runtime (Node, Bun, workerd)
|
||||||
* using a React framework (Astro, Remix, Next.js)
|
* using a React framework (Next.js, React Router, Astro)
|
||||||
* 📦 Official API and React SDK with type-safety
|
* 📦 Official API and React SDK with type-safety
|
||||||
* ⚛️ React elements for auto-configured authentication and media components
|
* ⚛️ React elements for auto-configured authentication and media components
|
||||||
|
|
||||||
@@ -78,7 +104,7 @@ export default function AdminPage() {
|
|||||||
### Using the REST API or TypeScript SDK (`bknd/client`)
|
### 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:
|
If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
|
||||||
```bash
|
```bash
|
||||||
curl -XGET <your-endpoint>/api/data/<entity>
|
curl -XGET <your-endpoint>/api/data/entity/<entity>
|
||||||
{
|
{
|
||||||
"data": [
|
"data": [
|
||||||
{ "id": 1, ... },
|
{ "id": 1, ... },
|
||||||
@@ -139,3 +165,19 @@ npx bknd run
|
|||||||
```bash
|
```bash
|
||||||
npm install bknd
|
npm install bknd
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) before getting started. It covers dev setup, project structure, testing, code style, and how to submit changes.
|
||||||
|
|
||||||
|
Before writing code, please open a GitHub Issue or start a conversation in [Discord](https://discord.com/invite/952SFk8Tb8) so we can align on the approach.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thank you to everyone who has contributed to bknd.
|
||||||
|
|
||||||
|
<a href="https://github.com/bknd-io/bknd/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=bknd-io/bknd" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Made with [contrib.rocks](https://contrib.rocks).
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# ===== 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=
|
||||||
|
PLUNK_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,5 @@
|
|||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
bknd.config.*
|
||||||
|
__test__/helper.d.ts
|
||||||
|
.env.local
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"typescript.preferences.includePackageJsonAutoImports": "off",
|
||||||
|
"typescript.suggest.autoImports": true,
|
||||||
|
"typescript.preferences.importModuleSpecifier": "relative",
|
||||||
|
"search.exclude": {
|
||||||
|
"**/dist/**": true,
|
||||||
|
"**/node_modules/**": true
|
||||||
|
},
|
||||||
|
"files.exclude": {
|
||||||
|
"**/dist/**": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+137
-4
@@ -1,15 +1,148 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterEach, describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
import { App } from "../src";
|
import { App, createApp } from "core/test/utils";
|
||||||
import { getDummyConnection } from "./helper";
|
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();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterEach(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("App tests", async () => {
|
describe("App tests", async () => {
|
||||||
test("boots and pongs", async () => {
|
test("boots and pongs", async () => {
|
||||||
const app = new App(dummyConnection);
|
const app = new App(dummyConnection);
|
||||||
await app.build();
|
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("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 +1,2 @@
|
|||||||
tmp/*
|
tmp/*
|
||||||
|
!tmp/.gitkeep
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
hello
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
||||||
|
import * as adapter from "adapter";
|
||||||
|
import { disableConsoleLog, enableConsoleLog, omitKeys } from "core/utils";
|
||||||
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
|
const stripConnection = <T extends Record<string, any>>(cfg: T) =>
|
||||||
|
omitKeys(cfg, ["connection"]);
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("adapter", () => {
|
||||||
|
describe("makeConfig", () => {
|
||||||
|
it("returns empty config for empty inputs", async () => {
|
||||||
|
const cases: Array<Parameters<typeof adapter.makeConfig>> = [
|
||||||
|
[{}],
|
||||||
|
[{}, { env: { TEST: "test" } }],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const args of cases) {
|
||||||
|
const cfg = await adapter.makeConfig(...(args as any));
|
||||||
|
expect(stripConnection(cfg)).toEqual({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges app output into config", async () => {
|
||||||
|
const cfg = await adapter.makeConfig(
|
||||||
|
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||||
|
{ env: { TEST: "test" } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stripConnection(cfg)).toEqual({
|
||||||
|
config: { server: { cors: { origin: "test" } } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows all properties in app() result", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
|
||||||
|
const cfg = await adapter.makeConfig(
|
||||||
|
{
|
||||||
|
app: (env) => ({
|
||||||
|
connection: { url: "test" },
|
||||||
|
config: { server: { cors: { origin: "test" } } },
|
||||||
|
options: { mode: "db" as const },
|
||||||
|
onBuilt: () => {
|
||||||
|
called();
|
||||||
|
expect(env).toEqual({ foo: "bar" });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ foo: "bar" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cfg.connection).toEqual({ url: "test" });
|
||||||
|
expect(cfg.config).toEqual({ server: { cors: { origin: "test" } } });
|
||||||
|
expect(cfg.options).toEqual({ mode: "db" });
|
||||||
|
|
||||||
|
await cfg.onBuilt?.({} as any);
|
||||||
|
expect(called).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("adapter test suites", () => {
|
||||||
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
makeApp: adapter.createFrameworkApp,
|
||||||
|
label: "framework app",
|
||||||
|
});
|
||||||
|
|
||||||
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
makeApp: adapter.createRuntimeApp,
|
||||||
|
label: "runtime app",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,21 +6,24 @@ describe("Api", async () => {
|
|||||||
it("should construct without options", () => {
|
it("should construct without options", () => {
|
||||||
const api = new Api();
|
const api = new Api();
|
||||||
expect(api.baseUrl).toBe("http://localhost");
|
expect(api.baseUrl).toBe("http://localhost");
|
||||||
expect(api.isAuthVerified()).toBe(false);
|
|
||||||
|
// 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", () => {
|
it("should ignore force verify if no claims given", () => {
|
||||||
const api = new Api({ verified: true });
|
const api = new Api({ verified: true });
|
||||||
expect(api.baseUrl).toBe("http://localhost");
|
expect(api.baseUrl).toBe("http://localhost");
|
||||||
expect(api.isAuthVerified()).toBe(false);
|
expect(api.isAuthVerified()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should construct from request (token)", async () => {
|
it("should construct from request (token)", async () => {
|
||||||
const token = await sign({ foo: "bar" }, "1234");
|
const token = await sign({ foo: "bar" }, "1234");
|
||||||
const request = new Request("http://example.com/test", {
|
const request = new Request("http://example.com/test", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const api = new Api({ request });
|
const api = new Api({ request });
|
||||||
expect(api.isAuthVerified()).toBe(false);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
@@ -35,14 +38,13 @@ describe("Api", async () => {
|
|||||||
const token = await sign({ foo: "bar" }, "1234");
|
const token = await sign({ foo: "bar" }, "1234");
|
||||||
const request = new Request("http://example.com/test", {
|
const request = new Request("http://example.com/test", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `auth=${token}`
|
Cookie: `auth=${token}`,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const api = new Api({ request });
|
const api = new Api({ request });
|
||||||
expect(api.isAuthVerified()).toBe(false);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
|
||||||
const params = api.getParams();
|
const params = api.getParams();
|
||||||
console.log(params);
|
|
||||||
expect(params.token).toBe(token);
|
expect(params.token).toBe(token);
|
||||||
expect(params.token_transport).toBe("cookie");
|
expect(params.token_transport).toBe("cookie");
|
||||||
expect(params.host).toBe("http://example.com");
|
expect(params.host).toBe("http://example.com");
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth/authorize/Guard";
|
||||||
import { parse } from "../../src/core/utils";
|
|
||||||
import { DataApi } from "../../src/data/api/DataApi";
|
import { DataApi } from "../../src/data/api/DataApi";
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { disableConsoleLog, enableConsoleLog, schemaToEm } from "../helper";
|
import { schemaToEm } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { parse } from "core/utils/schema";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -26,7 +27,7 @@ describe("DataApi", () => {
|
|||||||
|
|
||||||
it("returns result", async () => {
|
it("returns result", async () => {
|
||||||
const schema = proto.em({
|
const schema = proto.em({
|
||||||
posts: proto.entity("posts", { title: proto.text() })
|
posts: proto.entity("posts", { title: proto.text() }),
|
||||||
});
|
});
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
@@ -40,19 +41,17 @@ describe("DataApi", () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const res = (await app.request("/entity/posts")) as Response;
|
const res = (await app.request("/entity/posts")) as Response;
|
||||||
const { data } = await res.json();
|
const { data } = (await res.json()) as any;
|
||||||
expect(data.length).toEqual(3);
|
expect(data.length).toEqual(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore tests
|
// @ts-ignore tests
|
||||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 });
|
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
|
||||||
// @ts-ignore protected
|
|
||||||
api.fetcher = app.request as typeof fetch;
|
|
||||||
{
|
{
|
||||||
const req = api.readMany("posts", { select: ["title"] });
|
const req = api.readMany("posts", { select: ["title"] });
|
||||||
expect(req.request.method).toBe("GET");
|
expect(req.request.method).toBe("GET");
|
||||||
const res = await req;
|
const res = await req;
|
||||||
expect(res.data).toEqual(payload);
|
expect(res.data).toEqual(payload as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -60,11 +59,164 @@ describe("DataApi", () => {
|
|||||||
select: ["title"],
|
select: ["title"],
|
||||||
limit: 100000,
|
limit: 100000,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
sort: "id"
|
sort: "id",
|
||||||
});
|
});
|
||||||
expect(req.request.method).toBe("POST");
|
expect(req.request.method).toBe("POST");
|
||||||
const res = await req;
|
const res = await req;
|
||||||
expect(res.data).toEqual(payload);
|
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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/// <reference types="@types/bun" />
|
/// <reference types="@types/bun" />
|
||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it, mock } from "bun:test";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { getFileFromContext, isFile, isReadableStream } from "../../src/core/utils";
|
import { getFileFromContext, isFile, isReadableStream, s, jsc } from "core/utils";
|
||||||
import { MediaApi } from "../../src/media/api/MediaApi";
|
import { MediaApi } from "media/api/MediaApi";
|
||||||
import { assetsPath, assetsTmpPath } from "../helper";
|
import { assetsPath, assetsTmpPath } from "../helper";
|
||||||
|
|
||||||
const mockedBackend = new Hono()
|
const mockedBackend = new Hono()
|
||||||
@@ -15,11 +15,11 @@ const mockedBackend = new Hono()
|
|||||||
.get("/file/:name", async (c) => {
|
.get("/file/:name", async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
const file = Bun.file(`${assetsPath}/${name}`);
|
const file = Bun.file(`${assetsPath}/${name}`);
|
||||||
return new Response(file, {
|
return new Response(new File([await file.bytes()], name, { type: file.type }), {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": file.type,
|
"Content-Type": file.type,
|
||||||
"Content-Length": file.size.toString()
|
"Content-Length": file.size.toString(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,26 +30,44 @@ describe("MediaApi", () => {
|
|||||||
// @ts-ignore tests
|
// @ts-ignore tests
|
||||||
const api = new MediaApi({
|
const api = new MediaApi({
|
||||||
host,
|
host,
|
||||||
basepath
|
basepath,
|
||||||
});
|
});
|
||||||
expect(api.getFileUploadUrl({ path: "path" })).toBe(`${host}${basepath}/upload/path`);
|
expect(api.getFileUploadUrl({ path: "path" } as any)).toBe(`${host}${basepath}/upload/path`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct upload headers", () => {
|
it("should have correct upload headers", () => {
|
||||||
// @ts-ignore tests
|
// @ts-ignore tests
|
||||||
const api = new MediaApi({
|
const api = new MediaApi({
|
||||||
token: "token"
|
token: "token",
|
||||||
|
token_transport: "header",
|
||||||
});
|
});
|
||||||
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
|
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 () => {
|
it("should get file: native", async () => {
|
||||||
const name = "image.png";
|
const name = "image.png";
|
||||||
const path = `${assetsTmpPath}/${name}`;
|
const path = `${assetsTmpPath}/${name}`;
|
||||||
const res = await mockedBackend.request("/api/media/file/" + name);
|
const res = await mockedBackend.request("/api/media/file/" + name);
|
||||||
await Bun.write(path, res);
|
await Bun.write(path, res);
|
||||||
|
|
||||||
const file = await Bun.file(path);
|
const file = Bun.file(path);
|
||||||
expect(file.size).toBeGreaterThan(0);
|
expect(file.size).toBeGreaterThan(0);
|
||||||
expect(file.type).toBe("image/png");
|
expect(file.type).toBe("image/png");
|
||||||
await file.delete();
|
await file.delete();
|
||||||
@@ -80,7 +98,7 @@ describe("MediaApi", () => {
|
|||||||
expect(isReadableStream(res.body)).toBe(true);
|
expect(isReadableStream(res.body)).toBe(true);
|
||||||
expect(isReadableStream(res.res.body)).toBe(true);
|
expect(isReadableStream(res.res.body)).toBe(true);
|
||||||
|
|
||||||
const blob = await res.res.blob();
|
const blob = (await res.res.blob()) as File;
|
||||||
expect(isFile(blob)).toBe(true);
|
expect(isFile(blob)).toBe(true);
|
||||||
expect(blob.size).toBeGreaterThan(0);
|
expect(blob.size).toBeGreaterThan(0);
|
||||||
expect(blob.type).toBe("image/png");
|
expect(blob.type).toBe("image/png");
|
||||||
@@ -92,10 +110,11 @@ describe("MediaApi", () => {
|
|||||||
const api = new MediaApi({}, mockedBackend.request);
|
const api = new MediaApi({}, mockedBackend.request);
|
||||||
|
|
||||||
const name = "image.png";
|
const name = "image.png";
|
||||||
const res = await api.getFileStream(name);
|
const res = await api.getFile(name);
|
||||||
expect(isReadableStream(res)).toBe(true);
|
const stream = await api.getFileStream(name);
|
||||||
|
expect(isReadableStream(stream)).toBe(true);
|
||||||
|
|
||||||
const blob = await new Response(res).blob();
|
const blob = (await res.res.blob()) as File;
|
||||||
expect(isFile(blob)).toBe(true);
|
expect(isFile(blob)).toBe(true);
|
||||||
expect(blob.size).toBeGreaterThan(0);
|
expect(blob.size).toBeGreaterThan(0);
|
||||||
expect(blob.type).toBe("image/png");
|
expect(blob.type).toBe("image/png");
|
||||||
@@ -103,8 +122,12 @@ describe("MediaApi", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should upload file in various ways", async () => {
|
it("should upload file in various ways", async () => {
|
||||||
// @ts-ignore tests
|
const api = new MediaApi(
|
||||||
const api = new MediaApi({}, mockedBackend.request);
|
{
|
||||||
|
upload_fetcher: mockedBackend.request,
|
||||||
|
},
|
||||||
|
mockedBackend.request,
|
||||||
|
);
|
||||||
const file = Bun.file(`${assetsPath}/image.png`);
|
const file = Bun.file(`${assetsPath}/image.png`);
|
||||||
|
|
||||||
async function matches(req: Promise<any>, filename: string) {
|
async function matches(req: Promise<any>, filename: string) {
|
||||||
@@ -132,15 +155,38 @@ describe("MediaApi", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// upload via readable from bun
|
// upload via readable from bun
|
||||||
await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
await matches(api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
||||||
|
|
||||||
// upload via readable from response
|
// upload via readable from response
|
||||||
{
|
{
|
||||||
const response = (await mockedBackend.request(url)) as Response;
|
const response = (await mockedBackend.request(url)) as Response;
|
||||||
await matches(
|
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png");
|
||||||
await api.upload(response.body!, { filename: "readable.png" }),
|
|
||||||
"readable.png"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should add overwrite query for entity upload", async (c) => {
|
||||||
|
const call = mock(() => null);
|
||||||
|
const hono = new Hono().post(
|
||||||
|
"/api/media/entity/:entity/:id/:field",
|
||||||
|
jsc("query", s.object({ overwrite: s.boolean().optional() })),
|
||||||
|
async (c) => {
|
||||||
|
const { overwrite } = c.req.valid("query");
|
||||||
|
expect(overwrite).toBe(true);
|
||||||
|
call();
|
||||||
|
return c.json({ ok: true });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const api = new MediaApi(
|
||||||
|
{
|
||||||
|
upload_fetcher: hono.request,
|
||||||
|
},
|
||||||
|
hono.request,
|
||||||
|
);
|
||||||
|
const file = Bun.file(`${assetsPath}/image.png`);
|
||||||
|
const res = await api.uploadToEntity("posts", 1, "cover", file as any, {
|
||||||
|
overwrite: true,
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
expect(call).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ describe("ModuleApi", () => {
|
|||||||
|
|
||||||
it("adds additional headers from options", () => {
|
it("adds additional headers from options", () => {
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
"X-Test": "123"
|
"X-Test": "123",
|
||||||
});
|
});
|
||||||
const api = new Api({ host, headers });
|
const api = new Api({ host, headers });
|
||||||
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
|
||||||
@@ -75,7 +75,7 @@ describe("ModuleApi", () => {
|
|||||||
it("uses search params", () => {
|
it("uses search params", () => {
|
||||||
const api = new Api({ host });
|
const api = new Api({ host });
|
||||||
const search = new URLSearchParams({
|
const search = new URLSearchParams({
|
||||||
foo: "bar"
|
foo: "bar",
|
||||||
});
|
});
|
||||||
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
|
||||||
});
|
});
|
||||||
@@ -89,6 +89,14 @@ describe("ModuleApi", () => {
|
|||||||
expect(api.delete("/").request.method).toEqual("DELETE");
|
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 error response
|
||||||
// @todo: test method shortcut functions
|
// @todo: test method shortcut functions
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,131 @@
|
|||||||
|
import { describe, expect, mock, test, beforeAll, afterAll } 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";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
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,61 @@
|
|||||||
|
import { AppEvents } from "App";
|
||||||
|
import { describe, test, expect, beforeAll, mock, 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] 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,42 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { code, hybrid } from "modes";
|
||||||
|
|
||||||
|
describe("modes", () => {
|
||||||
|
describe("code", () => {
|
||||||
|
test("verify base configuration", async () => {
|
||||||
|
const c = code({}) as any;
|
||||||
|
const config = await c.app?.({} as any);
|
||||||
|
expect(Object.keys(config)).toEqual(["options"]);
|
||||||
|
expect(config.options.mode).toEqual("code");
|
||||||
|
expect(config.options.plugins).toEqual([]);
|
||||||
|
expect(config.options.manager.skipValidation).toEqual(false);
|
||||||
|
expect(config.options.manager.onModulesBuilt).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps overrides", async () => {
|
||||||
|
const c = code({
|
||||||
|
connection: {
|
||||||
|
url: ":memory:",
|
||||||
|
},
|
||||||
|
}) as any;
|
||||||
|
const config = await c.app?.({} as any);
|
||||||
|
expect(config.connection.url).toEqual(":memory:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hybrid", () => {
|
||||||
|
test("fails if no reader is provided", () => {
|
||||||
|
// @ts-ignore
|
||||||
|
expect(hybrid({} as any).app?.({} as any)).rejects.toThrow(/reader/);
|
||||||
|
});
|
||||||
|
test("verify base configuration", async () => {
|
||||||
|
const c = hybrid({ reader: async () => ({}) }) as any;
|
||||||
|
const config = await c.app?.({} as any);
|
||||||
|
expect(Object.keys(config)).toEqual(["reader", "beforeBuild", "config", "options"]);
|
||||||
|
expect(config.options.mode).toEqual("db");
|
||||||
|
expect(config.options.plugins).toEqual([]);
|
||||||
|
expect(config.options.manager.skipValidation).toEqual(false);
|
||||||
|
expect(config.options.manager.onModulesBuilt).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { createApp, registries } from "../../src";
|
import { registries } from "../../src";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("repros", async () => {
|
describe("repros", async () => {
|
||||||
/**
|
/**
|
||||||
@@ -24,9 +29,9 @@ describe("repros", async () => {
|
|||||||
adapter: {
|
adapter: {
|
||||||
type: "local",
|
type: "local",
|
||||||
config: {
|
config: {
|
||||||
path: "./"
|
path: "./",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.enabled).toBe(true);
|
expect(config.enabled).toBe(true);
|
||||||
@@ -38,9 +43,9 @@ describe("repros", async () => {
|
|||||||
"entities.test",
|
"entities.test",
|
||||||
proto
|
proto
|
||||||
.entity("test", {
|
.entity("test", {
|
||||||
content: proto.text()
|
content: proto.text(),
|
||||||
})
|
})
|
||||||
.toJSON()
|
.toJSON(),
|
||||||
);
|
);
|
||||||
expect(app.em.entities.map((e) => e.name)).toContain("test");
|
expect(app.em.entities.map((e) => e.name)).toContain("test");
|
||||||
}
|
}
|
||||||
@@ -54,8 +59,8 @@ describe("repros", async () => {
|
|||||||
hidden: false,
|
hidden: false,
|
||||||
mime_types: [],
|
mime_types: [],
|
||||||
virtual: true,
|
virtual: true,
|
||||||
entity: "test"
|
entity: "test",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -63,31 +68,31 @@ describe("repros", async () => {
|
|||||||
type: "poly",
|
type: "poly",
|
||||||
source: "test",
|
source: "test",
|
||||||
target: "media",
|
target: "media",
|
||||||
config: { mappedBy: "files" }
|
config: { mappedBy: "files" },
|
||||||
})
|
}),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.only("verify inversedBy", async () => {
|
test("verify inversedBy", async () => {
|
||||||
const schema = proto.em(
|
const schema = proto.em(
|
||||||
{
|
{
|
||||||
products: proto.entity("products", {
|
products: proto.entity("products", {
|
||||||
title: proto.text()
|
title: proto.text(),
|
||||||
}),
|
}),
|
||||||
product_likes: proto.entity("product_likes", {
|
product_likes: proto.entity("product_likes", {
|
||||||
created_at: proto.date()
|
created_at: proto.date(),
|
||||||
}),
|
}),
|
||||||
users: proto.entity("users", {})
|
users: proto.entity("users", {}),
|
||||||
},
|
},
|
||||||
(fns, schema) => {
|
(fns, schema) => {
|
||||||
fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" });
|
fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" });
|
||||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const app = createApp({ initialConfig: { data: schema.toJSON() } });
|
const app = createApp({ config: { data: schema.toJSON() } });
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||||
@@ -96,8 +101,8 @@ describe("repros", async () => {
|
|||||||
expect(info.relations.listable).toEqual([
|
expect(info.relations.listable).toEqual([
|
||||||
{
|
{
|
||||||
entity: "product_likes",
|
entity: "product_likes",
|
||||||
ref: "likes"
|
ref: "likes",
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,46 +1,41 @@
|
|||||||
|
import { Authenticator } from "auth/authenticate/Authenticator";
|
||||||
import { describe, expect, test } from "bun:test";
|
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 () => {
|
describe("Authenticator", async () => {
|
||||||
test("cookie options", async () => {
|
test("should return auth cookie headers", async () => {
|
||||||
console.log("parsed", parse(cookieConfig, undefined));
|
const auth = new Authenticator({}, null as any, {
|
||||||
console.log(Default(cookieConfig, {}));
|
jwt: {
|
||||||
});
|
secret: "secret",
|
||||||
/*const userpool = new MemoryUserPool([
|
fields: [],
|
||||||
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
},
|
||||||
]);
|
cookie: {
|
||||||
|
sameSite: "strict",
|
||||||
test("sha256 login", async () => {
|
},
|
||||||
const auth = new Authenticator(userpool, {
|
|
||||||
password: new PasswordStrategy({
|
|
||||||
hashing: "sha256",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
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" });
|
// now expect it to be removed
|
||||||
expect(token).toBeDefined();
|
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);
|
test("should return auth cookie string", async () => {
|
||||||
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
|
const auth = new Authenticator({}, null as any, {
|
||||||
expect(await auth.verify(token)).toBe(true);
|
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,85 +1,247 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test, beforeAll, afterAll } 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";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
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", () => {
|
describe("authorize", () => {
|
||||||
|
const read = new Permission("read", {
|
||||||
|
filterable: true,
|
||||||
|
});
|
||||||
|
const write = new Permission("write");
|
||||||
|
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const guard = Guard.create(
|
const guard = createGuard(
|
||||||
["read", "write"],
|
["read", "write"],
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
permissions: ["read", "write"]
|
permissions: ["read", "write"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ enabled: true }
|
{ enabled: true },
|
||||||
);
|
);
|
||||||
const user = {
|
const user = {
|
||||||
role: "admin"
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted("read", user)).toBe(true);
|
expect(guard.granted(read, user)).toBeUndefined();
|
||||||
expect(guard.granted("write", user)).toBe(true);
|
expect(guard.granted(write, user)).toBeUndefined();
|
||||||
|
|
||||||
expect(() => guard.granted("something")).toThrow();
|
expect(() => guard.granted(new Permission("something"), {})).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("with default", async () => {
|
test("with default", async () => {
|
||||||
const guard = Guard.create(
|
const guard = createGuard(
|
||||||
["read", "write"],
|
["read", "write"],
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
permissions: ["read", "write"]
|
permissions: ["read", "write"],
|
||||||
},
|
},
|
||||||
guest: {
|
guest: {
|
||||||
permissions: ["read"],
|
permissions: ["read"],
|
||||||
is_default: true
|
is_default: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ enabled: true }
|
{ enabled: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(guard.granted("read")).toBe(true);
|
expect(guard.granted(read, {})).toBeUndefined();
|
||||||
expect(guard.granted("write")).toBe(false);
|
expect(() => guard.granted(write, {})).toThrow();
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
role: "admin"
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted("read", user)).toBe(true);
|
expect(guard.granted(read, user)).toBeUndefined();
|
||||||
expect(guard.granted("write", user)).toBe(true);
|
expect(guard.granted(write, user)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guard implicit allow", async () => {
|
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(read, {})).toBeUndefined();
|
||||||
expect(guard.granted("write")).toBe(true);
|
expect(guard.granted(write, {})).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("role implicit allow", async () => {
|
test("role implicit allow", async () => {
|
||||||
const guard = Guard.create(["read", "write"], {
|
const guard = createGuard(["read", "write"], {
|
||||||
admin: {
|
admin: {
|
||||||
implicit_allow: true
|
implicit_allow: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
role: "admin"
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted("read", user)).toBe(true);
|
expect(guard.granted(read, user)).toBeUndefined();
|
||||||
expect(guard.granted("write", user)).toBe(true);
|
expect(guard.granted(write, user)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guard with guest role implicit allow", async () => {
|
test("guard with guest role implicit allow", async () => {
|
||||||
const guard = Guard.create(["read", "write"], {
|
const guard = createGuard(["read", "write"], {
|
||||||
guest: {
|
guest: {
|
||||||
implicit_allow: true,
|
implicit_allow: true,
|
||||||
is_default: true
|
is_default: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(guard.getUserRole()?.name).toBe("guest");
|
expect(guard.getUserRole()?.name).toBe("guest");
|
||||||
expect(guard.granted("read")).toBe(true);
|
expect(guard.granted(read, {})).toBeUndefined();
|
||||||
expect(guard.granted("write")).toBe(true);
|
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,40 @@
|
|||||||
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
|
import { createAuthTestApp } from "./shared";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { em, entity, text } from "data/prototype";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
const schema = em(
|
||||||
|
{
|
||||||
|
posts: entity("posts", {
|
||||||
|
title: text(),
|
||||||
|
content: text(),
|
||||||
|
}),
|
||||||
|
comments: entity("comments", {
|
||||||
|
content: text(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
({ relation }, { posts, comments }) => {
|
||||||
|
relation(posts).manyToOne(comments);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("DataController (auth)", () => {
|
||||||
|
test("reading schema.json", async () => {
|
||||||
|
const { request } = await createAuthTestApp(
|
||||||
|
{
|
||||||
|
permission: ["system.access.api", "data.entity.read", "system.schema.read"],
|
||||||
|
request: new Request("http://localhost/api/data/schema.json"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: { data: schema.toJSON() },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect((await request.guest()).status).toBe(403);
|
||||||
|
expect((await request.member()).status).toBe(403);
|
||||||
|
expect((await request.authorized()).status).toBe(200);
|
||||||
|
expect((await request.admin()).status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
|
import { createAuthTestApp } from "./shared";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("SystemController (auth)", () => {
|
||||||
|
test("reading info", async () => {
|
||||||
|
const { request } = await createAuthTestApp({
|
||||||
|
permission: ["system.access.api", "system.info"],
|
||||||
|
request: new Request("http://localhost/api/system/info"),
|
||||||
|
});
|
||||||
|
expect((await request.guest()).status).toBe(403);
|
||||||
|
expect((await request.member()).status).toBe(403);
|
||||||
|
expect((await request.authorized()).status).toBe(200);
|
||||||
|
expect((await request.admin()).status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reading permissions", async () => {
|
||||||
|
const { request } = await createAuthTestApp({
|
||||||
|
permission: ["system.access.api", "system.schema.read"],
|
||||||
|
request: new Request("http://localhost/api/system/permissions"),
|
||||||
|
});
|
||||||
|
expect((await request.guest()).status).toBe(403);
|
||||||
|
expect((await request.member()).status).toBe(403);
|
||||||
|
expect((await request.authorized()).status).toBe(200);
|
||||||
|
expect((await request.admin()).status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("access openapi", async () => {
|
||||||
|
const { request } = await createAuthTestApp({
|
||||||
|
permission: ["system.access.api", "system.openapi"],
|
||||||
|
request: new Request("http://localhost/api/system/openapi.json"),
|
||||||
|
});
|
||||||
|
expect((await request.guest()).status).toBe(403);
|
||||||
|
expect((await request.member()).status).toBe(403);
|
||||||
|
expect((await request.authorized()).status).toBe(200);
|
||||||
|
expect((await request.admin()).status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { createApp } from "core/test/utils";
|
||||||
|
import type { CreateAppConfig } from "App";
|
||||||
|
import type { RoleSchema } from "auth/authorize/Role";
|
||||||
|
import { isPlainObject } from "core/utils";
|
||||||
|
|
||||||
|
export type AuthTestConfig = {
|
||||||
|
guest?: RoleSchema;
|
||||||
|
member?: RoleSchema;
|
||||||
|
authorized?: RoleSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createAuthTestApp(
|
||||||
|
testConfig: {
|
||||||
|
permission: AuthTestConfig | string | string[];
|
||||||
|
request: Request;
|
||||||
|
},
|
||||||
|
config: Partial<CreateAppConfig> = {},
|
||||||
|
) {
|
||||||
|
let member: RoleSchema | undefined;
|
||||||
|
let authorized: RoleSchema | undefined;
|
||||||
|
let guest: RoleSchema | undefined;
|
||||||
|
if (isPlainObject(testConfig.permission)) {
|
||||||
|
if (testConfig.permission.guest)
|
||||||
|
guest = {
|
||||||
|
...testConfig.permission.guest,
|
||||||
|
is_default: true,
|
||||||
|
};
|
||||||
|
if (testConfig.permission.member) member = testConfig.permission.member;
|
||||||
|
if (testConfig.permission.authorized) authorized = testConfig.permission.authorized;
|
||||||
|
} else {
|
||||||
|
member = {
|
||||||
|
permissions: [],
|
||||||
|
};
|
||||||
|
authorized = {
|
||||||
|
permissions: Array.isArray(testConfig.permission)
|
||||||
|
? testConfig.permission
|
||||||
|
: [testConfig.permission],
|
||||||
|
};
|
||||||
|
guest = {
|
||||||
|
permissions: [],
|
||||||
|
is_default: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("authorized", authorized);
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
...config,
|
||||||
|
config: {
|
||||||
|
...config.config,
|
||||||
|
auth: {
|
||||||
|
...config.config?.auth,
|
||||||
|
enabled: true,
|
||||||
|
guard: {
|
||||||
|
enabled: true,
|
||||||
|
...config.config?.auth?.guard,
|
||||||
|
},
|
||||||
|
jwt: {
|
||||||
|
...config.config?.auth?.jwt,
|
||||||
|
secret: "secret",
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
...config.config?.auth?.roles,
|
||||||
|
guest,
|
||||||
|
member,
|
||||||
|
authorized,
|
||||||
|
admin: {
|
||||||
|
implicit_allow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const users = {
|
||||||
|
guest: null,
|
||||||
|
member: await app.createUser({
|
||||||
|
email: "member@test.com",
|
||||||
|
password: "12345678",
|
||||||
|
role: "member",
|
||||||
|
}),
|
||||||
|
authorized: await app.createUser({
|
||||||
|
email: "authorized@test.com",
|
||||||
|
password: "12345678",
|
||||||
|
role: "authorized",
|
||||||
|
}),
|
||||||
|
admin: await app.createUser({
|
||||||
|
email: "admin@test.com",
|
||||||
|
password: "12345678",
|
||||||
|
role: "admin",
|
||||||
|
}),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const tokens = {} as Record<keyof typeof users, string>;
|
||||||
|
for (const [key, user] of Object.entries(users)) {
|
||||||
|
if (user) {
|
||||||
|
tokens[key as keyof typeof users] = await app.module.auth.authenticator.jwt(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeRequest(user: keyof typeof users, input: string, init: RequestInit = {}) {
|
||||||
|
const headers = new Headers(init.headers ?? {});
|
||||||
|
if (user in tokens) {
|
||||||
|
headers.set("Authorization", `Bearer ${tokens[user as keyof typeof tokens]}`);
|
||||||
|
}
|
||||||
|
const res = await app.server.request(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: any;
|
||||||
|
if (res.headers.get("Content-Type")?.startsWith("application/json")) {
|
||||||
|
data = await res.json();
|
||||||
|
} else if (res.headers.get("Content-Type")?.startsWith("text/")) {
|
||||||
|
data = await res.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
ok: res.ok,
|
||||||
|
headers: Object.fromEntries(res.headers.entries()),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestFn = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(_, prop: keyof typeof users) {
|
||||||
|
return async (input: string, init: RequestInit = {}) => {
|
||||||
|
return makeRequest(prop, input, init);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) as {
|
||||||
|
[K in keyof typeof users]: (
|
||||||
|
input: string,
|
||||||
|
init?: RequestInit,
|
||||||
|
) => Promise<{
|
||||||
|
status: number;
|
||||||
|
ok: boolean;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
data: any;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(_, prop: keyof typeof users) {
|
||||||
|
return async () => {
|
||||||
|
return makeRequest(prop, testConfig.request.url, {
|
||||||
|
headers: testConfig.request.headers,
|
||||||
|
method: testConfig.request.method,
|
||||||
|
body: testConfig.request.body,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) as {
|
||||||
|
[K in keyof typeof users]: () => Promise<{
|
||||||
|
status: number;
|
||||||
|
ok: boolean;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
data: any;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { app, users, request, requestFn };
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,14 +3,16 @@ import { OAuthStrategy } from "../../../src/auth/authenticate/strategies";
|
|||||||
|
|
||||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||||
|
|
||||||
|
// @todo: add mock response
|
||||||
describe("OAuthStrategy", async () => {
|
describe("OAuthStrategy", async () => {
|
||||||
const strategy = new OAuthStrategy({
|
return;
|
||||||
|
/*const strategy = new OAuthStrategy({
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
client: {
|
client: {
|
||||||
client_id: process.env.OAUTH_CLIENT_ID,
|
client_id: process.env.OAUTH_CLIENT_ID!,
|
||||||
client_secret: process.env.OAUTH_CLIENT_SECRET
|
client_secret: process.env.OAUTH_CLIENT_SECRET!,
|
||||||
},
|
},
|
||||||
name: "google"
|
name: "google",
|
||||||
});
|
});
|
||||||
const state = "---";
|
const state = "---";
|
||||||
const redirect_uri = "http://localhost:3000/auth/google/callback";
|
const redirect_uri = "http://localhost:3000/auth/google/callback";
|
||||||
@@ -19,28 +21,29 @@ describe("OAuthStrategy", async () => {
|
|||||||
const config = await strategy.getConfig();
|
const config = await strategy.getConfig();
|
||||||
console.log("config", JSON.stringify(config, null, 2));
|
console.log("config", JSON.stringify(config, null, 2));
|
||||||
|
|
||||||
const request = await strategy.request({
|
|
||||||
redirect_uri,
|
|
||||||
state
|
|
||||||
});
|
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
fetch: async (req) => {
|
fetch: async (req) => {
|
||||||
|
console.log("req", req.method, req.url);
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
if (url.pathname === "/auth/google/callback") {
|
if (url.pathname === "/auth/google/callback") {
|
||||||
console.log("req", req);
|
console.log("req", req);
|
||||||
const user = await strategy.callback(url, {
|
const user = await strategy.callback(url, {
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
state
|
state,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("---user", user);
|
console.log("---user", user);
|
||||||
}
|
}
|
||||||
return new Response("Bun!");
|
return new Response("Bun!");
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = await strategy.request({
|
||||||
|
redirect_uri,
|
||||||
|
state,
|
||||||
});
|
});
|
||||||
console.log("request", request);
|
console.log("request", request);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100000));
|
await new Promise((resolve) => setTimeout(resolve, 100000));
|
||||||
});
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { PasswordStrategy } from "auth/authenticate/strategies/PasswordStrategy";
|
||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
|
||||||
|
describe("PasswordStrategy", () => {
|
||||||
|
it("should enforce provided minimum length", async () => {
|
||||||
|
const strategy = new PasswordStrategy({ minLength: 8, hashing: "plain" });
|
||||||
|
|
||||||
|
expect(strategy.verify("password")({} as any)).rejects.toThrow();
|
||||||
|
expect(
|
||||||
|
strategy.verify("password1234")({ strategy_value: "password1234" } as any),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||||
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -26,7 +26,7 @@ class ReturnEvent extends Event<{ foo: string }, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.clone({
|
return this.clone({
|
||||||
foo: [this.params.foo, value].join("-")
|
foo: [this.params.foo, value].join("-"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ describe("EventManager", async () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
delayed();
|
delayed();
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
// don't allow unknown
|
// don't allow unknown
|
||||||
@@ -70,6 +70,9 @@ describe("EventManager", async () => {
|
|||||||
new SpecialEvent({ foo: "bar" });
|
new SpecialEvent({ foo: "bar" });
|
||||||
new InformationalEvent();
|
new InformationalEvent();
|
||||||
|
|
||||||
|
// execute asyncs
|
||||||
|
await emgr.executeAsyncs();
|
||||||
|
|
||||||
expect(call).toHaveBeenCalledTimes(2);
|
expect(call).toHaveBeenCalledTimes(2);
|
||||||
expect(delayed).toHaveBeenCalled();
|
expect(delayed).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -80,15 +83,11 @@ describe("EventManager", async () => {
|
|||||||
call();
|
call();
|
||||||
return Promise.all(p);
|
return Promise.all(p);
|
||||||
};
|
};
|
||||||
const emgr = new EventManager(
|
const emgr = new EventManager({ InformationalEvent });
|
||||||
{ InformationalEvent },
|
|
||||||
{
|
|
||||||
asyncExecutor
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
emgr.onEvent(InformationalEvent, async () => {});
|
emgr.onEvent(InformationalEvent, async () => {});
|
||||||
await emgr.emit(new InformationalEvent());
|
await emgr.emit(new InformationalEvent());
|
||||||
|
await emgr.executeAsyncs(asyncExecutor);
|
||||||
expect(call).toHaveBeenCalled();
|
expect(call).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,8 +97,8 @@ describe("EventManager", async () => {
|
|||||||
const emgr = new EventManager(
|
const emgr = new EventManager(
|
||||||
{ ReturnEvent, InformationalEvent },
|
{ ReturnEvent, InformationalEvent },
|
||||||
{
|
{
|
||||||
onInvalidReturn
|
onInvalidReturn,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-expect-error InformationalEvent has no return value
|
// @ts-expect-error InformationalEvent has no return value
|
||||||
@@ -125,6 +124,9 @@ describe("EventManager", async () => {
|
|||||||
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
||||||
expect(e2.returned).toBe(true);
|
expect(e2.returned).toBe(true);
|
||||||
expect(e2.params.foo).toBe("bar-1-0");
|
expect(e2.params.foo).toBe("bar-1-0");
|
||||||
|
|
||||||
|
await emgr.executeAsyncs();
|
||||||
|
|
||||||
expect(onInvalidReturn).toHaveBeenCalled();
|
expect(onInvalidReturn).toHaveBeenCalled();
|
||||||
expect(asyncEventCallback).toHaveBeenCalled();
|
expect(asyncEventCallback).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -140,7 +142,7 @@ describe("EventManager", async () => {
|
|||||||
expect(slug).toBe("informational-event");
|
expect(slug).toBe("informational-event");
|
||||||
call();
|
call();
|
||||||
},
|
},
|
||||||
{ mode: "sync", once: true }
|
{ mode: "sync", once: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(emgr.getListeners().length).toBe(1);
|
expect(emgr.getListeners().length).toBe(1);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { TObject, TString } from "@sinclair/typebox";
|
import { Registry } from "core/registry/Registry";
|
||||||
import { Registry } from "../../src/core/registry/Registry";
|
import { s } from "core/utils/schema";
|
||||||
import { type TSchema, Type } from "../../src/core/utils";
|
|
||||||
|
|
||||||
type Constructor<T> = new (...args: any[]) => T;
|
type Constructor<T> = new (...args: any[]) => T;
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ class What {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
getType() {
|
getType() {
|
||||||
return Type.Object({ type: Type.String() });
|
return s.object({ type: s.string() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class What2 extends What {}
|
class What2 extends What {}
|
||||||
@@ -20,7 +19,7 @@ class NotAllowed {}
|
|||||||
|
|
||||||
type Test1 = {
|
type Test1 = {
|
||||||
cls: new (...args: any[]) => What;
|
cls: new (...args: any[]) => What;
|
||||||
schema: TObject<{ type: TString }>;
|
schema: s.ObjectSchema<{ type: s.StringSchema }>;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,40 +28,40 @@ describe("Registry", () => {
|
|||||||
const registry = new Registry<Test1>().set({
|
const registry = new Registry<Test1>().set({
|
||||||
first: {
|
first: {
|
||||||
cls: What,
|
cls: What,
|
||||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
schema: s.object({ type: s.string(), what: s.string() }),
|
||||||
enabled: true
|
enabled: true,
|
||||||
}
|
},
|
||||||
} satisfies Record<string, Test1>);
|
} satisfies Record<string, Test1>);
|
||||||
|
|
||||||
const item = registry.get("first");
|
const item = registry.get("first");
|
||||||
expect(item).toBeDefined();
|
expect(item).toBeDefined();
|
||||||
expect(item?.cls).toBe(What);
|
expect(item?.cls).toBe(What);
|
||||||
|
|
||||||
const second = Type.Object({ type: Type.String(), what: Type.String() });
|
const second = s.object({ type: s.string(), what: s.string() });
|
||||||
registry.add("second", {
|
registry.add("second", {
|
||||||
cls: What2,
|
cls: What2,
|
||||||
schema: second,
|
schema: second,
|
||||||
enabled: true
|
enabled: true,
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("second").schema).toEqual(second);
|
expect(registry.get("second").schema).toEqual(second);
|
||||||
|
|
||||||
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() });
|
const third = s.object({ type: s.string({ default: "1" }), what22: s.string() });
|
||||||
registry.add("third", {
|
registry.add("third", {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
cls: NotAllowed,
|
cls: NotAllowed,
|
||||||
schema: third,
|
schema: third,
|
||||||
enabled: true
|
enabled: true,
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("third").schema).toEqual(third);
|
expect(registry.get("third").schema).toEqual(third);
|
||||||
|
|
||||||
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() });
|
const fourth = s.object({ type: s.number(), what22: s.string() });
|
||||||
registry.add("fourth", {
|
registry.add("fourth", {
|
||||||
cls: What,
|
cls: What,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
schema: fourth,
|
schema: fourth,
|
||||||
enabled: true
|
enabled: true,
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(registry.get("fourth").schema).toEqual(fourth);
|
expect(registry.get("fourth").schema).toEqual(fourth);
|
||||||
@@ -75,13 +74,15 @@ describe("Registry", () => {
|
|||||||
return {
|
return {
|
||||||
cls: a,
|
cls: a,
|
||||||
schema: a.prototype.getType(),
|
schema: a.prototype.getType(),
|
||||||
enabled: true
|
enabled: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.register("what2", What2);
|
registry.register("what2", What2);
|
||||||
expect(registry.get("what2")).toBeDefined();
|
expect(registry.get("what2")).toBeDefined();
|
||||||
expect(registry.get("what2").cls).toBe(What2);
|
expect(registry.get("what2").cls).toBe(What2);
|
||||||
expect(registry.get("what2").schema).toEqual(What2.prototype.getType());
|
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);
|
|
||||||
});*/
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import { checksum, hash } from "../../src/core/utils";
|
|||||||
describe("crypto", async () => {
|
describe("crypto", async () => {
|
||||||
test("sha256", async () => {
|
test("sha256", async () => {
|
||||||
expect(await hash.sha256("test")).toBe(
|
expect(await hash.sha256("test")).toBe(
|
||||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
test("sha1", async () => {
|
test("sha1", async () => {
|
||||||
|
|||||||
@@ -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 { describe, expect, test } from "bun:test";
|
||||||
import { SchemaObject } from "../../../src/core";
|
import { s } from "core/utils/schema";
|
||||||
import { Type } from "../../../src/core/utils";
|
import { SchemaObject } from "core/object/SchemaObject";
|
||||||
|
|
||||||
describe("SchemaObject", async () => {
|
describe("SchemaObject", async () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({ a: Type.String({ default: "b" }) }),
|
s.strictObject({ a: s.string({ default: "b" }) }),
|
||||||
{ a: "test" },
|
{ a: "test" },
|
||||||
{
|
{
|
||||||
forceParse: true
|
forceParse: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.get()).toEqual({ a: "test" });
|
expect(m.get()).toEqual({ a: "test" });
|
||||||
@@ -23,28 +23,28 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("patch", async () => {
|
test("patch", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
s: Type.Object(
|
s: s.strictObject(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
b: s.strictObject(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" }),
|
c: s.string({ default: "d" }),
|
||||||
e: Type.String({ default: "f" })
|
e: s.string({ default: "f" }),
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{ default: {}, additionalProperties: false }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
|
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
|
||||||
|
|
||||||
await m.patch("s.a", "c");
|
await m.patch("s.a", "c");
|
||||||
|
|
||||||
// non-existing path on no additional properties
|
// 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
|
// wrong type
|
||||||
expect(() => m.patch("s.a", 1)).toThrow();
|
expect(() => m.patch("s.a", 1)).toThrow();
|
||||||
|
|
||||||
@@ -58,9 +58,9 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("patch array", async () => {
|
test("patch array", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
|
|
||||||
@@ -75,20 +75,20 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("remove", async () => {
|
test("remove", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.object({
|
||||||
s: Type.Object(
|
s: s.object(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
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" } } });
|
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
|
||||||
|
|
||||||
@@ -107,9 +107,9 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("set", async () => {
|
test("set", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||||
|
|
||||||
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
|
|||||||
let called = false;
|
let called = false;
|
||||||
let result: any;
|
let result: any;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
|
|||||||
await new Promise((r) => setTimeout(r, 10));
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
called = true;
|
called = true;
|
||||||
result = config;
|
result = config;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await m.set({ methods: ["GET", "POST"] });
|
await m.set({ methods: ["GET", "POST"] });
|
||||||
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
|
|||||||
test("listener: onBeforeUpdate", async () => {
|
test("listener: onBeforeUpdate", async () => {
|
||||||
let called = false;
|
let called = false;
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
|
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
|
|||||||
called = true;
|
called = true;
|
||||||
to.methods.push("OPTIONS");
|
to.methods.push("OPTIONS");
|
||||||
return to;
|
return to;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await m.set({ methods: ["GET", "POST"] });
|
const result = await m.set({ methods: ["GET", "POST"] });
|
||||||
@@ -167,8 +167,8 @@ describe("SchemaObject", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("throwIfRestricted", async () => {
|
test("throwIfRestricted", async () => {
|
||||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
const m = new SchemaObject(s.strictObject({}), undefined, {
|
||||||
restrictPaths: ["a.b"]
|
restrictPaths: ["a.b"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => m.throwIfRestricted("a.b")).toThrow();
|
expect(() => m.throwIfRestricted("a.b")).toThrow();
|
||||||
@@ -179,24 +179,24 @@ describe("SchemaObject", async () => {
|
|||||||
|
|
||||||
test("restriction bypass", async () => {
|
test("restriction bypass", async () => {
|
||||||
const m = new SchemaObject(
|
const m = new SchemaObject(
|
||||||
Type.Object({
|
s.strictObject({
|
||||||
s: Type.Object(
|
s: s.strictObject(
|
||||||
{
|
{
|
||||||
a: Type.String({ default: "b" }),
|
a: s.string({ default: "b" }),
|
||||||
b: Type.Object(
|
b: s.strictObject(
|
||||||
{
|
{
|
||||||
c: Type.String({ default: "d" })
|
c: s.string({ default: "d" }),
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} },
|
||||||
)
|
),
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
restrictPaths: ["s.b"]
|
restrictPaths: ["s.b"],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(m.patch("s.b.c", "e")).rejects.toThrow();
|
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" } } });
|
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(
|
entities: Type.Object(
|
||||||
{},
|
{},
|
||||||
@@ -217,33 +231,33 @@ describe("SchemaObject", async () => {
|
|||||||
additionalProperties: Type.Object({
|
additionalProperties: Type.Object({
|
||||||
type: Type.String(),
|
type: Type.String(),
|
||||||
config: Type.Optional(
|
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 () => {
|
test("patch safe object, overwrite", async () => {
|
||||||
const data = {
|
const data = {
|
||||||
entities: {
|
entities: {
|
||||||
some: {
|
some: {
|
||||||
fields: {
|
fields: {
|
||||||
a: { type: "string", config: { some: "thing" } }
|
a: { type: "string", config: { some: "thing" } },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||||
forceParse: true,
|
forceParse: true,
|
||||||
overwritePaths: [/^entities\..*\.fields\..*\.config/]
|
overwritePaths: [/^entities\..*\.fields\..*\.config/],
|
||||||
});
|
});
|
||||||
|
|
||||||
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
|
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
|
||||||
@@ -252,10 +266,10 @@ describe("SchemaObject", async () => {
|
|||||||
entities: {
|
entities: {
|
||||||
some: {
|
some: {
|
||||||
fields: {
|
fields: {
|
||||||
a: { type: "string", config: { another: "one" } }
|
a: { type: "string", config: { another: "one" } },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,22 +279,22 @@ describe("SchemaObject", async () => {
|
|||||||
users: {
|
users: {
|
||||||
fields: {
|
fields: {
|
||||||
email: { type: "string" },
|
email: { type: "string" },
|
||||||
password: { type: "string" }
|
password: { type: "string" },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||||
forceParse: true,
|
forceParse: true,
|
||||||
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
|
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
|
||||||
});
|
});
|
||||||
|
|
||||||
await m.patch("entities.test", {
|
await m.patch("entities.test", {
|
||||||
fields: {
|
fields: {
|
||||||
content: {
|
content: {
|
||||||
type: "text"
|
type: "text",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(m.get()).toEqual({
|
expect(m.get()).toEqual({
|
||||||
@@ -288,17 +302,17 @@ describe("SchemaObject", async () => {
|
|||||||
users: {
|
users: {
|
||||||
fields: {
|
fields: {
|
||||||
email: { type: "string" },
|
email: { type: "string" },
|
||||||
password: { type: "string" }
|
password: { type: "string" },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
fields: {
|
fields: {
|
||||||
content: {
|
content: {
|
||||||
type: "text"
|
type: "text",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -308,14 +322,14 @@ describe("SchemaObject", async () => {
|
|||||||
users: {
|
users: {
|
||||||
fields: {
|
fields: {
|
||||||
email: { type: "string" },
|
email: { type: "string" },
|
||||||
password: { type: "string" }
|
password: { type: "string" },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const m = new SchemaObject(dataEntitiesSchema, data, {
|
const m = new SchemaObject(dataEntitiesSchema, data, {
|
||||||
forceParse: true,
|
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();
|
expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow();
|
||||||
@@ -323,13 +337,13 @@ describe("SchemaObject", async () => {
|
|||||||
await m.patch("entities.test", {
|
await m.patch("entities.test", {
|
||||||
fields: {
|
fields: {
|
||||||
content: {
|
content: {
|
||||||
type: "text"
|
type: "text",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await m.patch("entities.users.config", {
|
await m.patch("entities.users.config", {
|
||||||
sort_dir: "desc"
|
sort_dir: "desc",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(m.get()).toEqual({
|
expect(m.get()).toEqual({
|
||||||
@@ -337,20 +351,20 @@ describe("SchemaObject", async () => {
|
|||||||
users: {
|
users: {
|
||||||
fields: {
|
fields: {
|
||||||
email: { type: "string" },
|
email: { type: "string" },
|
||||||
password: { type: "string" }
|
password: { type: "string" },
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
sort_dir: "desc"
|
sort_dir: "desc",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
fields: {
|
fields: {
|
||||||
content: {
|
content: {
|
||||||
type: "text"
|
type: "text",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ describe("diff", () => {
|
|||||||
t: "a",
|
t: "a",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -35,8 +35,8 @@ describe("diff", () => {
|
|||||||
t: "r",
|
t: "r",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: 2,
|
o: 2,
|
||||||
n: undefined
|
n: undefined,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -57,8 +57,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -79,8 +79,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a", "b"],
|
p: ["a", "b"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -101,14 +101,14 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a", 1],
|
p: ["a", 1],
|
||||||
o: 2,
|
o: 2,
|
||||||
n: 4
|
n: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", 3],
|
p: ["a", 3],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 5
|
n: 5,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -129,20 +129,20 @@ describe("diff", () => {
|
|||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", 0],
|
p: ["a", 0],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 1
|
n: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", 1],
|
p: ["a", 1],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 2
|
n: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", 2],
|
p: ["a", 2],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 3
|
n: 3,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -163,14 +163,14 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a", 1],
|
p: ["a", 1],
|
||||||
o: 2,
|
o: 2,
|
||||||
n: 3
|
n: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "r",
|
t: "r",
|
||||||
p: ["a", 2],
|
p: ["a", 2],
|
||||||
o: 3,
|
o: 3,
|
||||||
n: undefined
|
n: undefined,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -183,14 +183,14 @@ describe("diff", () => {
|
|||||||
it("should handle complex nested changes", () => {
|
it("should handle complex nested changes", () => {
|
||||||
const oldObj = {
|
const oldObj = {
|
||||||
a: {
|
a: {
|
||||||
b: [1, 2, { c: 3 }]
|
b: [1, 2, { c: 3 }],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const newObj = {
|
const newObj = {
|
||||||
a: {
|
a: {
|
||||||
b: [1, 2, { c: 4 }, 5]
|
b: [1, 2, { c: 4 }, 5],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const diffs = diff(oldObj, newObj);
|
const diffs = diff(oldObj, newObj);
|
||||||
@@ -200,14 +200,14 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a", "b", 2, "c"],
|
p: ["a", "b", 2, "c"],
|
||||||
o: 3,
|
o: 3,
|
||||||
n: 4
|
n: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["a", "b", 3],
|
p: ["a", "b", 3],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 5
|
n: 5,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -228,14 +228,14 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: null
|
n: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "e",
|
t: "e",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: null,
|
o: null,
|
||||||
n: undefined
|
n: undefined,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -256,8 +256,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: "1"
|
n: "1",
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -278,14 +278,14 @@ describe("diff", () => {
|
|||||||
t: "r",
|
t: "r",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: 2,
|
o: 2,
|
||||||
n: undefined
|
n: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["c"],
|
p: ["c"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 3
|
n: 3,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -306,8 +306,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: [1, 2, 3],
|
o: [1, 2, 3],
|
||||||
n: { b: 4 }
|
n: { b: 4 },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -328,8 +328,8 @@ describe("diff", () => {
|
|||||||
t: "e",
|
t: "e",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: { b: 1 },
|
o: { b: 1 },
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -350,14 +350,14 @@ describe("diff", () => {
|
|||||||
t: "r",
|
t: "r",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: undefined
|
n: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "a",
|
t: "a",
|
||||||
p: ["b"],
|
p: ["b"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 2
|
n: 2,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -408,8 +408,8 @@ describe("diff", () => {
|
|||||||
t: "a",
|
t: "a",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: undefined,
|
o: undefined,
|
||||||
n: 1
|
n: 1,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
@@ -430,8 +430,8 @@ describe("diff", () => {
|
|||||||
t: "r",
|
t: "r",
|
||||||
p: ["a"],
|
p: ["a"],
|
||||||
o: 1,
|
o: 1,
|
||||||
n: undefined
|
n: undefined,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appliedObj = apply(oldObj, diffs);
|
const appliedObj = apply(oldObj, diffs);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query";
|
import { type ObjectQuery, convert, validate } from "core/object/query/object-query";
|
||||||
|
|
||||||
describe("object-query", () => {
|
describe("object-query", () => {
|
||||||
const q: ObjectQuery = { name: "Michael" };
|
const q: ObjectQuery = { name: "Michael" };
|
||||||
@@ -9,7 +9,7 @@ describe("object-query", () => {
|
|||||||
|
|
||||||
test("validates", async () => {
|
test("validates", async () => {
|
||||||
const converted = convert({
|
const converted = convert({
|
||||||
name: { $eq: "ch" }
|
name: { $eq: "ch" },
|
||||||
});
|
});
|
||||||
validate(converted, { name: "Michael" });
|
validate(converted, { name: "Michael" });
|
||||||
});
|
});
|
||||||
@@ -31,7 +31,7 @@ describe("object-query", () => {
|
|||||||
[{ val: { $notnull: 1 } }, { val: null }, false],
|
[{ val: { $notnull: 1 } }, { val: null }, false],
|
||||||
[{ val: { $regex: ".*" } }, { val: "test" }, true],
|
[{ val: { $regex: ".*" } }, { val: "test" }, true],
|
||||||
[{ val: { $regex: /^t.*/ } }, { 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) {
|
for (const [query, object, expected] of tests) {
|
||||||
@@ -55,10 +55,10 @@ describe("object-query", () => {
|
|||||||
[
|
[
|
||||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||||
{ val1: "foo", val2: "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: 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) {
|
for (const [query, object, expected] of tests) {
|
||||||
@@ -66,4 +66,14 @@ describe("object-query", () => {
|
|||||||
expect(result).toBe(expected);
|
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" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
+294
-59
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Perf, isBlob, ucFirst } from "../../src/core/utils";
|
import { Perf, ucFirst } from "../../src/core/utils";
|
||||||
import * as utils from "../../src/core/utils";
|
import * as utils from "../../src/core/utils";
|
||||||
|
import { assetsPath } from "../helper";
|
||||||
|
|
||||||
async function wait(ms: number) {
|
async function wait(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -16,7 +17,7 @@ describe("Core Utils", async () => {
|
|||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ key: "a", value: 1 },
|
{ key: "a", value: 1 },
|
||||||
{ key: "b", value: 2 },
|
{ key: "b", value: 2 },
|
||||||
{ key: "c", value: 3 }
|
{ key: "c", value: 3 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ describe("Core Utils", async () => {
|
|||||||
const obj = utils.headersToObject(headers);
|
const obj = utils.headersToObject(headers);
|
||||||
expect(obj).toEqual({
|
expect(obj).toEqual({
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
authorization: "Bearer 123"
|
authorization: "Bearer 123",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,57 +76,6 @@ describe("Core Utils", async () => {
|
|||||||
const result3 = utils.encodeSearch(obj3, { encode: true });
|
const result3 = utils.encodeSearch(obj3, { encode: true });
|
||||||
expect(result3).toBe("id=123&name=%7B%22test%22%3A%22test%22%7D");
|
expect(result3).toBe("id=123&name=%7B%22test%22%3A%22test%22%7D");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("perf", async () => {
|
describe("perf", async () => {
|
||||||
@@ -166,7 +116,7 @@ describe("Core Utils", async () => {
|
|||||||
[{ a: 1, b: 2, c: 3 }, ["b"], { a: 1, 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 }, ["c"], { a: 1, b: 2 }],
|
||||||
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
|
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
|
||||||
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}]
|
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
|
||||||
] as [object, string[], object][];
|
] as [object, string[], object][];
|
||||||
|
|
||||||
for (const [obj, keys, expected] of objects) {
|
for (const [obj, keys, expected] of objects) {
|
||||||
@@ -197,9 +147,9 @@ describe("Core Utils", async () => {
|
|||||||
new Map([["a", 1]]),
|
new Map([["a", 1]]),
|
||||||
new Map([
|
new Map([
|
||||||
["a", 1],
|
["a", 1],
|
||||||
["b", 2]
|
["b", 2],
|
||||||
]),
|
]),
|
||||||
false
|
false,
|
||||||
],
|
],
|
||||||
[{ a: 1 }, { a: 1 }, true],
|
[{ a: 1 }, { a: 1 }, true],
|
||||||
[{ a: 1 }, { a: 2 }, false],
|
[{ a: 1 }, { a: 2 }, false],
|
||||||
@@ -220,7 +170,7 @@ describe("Core Utils", async () => {
|
|||||||
[[1, 2, 3], [1, 2, 3, 4], false],
|
[[1, 2, 3], [1, 2, 3, 4], false],
|
||||||
[[{ a: 1 }], [{ a: 1 }], true],
|
[[{ a: 1 }], [{ a: 1 }], true],
|
||||||
[[{ a: 1 }], [{ a: 2 }], false],
|
[[{ a: 1 }], [{ a: 2 }], false],
|
||||||
[[{ a: 1 }], [{ b: 1 }], false]
|
[[{ a: 1 }], [{ b: 1 }], false],
|
||||||
] as [any, any, boolean][];
|
] as [any, any, boolean][];
|
||||||
|
|
||||||
for (const [a, b, expected] of objects) {
|
for (const [a, b, expected] of objects) {
|
||||||
@@ -236,7 +186,7 @@ describe("Core Utils", async () => {
|
|||||||
[{ a: { b: 1 } }, "a.b", 1],
|
[{ a: { b: 1 } }, "a.b", 1],
|
||||||
[{ a: { b: 1 } }, "a.b.c", null, null],
|
[{ a: { b: 1 } }, "a.b.c", null, null],
|
||||||
[{ a: { b: 1 } }, "a.b.c", 1, 1],
|
[{ a: { b: 1 } }, "a.b.c", 1, 1],
|
||||||
[[[1]], "0.0", 1]
|
[[[1]], "0.0", 1],
|
||||||
] as [object, string, any, any][];
|
] as [object, string, any, any][];
|
||||||
|
|
||||||
for (const [obj, path, expected, defaultValue] of tests) {
|
for (const [obj, path, expected, defaultValue] of tests) {
|
||||||
@@ -244,5 +194,290 @@ describe("Core Utils", async () => {
|
|||||||
expect(result).toEqual(expected);
|
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 { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { Guard } from "../../src/auth";
|
import { Guard } from "../../src/auth/authorize/Guard";
|
||||||
import { parse } from "../../src/core/utils";
|
import { parse } from "core/utils/schema";
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
type EntityData,
|
|
||||||
EntityManager,
|
|
||||||
ManyToOneRelation,
|
|
||||||
type MutatorResponse,
|
|
||||||
type RepositoryResponse,
|
|
||||||
TextField
|
|
||||||
} from "../../src/data";
|
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
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();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
beforeAll(() => disableConsoleLog());
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
const dataConfig = parse(dataConfigSchema, {});
|
const dataConfig = parse(dataConfigSchema, {});
|
||||||
describe("[data] DataController", async () => {
|
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 () => {
|
describe("getController", async () => {
|
||||||
const users = new Entity("users", [
|
const users = new Entity("users", [
|
||||||
new TextField("name", { required: true }),
|
new TextField("name", { required: true }),
|
||||||
new TextField("bio")
|
new TextField("bio"),
|
||||||
]);
|
]);
|
||||||
const posts = new Entity("posts", [new TextField("content")]);
|
const posts = new Entity("posts", [new TextField("content")]);
|
||||||
const em = new EntityManager([users, posts], dummyConnection, [
|
const em = new EntityManager([users, posts], dummyConnection, [
|
||||||
new ManyToOneRelation(posts, users)
|
new ManyToOneRelation(posts, users),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
@@ -83,12 +35,12 @@ describe("[data] DataController", async () => {
|
|||||||
users: [
|
users: [
|
||||||
{ name: "foo", bio: "bar" },
|
{ name: "foo", bio: "bar" },
|
||||||
{ name: "bar", bio: null },
|
{ name: "bar", bio: null },
|
||||||
{ name: "baz", bio: "!!!" }
|
{ name: "baz", bio: "!!!" },
|
||||||
],
|
],
|
||||||
posts: [
|
posts: [
|
||||||
{ content: "post 1", users_id: 1 },
|
{ 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() };
|
const ctx: any = { em, guard: new Guard() };
|
||||||
@@ -118,10 +70,9 @@ describe("[data] DataController", async () => {
|
|||||||
for await (const _user of fixtures.users) {
|
for await (const _user of fixtures.users) {
|
||||||
const res = await app.request("/entity/users", {
|
const res = await app.request("/entity/users", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(_user)
|
body: JSON.stringify(_user),
|
||||||
});
|
});
|
||||||
//console.log("res", { _user }, res);
|
const result = (await res.json()) as MutatorResultJSON;
|
||||||
const result = (await res.json()) as MutatorResponse;
|
|
||||||
const { id, ...data } = result.data as any;
|
const { id, ...data } = result.data as any;
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
@@ -133,9 +84,9 @@ describe("[data] DataController", async () => {
|
|||||||
for await (const _post of fixtures.posts) {
|
for await (const _post of fixtures.posts) {
|
||||||
const res = await app.request("/entity/posts", {
|
const res = await app.request("/entity/posts", {
|
||||||
method: "POST",
|
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;
|
const { id, ...data } = result.data as any;
|
||||||
|
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
@@ -146,52 +97,51 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
test("/:entity (read many)", async () => {
|
test("/:entity (read many)", async () => {
|
||||||
const res = await app.request("/entity/users");
|
const res = await app.request("/entity/users");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResultJSON;
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
//expect(data.meta.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(3);
|
//expect(data.meta.count).toBe(3);
|
||||||
expect(data.meta.items).toBe(3);
|
expect(data.meta.items).toBe(3);
|
||||||
expect(data.data.length).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 () => {
|
test("/:entity/query (func query)", async () => {
|
||||||
const res = await app.request("/entity/users/query", {
|
const res = await app.request("/entity/users/query", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
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.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).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 () => {
|
test("/:entity (read many, paginated)", async () => {
|
||||||
const res = await app.request("/entity/users?limit=1&offset=2");
|
const res = await app.request("/entity/users?limit=1&offset=2");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResultJSON;
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
//expect(data.meta.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(3);
|
//expect(data.meta.count).toBe(3);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).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 () => {
|
test("/:entity/:id (read one)", async () => {
|
||||||
const res = await app.request("/entity/users/3");
|
const res = await app.request("/entity/users/3");
|
||||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||||
console.log("data", data);
|
|
||||||
|
|
||||||
expect(data.meta.total).toBe(3);
|
//expect(data.meta.total).toBe(3);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||||
});
|
});
|
||||||
@@ -199,9 +149,9 @@ describe("[data] DataController", async () => {
|
|||||||
test("/:entity (update one)", async () => {
|
test("/:entity (update one)", async () => {
|
||||||
const res = await app.request("/entity/users/3", {
|
const res = await app.request("/entity/users/3", {
|
||||||
method: "PATCH",
|
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(res.ok).toBe(true);
|
||||||
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||||
@@ -209,27 +159,26 @@ describe("[data] DataController", async () => {
|
|||||||
|
|
||||||
test("/:entity/:id/:reference (read references)", async () => {
|
test("/:entity/:id/:reference (read references)", async () => {
|
||||||
const res = await app.request("/entity/users/1/posts");
|
const res = await app.request("/entity/users/1/posts");
|
||||||
const data = (await res.json()) as RepositoryResponse;
|
const data = (await res.json()) as RepositoryResultJSON;
|
||||||
console.log("data", data);
|
|
||||||
|
|
||||||
expect(data.meta.total).toBe(2);
|
//expect(data.meta.total).toBe(2);
|
||||||
expect(data.meta.count).toBe(1);
|
//expect(data.meta.count).toBe(1);
|
||||||
expect(data.meta.items).toBe(1);
|
expect(data.meta.items).toBe(1);
|
||||||
expect(data.data.length).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 () => {
|
test("/:entity/:id (delete one)", async () => {
|
||||||
const res = await app.request("/entity/posts/2", {
|
const res = await app.request("/entity/posts/2", {
|
||||||
method: "DELETE"
|
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] });
|
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
const res2 = await app.request("/entity/posts");
|
const res2 = await app.request("/entity/posts");
|
||||||
const data2 = (await res2.json()) as RepositoryResponse;
|
const data2 = (await res2.json()) as RepositoryResultJSON;
|
||||||
expect(data2.meta.total).toBe(1);
|
//expect(data2.meta.total).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import { Value, _jsonp } from "../../src/core/utils";
|
|
||||||
import { type RepoQuery, WhereBuilder, type WhereQuery, querySchema } from "../../src/data";
|
|
||||||
import type { RepoQueryIn } from "../../src/data/server/data-query-impl";
|
|
||||||
import { getDummyConnection } from "./helper";
|
|
||||||
|
|
||||||
const decode = (input: RepoQueryIn, expected: RepoQuery) => {
|
|
||||||
const result = Value.Decode(querySchema, input);
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("data-query-impl", () => {
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
test("single validation", () => {
|
|
||||||
const tests: [WhereQuery, string, any[]][] = [
|
|
||||||
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
|
|
||||||
[{ name: { $eq: "Michael" } }, '"name" = ?', ["Michael"]],
|
|
||||||
[{ int: { $between: [1, 2] } }, '"int" between ? and ?', [1, 2]],
|
|
||||||
[{ val: { $isnull: 1 } }, '"val" is null', []],
|
|
||||||
[{ val: { $isnull: true } }, '"val" is null', []],
|
|
||||||
[{ 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"]]
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [query, expectedSql, expectedParams] of tests) {
|
|
||||||
const { sql, parameters } = compile(query);
|
|
||||||
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
|
||||||
expect(parameters).toEqual(expectedParams);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("multiple validations", () => {
|
|
||||||
const tests: [WhereQuery, string, any[]][] = [
|
|
||||||
// multiple constraints per property
|
|
||||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
|
||||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
|
||||||
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
|
||||||
|
|
||||||
// multiple properties
|
|
||||||
[
|
|
||||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
|
||||||
'("val1" = ? and "val2" = ?)',
|
|
||||||
["foo", "bar"]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
|
||||||
'("val1" = ? and "val2" = ?)',
|
|
||||||
["foo", "bar"]
|
|
||||||
],
|
|
||||||
|
|
||||||
// or constructs
|
|
||||||
[
|
|
||||||
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
|
||||||
'("val1" = ? or "val2" = ?)',
|
|
||||||
["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]]
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [query, expectedSql, expectedParams] of tests) {
|
|
||||||
const { sql, parameters } = compile(query);
|
|
||||||
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
|
||||||
expect(parameters).toEqual(expectedParams);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("keys", () => {
|
|
||||||
const tests: [WhereQuery, string[]][] = [
|
|
||||||
// multiple constraints per property
|
|
||||||
[{ val: { $lt: 10, $gte: 3 } }, ["val"]],
|
|
||||||
|
|
||||||
// multiple properties
|
|
||||||
[{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, ["val1", "val2"]],
|
|
||||||
|
|
||||||
// or constructs
|
|
||||||
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
|
|
||||||
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]]
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [query, expectedKeys] of tests) {
|
|
||||||
const keys = WhereBuilder.getPropertyNames(query);
|
|
||||||
expect(keys).toEqual(expectedKeys);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("with", () => {
|
|
||||||
decode({ with: ["posts"] }, { with: { posts: {} } });
|
|
||||||
decode({ with: { posts: {} } }, { with: { posts: {} } });
|
|
||||||
decode({ with: { posts: { limit: 1 } } }, { with: { posts: { limit: 1 } } });
|
|
||||||
decode(
|
|
||||||
{
|
|
||||||
with: {
|
|
||||||
posts: {
|
|
||||||
with: {
|
|
||||||
images: {
|
|
||||||
select: ["id"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
with: {
|
|
||||||
posts: {
|
|
||||||
with: {
|
|
||||||
images: {
|
|
||||||
select: ["id"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// over http
|
|
||||||
{
|
|
||||||
const output = { with: { images: {} } };
|
|
||||||
decode({ with: "images" }, output);
|
|
||||||
decode({ with: '["images"]' }, output);
|
|
||||||
decode({ with: ["images"] }, output);
|
|
||||||
decode({ with: { images: {} } }, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const output = { with: { images: {}, comments: {} } };
|
|
||||||
decode({ with: "images,comments" }, output);
|
|
||||||
decode({ with: ["images", "comments"] }, output);
|
|
||||||
decode({ with: '["images", "comments"]' }, output);
|
|
||||||
decode({ with: { images: {}, comments: {} } }, output);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("data-query-impl: Typebox", () => {
|
|
||||||
test("sort", async () => {
|
|
||||||
const _dflt = { sort: { by: "id", dir: "asc" } };
|
|
||||||
|
|
||||||
decode({ sort: "" }, _dflt);
|
|
||||||
decode({ sort: "name" }, { sort: { by: "name", dir: "asc" } });
|
|
||||||
decode({ sort: "-name" }, { sort: { by: "name", dir: "desc" } });
|
|
||||||
decode({ sort: "-posts.name" }, { sort: { by: "posts.name", dir: "desc" } });
|
|
||||||
decode({ sort: "-1name" }, _dflt);
|
|
||||||
decode({ sort: { by: "name", dir: "desc" } }, { sort: { by: "name", dir: "desc" } });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import {
|
import { Entity, EntityManager } from "data/entities";
|
||||||
Entity,
|
import { TextField, PrimaryField, NumberField } from "data/fields";
|
||||||
EntityManager,
|
|
||||||
NumberField,
|
|
||||||
PrimaryField,
|
|
||||||
Repository,
|
|
||||||
TextField
|
|
||||||
} from "../../src/data";
|
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -18,14 +12,14 @@ describe("some tests", async () => {
|
|||||||
|
|
||||||
const users = new Entity("users", [
|
const users = new Entity("users", [
|
||||||
new TextField("username", { required: true, default_value: "nobody" }),
|
new TextField("username", { required: true, default_value: "nobody" }),
|
||||||
new TextField("email", { maxLength: 3 })
|
new TextField("email", { maxLength: 3 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const posts = new Entity("posts", [
|
const posts = new Entity("posts", [
|
||||||
new TextField("title"),
|
new TextField("title"),
|
||||||
new TextField("content"),
|
new TextField("content"),
|
||||||
new TextField("created_at"),
|
new TextField("created_at"),
|
||||||
new NumberField("likes", { default_value: 0 })
|
new NumberField("likes", { default_value: 0 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const em = new EntityManager([users, posts], connection);
|
const em = new EntityManager([users, posts], connection);
|
||||||
@@ -34,39 +28,32 @@ describe("some tests", async () => {
|
|||||||
|
|
||||||
test("findId", async () => {
|
test("findId", async () => {
|
||||||
const query = await em.repository(users).findId(1);
|
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(
|
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.parameters).toEqual([1, 1, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findMany", async () => {
|
test("findMany", async () => {
|
||||||
const query = await em.repository(users).findMany();
|
const query = await em.repository(users).findMany();
|
||||||
|
|
||||||
expect(query.sql).toBe(
|
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.parameters).toEqual([10, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("findMany with number", async () => {
|
test("findMany with number", async () => {
|
||||||
const query = await em.repository(posts).findMany();
|
const query = await em.repository(posts).findMany();
|
||||||
|
|
||||||
expect(query.sql).toBe(
|
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.parameters).toEqual([10, 0]);
|
||||||
expect(query.result).toEqual([]);
|
expect(query.data).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("try adding an existing field name", async () => {
|
test("try adding an existing field name", async () => {
|
||||||
@@ -74,7 +61,7 @@ describe("some tests", async () => {
|
|||||||
new Entity("users", [
|
new Entity("users", [
|
||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email"),
|
new TextField("email"),
|
||||||
new TextField("email") // not throwing, it's just being ignored
|
new TextField("email"), // not throwing, it's just being ignored
|
||||||
]);
|
]);
|
||||||
}).toBeDefined();
|
}).toBeDefined();
|
||||||
|
|
||||||
@@ -83,7 +70,7 @@ describe("some tests", async () => {
|
|||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email"),
|
new TextField("email"),
|
||||||
// field config differs, will throw
|
// field config differs, will throw
|
||||||
new TextField("email", { required: true })
|
new TextField("email", { required: true }),
|
||||||
]);
|
]);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
|
|
||||||
@@ -91,7 +78,7 @@ describe("some tests", async () => {
|
|||||||
new Entity("users", [
|
new Entity("users", [
|
||||||
new PrimaryField(),
|
new PrimaryField(),
|
||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email")
|
new TextField("email"),
|
||||||
]);
|
]);
|
||||||
}).toBeDefined();
|
}).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -110,4 +97,18 @@ describe("some tests", async () => {
|
|||||||
new EntityManager([entity, entity2], connection);
|
new EntityManager([entity, entity2], connection);
|
||||||
}).toThrow();
|
}).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";
|
import type { SqliteDatabase } from "kysely";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Database from "libsql";
|
import Database from "libsql";
|
||||||
import { SqliteLocalConnection } from "../../src/data";
|
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||||
|
|
||||||
export function getDummyDatabase(memory: boolean = true): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
|||||||
afterAllCleanup: async () => {
|
afterAllCleanup: async () => {
|
||||||
if (!memory) await unlink(DB_NAME);
|
if (!memory) await unlink(DB_NAME);
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dummyConnection,
|
dummyConnection,
|
||||||
afterAllCleanup
|
afterAllCleanup,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import {
|
import { Entity } from "data/entities";
|
||||||
Entity,
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
EntityManager,
|
import { ManyToOneRelation } from "data/relations";
|
||||||
ManyToOneRelation,
|
import { NumberField, TextField } from "data/fields";
|
||||||
NumberField,
|
import { SchemaManager } from "data/schema/SchemaManager";
|
||||||
SchemaManager,
|
|
||||||
TextField
|
|
||||||
} from "../../src/data";
|
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -21,7 +18,7 @@ describe("Mutator relation", async () => {
|
|||||||
const posts = new Entity("posts", [
|
const posts = new Entity("posts", [
|
||||||
new TextField("title"),
|
new TextField("title"),
|
||||||
new TextField("content", { default_value: "..." }),
|
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")]);
|
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: 10 })).rejects.toThrow();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
em.mutator(posts).insertOne({ title: "post2", users_id: data.id })
|
em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import { TransformPersistFailedException } from "../../src/data/errors";
|
import { NumberField, TextField } from "data/fields";
|
||||||
|
import { TransformPersistFailedException } from "data/errors";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -14,7 +15,7 @@ describe("Mutator simple", async () => {
|
|||||||
|
|
||||||
const items = new Entity("items", [
|
const items = new Entity("items", [
|
||||||
new TextField("label", { required: true, minLength: 1 }),
|
new TextField("label", { required: true, minLength: 1 }),
|
||||||
new NumberField("count", { default_value: 0 })
|
new NumberField("count", { default_value: 0 }),
|
||||||
]);
|
]);
|
||||||
const em = new EntityManager<any>([items], connection);
|
const em = new EntityManager<any>([items], connection);
|
||||||
|
|
||||||
@@ -29,11 +30,11 @@ describe("Mutator simple", async () => {
|
|||||||
test("insert single row", async () => {
|
test("insert single row", async () => {
|
||||||
const mutation = await em.mutator(items).insertOne({
|
const mutation = await em.mutator(items).insertOne({
|
||||||
label: "test",
|
label: "test",
|
||||||
count: 1
|
count: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mutation.sql).toBe(
|
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 });
|
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
|
||||||
|
|
||||||
@@ -41,11 +42,11 @@ describe("Mutator simple", async () => {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
sort: {
|
sort: {
|
||||||
by: "id",
|
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 () => {
|
test("update inserted row", async () => {
|
||||||
@@ -53,18 +54,18 @@ describe("Mutator simple", async () => {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
sort: {
|
sort: {
|
||||||
by: "id",
|
by: "id",
|
||||||
dir: "desc"
|
dir: "desc",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const id = query.data![0].id as number;
|
const id = query.data![0].id as number;
|
||||||
|
|
||||||
const mutation = await em.mutator(items).updateOne(id, {
|
const mutation = await em.mutator(items).updateOne(id, {
|
||||||
label: "new label",
|
label: "new label",
|
||||||
count: 100
|
count: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mutation.sql).toBe(
|
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 });
|
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||||
});
|
});
|
||||||
@@ -74,27 +75,27 @@ describe("Mutator simple", async () => {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
sort: {
|
sort: {
|
||||||
by: "id",
|
by: "id",
|
||||||
dir: "desc"
|
dir: "desc",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = query.data![0].id as number;
|
const id = query.data![0].id as number;
|
||||||
const mutation = await em.mutator(items).deleteOne(id);
|
const mutation = await em.mutator(items).deleteOne(id);
|
||||||
|
|
||||||
expect(mutation.sql).toBe(
|
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 });
|
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||||
|
|
||||||
const query2 = await em.repository(items).findId(id);
|
const query2 = await em.repository(items).findId(id);
|
||||||
expect(query2.result.length).toBe(0);
|
expect(query2.data).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validation: insert incomplete row", async () => {
|
test("validation: insert incomplete row", async () => {
|
||||||
const incompleteCreate = async () =>
|
const incompleteCreate = async () =>
|
||||||
await em.mutator(items).insertOne({
|
await em.mutator(items).insertOne({
|
||||||
//label: "test",
|
//label: "test",
|
||||||
count: 1
|
count: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(incompleteCreate()).rejects.toThrow();
|
expect(incompleteCreate()).rejects.toThrow();
|
||||||
@@ -104,7 +105,7 @@ describe("Mutator simple", async () => {
|
|||||||
const invalidCreate1 = async () =>
|
const invalidCreate1 = async () =>
|
||||||
await em.mutator(items).insertOne({
|
await em.mutator(items).insertOne({
|
||||||
label: 111, // this should work
|
label: 111, // this should work
|
||||||
count: "1" // this should fail
|
count: "1", // this should fail
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
|
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
|
||||||
@@ -112,7 +113,7 @@ describe("Mutator simple", async () => {
|
|||||||
const invalidCreate2 = async () =>
|
const invalidCreate2 = async () =>
|
||||||
await em.mutator(items).insertOne({
|
await em.mutator(items).insertOne({
|
||||||
label: "", // this should fail
|
label: "", // this should fail
|
||||||
count: 1
|
count: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
|
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
|
||||||
@@ -137,7 +138,7 @@ describe("Mutator simple", async () => {
|
|||||||
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||||
//console.log((await em.repository(items).findMany()).data);
|
//console.log((await em.repository(items).findMany()).data);
|
||||||
|
|
||||||
await em.mutator(items).deleteWhere();
|
await em.mutator(items).deleteWhere({ id: { $isnull: 0 } });
|
||||||
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
||||||
|
|
||||||
//expect(res.data.count).toBe(0);
|
//expect(res.data.count).toBe(0);
|
||||||
@@ -152,38 +153,38 @@ describe("Mutator simple", async () => {
|
|||||||
await em.mutator(items).updateWhere(
|
await em.mutator(items).updateWhere(
|
||||||
{ count: 2 },
|
{ count: 2 },
|
||||||
{
|
{
|
||||||
count: 10
|
count: 10,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
expect((await em.repository(items).findMany()).data).toEqual([
|
expect((await em.repository(items).findMany()).data).toEqual([
|
||||||
{ id: 6, label: "update", count: 1 },
|
{ id: 6, label: "update", count: 1 },
|
||||||
{ id: 7, label: "update too", count: 1 },
|
{ id: 7, label: "update too", count: 1 },
|
||||||
{ id: 8, label: "keep", count: 0 }
|
{ id: 8, label: "keep", count: 0 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// expect 2 to be updated
|
// expect 2 to be updated
|
||||||
await em.mutator(items).updateWhere(
|
await em.mutator(items).updateWhere(
|
||||||
{ count: 2 },
|
{ count: 2 },
|
||||||
{
|
{
|
||||||
count: 1
|
count: 1,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect((await em.repository(items).findMany()).data).toEqual([
|
expect((await em.repository(items).findMany()).data).toEqual([
|
||||||
{ id: 6, label: "update", count: 2 },
|
{ id: 6, label: "update", count: 2 },
|
||||||
{ id: 7, label: "update too", count: 2 },
|
{ id: 7, label: "update too", count: 2 },
|
||||||
{ id: 8, label: "keep", count: 0 }
|
{ id: 8, label: "keep", count: 0 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("insertMany", async () => {
|
test("insertMany", async () => {
|
||||||
const oldCount = (await em.repo(items).count()).count;
|
const oldCount = (await em.repo(items).count()).data.count;
|
||||||
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||||
const { data } = await em.mutator(items).insertMany(inserts);
|
const { data } = await em.mutator(items).insertMany(inserts);
|
||||||
|
|
||||||
expect(data.length).toBe(2);
|
expect(data.length).toBe(2);
|
||||||
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
||||||
const newCount = (await em.repo(items).count()).count;
|
const newCount = (await em.repo(items).count()).data.count;
|
||||||
expect(newCount).toBe(oldCount + inserts.length);
|
expect(newCount).toBe(oldCount + inserts.length);
|
||||||
|
|
||||||
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||||
import { stripMark } from "../../src/core/utils";
|
import { stripMark } from "core/utils/schema";
|
||||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
|
import { TextField } from "data/fields";
|
||||||
|
import { PolymorphicRelation } from "data/relations";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -23,23 +25,23 @@ describe("Polymorphic", async () => {
|
|||||||
source: "categories",
|
source: "categories",
|
||||||
target: "media",
|
target: "media",
|
||||||
config: {
|
config: {
|
||||||
mappedBy: "image"
|
mappedBy: "image",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
// media should not see categories
|
// media should not see categories
|
||||||
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
|
||||||
|
|
||||||
// it's important that media cannot access categories
|
// it's important that media cannot access categories
|
||||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
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.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||||
|
|
||||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.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([
|
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([]);
|
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ describe("Polymorphic", async () => {
|
|||||||
"id",
|
"id",
|
||||||
"path",
|
"path",
|
||||||
"reference",
|
"reference",
|
||||||
"entity_id"
|
"entity_id",
|
||||||
]);
|
]);
|
||||||
expect(media.getSelect()).toEqual(["id", "path"]);
|
expect(media.getSelect()).toEqual(["id", "path"]);
|
||||||
});
|
});
|
||||||
@@ -60,7 +62,7 @@ describe("Polymorphic", async () => {
|
|||||||
const entities = [media, categories];
|
const entities = [media, categories];
|
||||||
const single = new PolymorphicRelation(categories, media, {
|
const single = new PolymorphicRelation(categories, media, {
|
||||||
mappedBy: "single",
|
mappedBy: "single",
|
||||||
targetCardinality: 1
|
targetCardinality: 1,
|
||||||
});
|
});
|
||||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||||
|
|
||||||
@@ -71,17 +73,17 @@ describe("Polymorphic", async () => {
|
|||||||
|
|
||||||
// it's important that media cannot access categories
|
// it's important that media cannot access categories
|
||||||
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
|
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.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
|
||||||
|
|
||||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
|
||||||
"media",
|
"media",
|
||||||
"media"
|
"media",
|
||||||
]);
|
]);
|
||||||
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
|
||||||
"single",
|
"single",
|
||||||
"multiple"
|
"multiple",
|
||||||
]);
|
]);
|
||||||
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ describe("Polymorphic", async () => {
|
|||||||
"id",
|
"id",
|
||||||
"path",
|
"path",
|
||||||
"reference",
|
"reference",
|
||||||
"entity_id"
|
"entity_id",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, beforeAll, afterAll, test } from "bun:test";
|
||||||
|
import type { PostgresConnection } from "data/connection/postgres/PostgresConnection";
|
||||||
|
import { pg, postgresJs } from "bknd";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import { disableConsoleLog, enableConsoleLog, $waitUntil } from "bknd/utils";
|
||||||
|
import { $ } from "bun";
|
||||||
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
host: "localhost",
|
||||||
|
port: 5430,
|
||||||
|
user: "postgres",
|
||||||
|
password: "postgres",
|
||||||
|
database: "bknd",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function cleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
|
||||||
|
const kysely = connection.kysely;
|
||||||
|
|
||||||
|
// drop all tables+indexes & create new schema
|
||||||
|
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
|
||||||
|
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
|
||||||
|
await kysely.schema.createSchema("public").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isPostgresRunning() {
|
||||||
|
try {
|
||||||
|
// Try to actually connect to PostgreSQL
|
||||||
|
const conn = pg({ pool: new Pool(credentials) });
|
||||||
|
await conn.ping();
|
||||||
|
await conn.close();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("postgres", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
if (!(await isPostgresRunning())) {
|
||||||
|
await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`;
|
||||||
|
await $waitUntil("Postgres is running", isPostgresRunning);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
disableConsoleLog();
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
if (await isPostgresRunning()) {
|
||||||
|
try {
|
||||||
|
await $`docker stop bknd-test-postgres`;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enableConsoleLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.serial.each([
|
||||||
|
["pg", () => pg({ pool: new Pool(credentials) })],
|
||||||
|
["postgresjs", () => postgresJs({ postgres: postgres(credentials) })],
|
||||||
|
])("%s", (name, createConnection) => {
|
||||||
|
connectionTestSuite(
|
||||||
|
{
|
||||||
|
...bunTestRunner,
|
||||||
|
test: test.serial,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
makeConnection: () => {
|
||||||
|
const connection = createConnection();
|
||||||
|
return {
|
||||||
|
connection,
|
||||||
|
dispose: async () => {
|
||||||
|
await cleanDatabase(connection);
|
||||||
|
await connection.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rawDialectDetails: [],
|
||||||
|
disableConsoleLog: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,19 +2,20 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
Entity,
|
|
||||||
EntityIndex,
|
|
||||||
EntityManager,
|
|
||||||
EnumField,
|
EnumField,
|
||||||
JsonField,
|
JsonField,
|
||||||
|
NumberField,
|
||||||
|
TextField,
|
||||||
|
EntityIndex,
|
||||||
|
} from "data/fields";
|
||||||
|
import { Entity, EntityManager } from "data/entities";
|
||||||
|
import {
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
NumberField,
|
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
TextField
|
} from "data/relations";
|
||||||
} from "../../src/data";
|
import { DummyConnection } from "data/connection/DummyConnection";
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
|
||||||
import {
|
import {
|
||||||
FieldPrototype,
|
FieldPrototype,
|
||||||
type FieldSchema,
|
type FieldSchema,
|
||||||
@@ -31,9 +32,9 @@ import {
|
|||||||
medium,
|
medium,
|
||||||
number,
|
number,
|
||||||
relation,
|
relation,
|
||||||
text
|
text,
|
||||||
} from "../../src/data/prototype";
|
} from "data/prototype";
|
||||||
import { MediaField } from "../../src/media/MediaField";
|
import { MediaField } from "media/MediaField";
|
||||||
|
|
||||||
describe("prototype", () => {
|
describe("prototype", () => {
|
||||||
test("...", () => {
|
test("...", () => {
|
||||||
@@ -54,7 +55,7 @@ describe("prototype", () => {
|
|||||||
name: text(),
|
name: text(),
|
||||||
bio: text(),
|
bio: text(),
|
||||||
age: number(),
|
age: number(),
|
||||||
some: number()
|
some: number(),
|
||||||
});
|
});
|
||||||
type db = {
|
type db = {
|
||||||
users: Schema<typeof users>;
|
users: Schema<typeof users>;
|
||||||
@@ -70,7 +71,7 @@ describe("prototype", () => {
|
|||||||
name: text({ default_value: "hello" }).required(),
|
name: text({ default_value: "hello" }).required(),
|
||||||
bio: text(),
|
bio: text(),
|
||||||
age: number(),
|
age: number(),
|
||||||
some: number().required()
|
some: number().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
|
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
|
||||||
@@ -83,12 +84,12 @@ describe("prototype", () => {
|
|||||||
new TextField("title", { required: true }),
|
new TextField("title", { required: true }),
|
||||||
new TextField("content"),
|
new TextField("content"),
|
||||||
new DateField("created_at", {
|
new DateField("created_at", {
|
||||||
type: "datetime"
|
type: "datetime",
|
||||||
}),
|
}),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
new MediaField("images", { entity: "posts" }),
|
new MediaField("images", { entity: "posts" }),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
new MediaField("cover", { entity: "posts", max_items: 1 })
|
new MediaField("cover", { entity: "posts", max_items: 1 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const posts2 = entity("posts", {
|
const posts2 = entity("posts", {
|
||||||
@@ -96,12 +97,13 @@ describe("prototype", () => {
|
|||||||
content: text(),
|
content: text(),
|
||||||
created_at: datetime(),
|
created_at: datetime(),
|
||||||
images: media(),
|
images: media(),
|
||||||
cover: medium()
|
cover: medium(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Posts = Schema<typeof posts2>;
|
type Posts = Schema<typeof posts2>;
|
||||||
|
|
||||||
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
// @todo: check
|
||||||
|
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("test example", async () => {
|
test("test example", async () => {
|
||||||
@@ -117,11 +119,11 @@ describe("prototype", () => {
|
|||||||
type: "objects",
|
type: "objects",
|
||||||
values: [
|
values: [
|
||||||
{ value: "active", label: "Active" },
|
{ value: "active", label: "Active" },
|
||||||
{ value: "inactive", label: "Not active" }
|
{ value: "inactive", label: "Not active" },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
new JsonField("json")
|
new JsonField("json"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const test2 = entity("test", {
|
const test2 = entity("test", {
|
||||||
@@ -134,10 +136,10 @@ describe("prototype", () => {
|
|||||||
status: enumm<"active" | "inactive">({
|
status: enumm<"active" | "inactive">({
|
||||||
enum: [
|
enum: [
|
||||||
{ value: "active", label: "Active" },
|
{ 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());
|
expect(test.toJSON()).toEqual(test2.toJSON());
|
||||||
@@ -161,12 +163,12 @@ describe("prototype", () => {
|
|||||||
// category has single image
|
// category has single image
|
||||||
new PolymorphicRelation(categories, _media, {
|
new PolymorphicRelation(categories, _media, {
|
||||||
mappedBy: "image",
|
mappedBy: "image",
|
||||||
targetCardinality: 1
|
targetCardinality: 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// post has multiple images
|
// post has multiple images
|
||||||
new PolymorphicRelation(posts, _media, { mappedBy: "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 = [
|
const relations2 = [
|
||||||
@@ -180,7 +182,7 @@ describe("prototype", () => {
|
|||||||
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
relation(categories).polyToOne(_media, { mappedBy: "image" }),
|
||||||
|
|
||||||
relation(posts).polyToMany(_media, { mappedBy: "images" }),
|
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()));
|
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
|
||||||
@@ -194,21 +196,21 @@ describe("prototype", () => {
|
|||||||
posts,
|
posts,
|
||||||
categories,
|
categories,
|
||||||
{
|
{
|
||||||
connectionTableMappedName: "custom"
|
connectionTableMappedName: "custom",
|
||||||
},
|
},
|
||||||
[new TextField("description")]
|
[new TextField("description")],
|
||||||
);
|
);
|
||||||
|
|
||||||
const fields = {
|
const fields = {
|
||||||
description: text()
|
description: text(),
|
||||||
};
|
};
|
||||||
let o: FieldSchema<typeof fields>;
|
let o: FieldSchema<typeof fields>;
|
||||||
const rel2 = relation(posts).manyToMany(
|
const rel2 = relation(posts).manyToMany(
|
||||||
categories,
|
categories,
|
||||||
{
|
{
|
||||||
connectionTableMappedName: "custom"
|
connectionTableMappedName: "custom",
|
||||||
},
|
},
|
||||||
fields
|
fields,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(rel.toJSON()).toEqual(rel2.toJSON());
|
expect(rel.toJSON()).toEqual(rel2.toJSON());
|
||||||
@@ -216,11 +218,11 @@ describe("prototype", () => {
|
|||||||
|
|
||||||
test("devexample", async () => {
|
test("devexample", async () => {
|
||||||
const users = entity("users", {
|
const users = entity("users", {
|
||||||
username: text()
|
username: text(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const comments = entity("comments", {
|
const comments = entity("comments", {
|
||||||
content: text()
|
content: text(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const posts = entity("posts", {
|
const posts = entity("posts", {
|
||||||
@@ -228,17 +230,17 @@ describe("prototype", () => {
|
|||||||
content: text(),
|
content: text(),
|
||||||
created_at: datetime(),
|
created_at: datetime(),
|
||||||
images: media(),
|
images: media(),
|
||||||
cover: medium()
|
cover: medium(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const categories = entity("categories", {
|
const categories = entity("categories", {
|
||||||
name: text(),
|
name: text(),
|
||||||
description: text(),
|
description: text(),
|
||||||
image: medium()
|
image: medium(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const settings = entity("settings", {
|
const settings = entity("settings", {
|
||||||
theme: text()
|
theme: text(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const test = entity("test", {
|
const test = entity("test", {
|
||||||
@@ -251,10 +253,10 @@ describe("prototype", () => {
|
|||||||
status: enumm<"active" | "inactive">({
|
status: enumm<"active" | "inactive">({
|
||||||
enum: [
|
enum: [
|
||||||
{ value: "active", label: "Active" },
|
{ 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", {});
|
const _media = entity("media", {});
|
||||||
@@ -270,7 +272,7 @@ describe("prototype", () => {
|
|||||||
relation(users).oneToOne(settings),
|
relation(users).oneToOne(settings),
|
||||||
|
|
||||||
relation(comments).manyToOne(users, { required: true }),
|
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;
|
const obj: Schema<typeof test> = {} as any;
|
||||||
@@ -281,12 +283,12 @@ describe("prototype", () => {
|
|||||||
{
|
{
|
||||||
posts: entity("posts", { name: text(), slug: text().required() }),
|
posts: entity("posts", { name: text(), slug: text().required() }),
|
||||||
comments: entity("comments", { some: text() }),
|
comments: entity("comments", { some: text() }),
|
||||||
users: entity("users", { email: text() })
|
users: entity("users", { email: text() }),
|
||||||
},
|
},
|
||||||
({ relation, index }, { posts, comments, users }) => {
|
({ relation, index }, { posts, comments, users }) => {
|
||||||
relation(posts).manyToOne(comments).manyToOne(users);
|
relation(posts).manyToOne(comments).manyToOne(users);
|
||||||
index(posts).on(["name"]).on(["slug"], true);
|
index(posts).on(["name"]).on(["slug"], true);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
type LocalDb = (typeof _em)["DB"];
|
type LocalDb = (typeof _em)["DB"];
|
||||||
@@ -294,16 +296,16 @@ describe("prototype", () => {
|
|||||||
const es = [
|
const es = [
|
||||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||||
new Entity("comments", [new TextField("some")]),
|
new Entity("comments", [new TextField("some")]),
|
||||||
new Entity("users", [new TextField("email")])
|
new Entity("users", [new TextField("email")]),
|
||||||
];
|
] as const;
|
||||||
const _em2 = new EntityManager(
|
const _em2 = new EntityManager(
|
||||||
es,
|
[...es],
|
||||||
new DummyConnection(),
|
new DummyConnection(),
|
||||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
[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("name")!]),
|
||||||
new EntityIndex(es[0], [es[0].field("slug")!], true)
|
new EntityIndex(es[0], [es[0].field("slug")!], true),
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
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 {
|
import {
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
RelationField
|
RelationField,
|
||||||
} from "../../src/data/relations";
|
} from "data/relations";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -22,32 +23,32 @@ describe("Relations", async () => {
|
|||||||
const r1 = new RelationField("users_id", {
|
const r1 = new RelationField("users_id", {
|
||||||
reference: "users",
|
reference: "users",
|
||||||
target: "users",
|
target: "users",
|
||||||
target_field: "id"
|
target_field: "id",
|
||||||
});
|
});
|
||||||
|
|
||||||
const sql1 = schema
|
const sql1 = schema
|
||||||
.createTable("posts")
|
.createTable("posts")
|
||||||
.addColumn(...r1.schema()!)
|
.addColumn(...em.connection.getFieldSchema(r1.schema())!)
|
||||||
.compile().sql;
|
.compile().sql;
|
||||||
|
|
||||||
expect(sql1).toBe(
|
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(new Entity("users"), "author");
|
||||||
const r2 = new RelationField("author_id", {
|
const r2 = new RelationField("author_id", {
|
||||||
reference: "author",
|
reference: "author",
|
||||||
target: "users",
|
target: "users",
|
||||||
target_field: "id"
|
target_field: "id",
|
||||||
});
|
});
|
||||||
|
|
||||||
const sql2 = schema
|
const sql2 = schema
|
||||||
.createTable("posts")
|
.createTable("posts")
|
||||||
.addColumn(...r2.schema()!)
|
.addColumn(...em.connection.getFieldSchema(r2.schema())!)
|
||||||
.compile().sql;
|
.compile().sql;
|
||||||
|
|
||||||
expect(sql2).toBe(
|
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",
|
reference: "users",
|
||||||
target: "users",
|
target: "users",
|
||||||
target_field: "id",
|
target_field: "id",
|
||||||
required: true
|
required: true,
|
||||||
});
|
});
|
||||||
expect(r1.isRequired()).toBeTrue();
|
expect(r1.isRequired()).toBeTrue();
|
||||||
});
|
});
|
||||||
@@ -66,8 +67,8 @@ describe("Relations", async () => {
|
|||||||
const users = new Entity("users", [new TextField("username")]);
|
const users = new Entity("users", [new TextField("username")]);
|
||||||
const posts = new Entity("posts", [
|
const posts = new Entity("posts", [
|
||||||
new TextField("title", {
|
new TextField("title", {
|
||||||
maxLength: 2
|
maxLength: 2,
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const entities = [users, posts];
|
const entities = [users, posts];
|
||||||
@@ -77,7 +78,7 @@ describe("Relations", async () => {
|
|||||||
const em = new EntityManager(entities, dummyConnection, relations);
|
const em = new EntityManager(entities, dummyConnection, relations);
|
||||||
|
|
||||||
// verify naming
|
// 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.entity.name).toBe(posts.name);
|
||||||
expect(rel.source.reference).toBe(posts.name);
|
expect(rel.source.reference).toBe(posts.name);
|
||||||
expect(rel.target.entity.name).toBe(users.name);
|
expect(rel.target.entity.name).toBe(users.name);
|
||||||
@@ -89,11 +90,11 @@ describe("Relations", async () => {
|
|||||||
// verify low level relation
|
// 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).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(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).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)
|
// verify high level relation (from users)
|
||||||
const userPostsRel = em.relationOf(users.name, "posts");
|
const userPostsRel = em.relationOf(users.name, "posts");
|
||||||
@@ -122,7 +123,7 @@ describe("Relations", async () => {
|
|||||||
.selectFrom(users.name)
|
.selectFrom(users.name)
|
||||||
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
|
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
|
||||||
expect(selectPostsFromUsers.compile().sql).toBe(
|
expect(selectPostsFromUsers.compile().sql).toBe(
|
||||||
'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") 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);
|
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
|
||||||
const userObj = { id: 1, username: "test" };
|
const userObj = { id: 1, username: "test" };
|
||||||
@@ -142,7 +143,7 @@ describe("Relations", async () => {
|
|||||||
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
|
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
|
||||||
|
|
||||||
expect(selectUsersFromPosts.compile().sql).toBe(
|
expect(selectUsersFromPosts.compile().sql).toBe(
|
||||||
'select (select 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);
|
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
|
||||||
const postObj = { id: 1, title: "test" };
|
const postObj = { id: 1, title: "test" };
|
||||||
@@ -158,7 +159,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: undefined,
|
primary: undefined,
|
||||||
cardinality: undefined,
|
cardinality: undefined,
|
||||||
relation_type: "n:1"
|
relation_type: "n:1",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
|
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||||
@@ -170,7 +171,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: "id",
|
primary: "id",
|
||||||
cardinality: 1,
|
cardinality: 1,
|
||||||
relation_type: "n:1"
|
relation_type: "n:1",
|
||||||
});
|
});
|
||||||
|
|
||||||
/*console.log("ManyToOne (source=posts, target=users)");
|
/*console.log("ManyToOne (source=posts, target=users)");
|
||||||
@@ -191,7 +192,7 @@ describe("Relations", async () => {
|
|||||||
const em = new EntityManager(entities, dummyConnection, relations);
|
const em = new EntityManager(entities, dummyConnection, relations);
|
||||||
|
|
||||||
// verify naming
|
// 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.entity.name).toBe(users.name);
|
||||||
expect(rel.source.reference).toBe(users.name);
|
expect(rel.source.reference).toBe(users.name);
|
||||||
expect(rel.target.entity.name).toBe(settings.name);
|
expect(rel.target.entity.name).toBe(settings.name);
|
||||||
@@ -202,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).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]!.source.entity).toBe(users);
|
||||||
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
|
||||||
|
|
||||||
// verify high level relation (from users)
|
// verify high level relation (from users)
|
||||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||||
@@ -225,7 +226,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: "id",
|
primary: "id",
|
||||||
cardinality: 1,
|
cardinality: 1,
|
||||||
relation_type: "1:1"
|
relation_type: "1:1",
|
||||||
});
|
});
|
||||||
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
|
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "users",
|
reference: "users",
|
||||||
@@ -236,7 +237,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: undefined,
|
primary: undefined,
|
||||||
cardinality: 1,
|
cardinality: 1,
|
||||||
relation_type: "1:1"
|
relation_type: "1:1",
|
||||||
});
|
});
|
||||||
|
|
||||||
/*console.log("");
|
/*console.log("");
|
||||||
@@ -310,20 +311,20 @@ describe("Relations", async () => {
|
|||||||
*/
|
*/
|
||||||
const selectCategoriesFromPosts = kysely
|
const selectCategoriesFromPosts = kysely
|
||||||
.selectFrom(posts.name)
|
.selectFrom(posts.name)
|
||||||
.select((eb) => postCategoriesRel.buildWith(posts)(eb).as("categories"));
|
.select((eb) => postCategoriesRel.buildWith(posts)(eb).select("id").as("categories"));
|
||||||
expect(selectCategoriesFromPosts.compile().sql).toBe(
|
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 = kysely
|
const selectPostsFromCategories = kysely
|
||||||
.selectFrom(categories.name)
|
.selectFrom(categories.name)
|
||||||
.select((eb) => postCategoriesRel.buildWith(categories)(eb).as("posts"));
|
.select((eb) => postCategoriesRel.buildWith(categories)(eb).select("id").as("posts"));
|
||||||
expect(selectPostsFromCategories.compile().sql).toBe(
|
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
|
// mutation info
|
||||||
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "categories",
|
reference: "categories",
|
||||||
local_field: undefined,
|
local_field: undefined,
|
||||||
$set: false,
|
$set: false,
|
||||||
@@ -332,9 +333,9 @@ describe("Relations", async () => {
|
|||||||
$detach: true,
|
$detach: true,
|
||||||
primary: "id",
|
primary: "id",
|
||||||
cardinality: undefined,
|
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",
|
reference: "posts",
|
||||||
local_field: undefined,
|
local_field: undefined,
|
||||||
$set: false,
|
$set: false,
|
||||||
@@ -343,7 +344,7 @@ describe("Relations", async () => {
|
|||||||
$detach: false,
|
$detach: false,
|
||||||
primary: undefined,
|
primary: undefined,
|
||||||
cardinality: undefined,
|
cardinality: undefined,
|
||||||
relation_type: "m:n"
|
relation_type: "m:n",
|
||||||
});
|
});
|
||||||
|
|
||||||
/*console.log("");
|
/*console.log("");
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
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 () => {
|
describe("[data] Entity", async () => {
|
||||||
const entity = new Entity("test", [
|
const entity = new Entity("test", [
|
||||||
new TextField("name", { required: true }),
|
new TextField("name", { required: true }),
|
||||||
new TextField("description"),
|
new TextField("description"),
|
||||||
new NumberField("age", { fillable: false, default_value: 18 }),
|
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 () => {
|
test("getSelect", async () => {
|
||||||
@@ -17,7 +18,7 @@ describe("[data] Entity", async () => {
|
|||||||
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
|
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"hidden"
|
"hidden",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ describe("[data] Entity", async () => {
|
|||||||
test("getDefaultObject", async () => {
|
test("getDefaultObject", async () => {
|
||||||
expect(entity.getDefaultObject()).toEqual({
|
expect(entity.getDefaultObject()).toEqual({
|
||||||
age: 18,
|
age: 18,
|
||||||
hidden: "secret"
|
hidden: "secret",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,15 +47,4 @@ describe("[data] Entity", async () => {
|
|||||||
entity.addField(field);
|
entity.addField(field);
|
||||||
expect(entity.getField("new_field")).toBe(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 { afterAll, describe, expect, test } from "bun:test";
|
||||||
import {
|
import { Entity, EntityManager } from "data/entities";
|
||||||
Entity,
|
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
||||||
EntityManager,
|
import { SchemaManager } from "data/schema/SchemaManager";
|
||||||
ManyToManyRelation,
|
import { UnableToConnectException } from "data/errors";
|
||||||
ManyToOneRelation,
|
|
||||||
SchemaManager
|
|
||||||
} from "../../../src/data";
|
|
||||||
import { UnableToConnectException } from "../../../src/data/errors";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -25,7 +21,7 @@ describe("[data] EntityManager", async () => {
|
|||||||
expect(await em.ping()).toBe(true);
|
expect(await em.ping()).toBe(true);
|
||||||
expect(() => em.entity("...")).toThrow();
|
expect(() => em.entity("...")).toThrow();
|
||||||
expect(() =>
|
expect(() =>
|
||||||
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2")))
|
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
expect(em.schema()).toBeInstanceOf(SchemaManager);
|
expect(em.schema()).toBeInstanceOf(SchemaManager);
|
||||||
|
|
||||||
@@ -47,8 +43,8 @@ describe("[data] EntityManager", async () => {
|
|||||||
em.addRelation(new ManyToOneRelation(posts, users));
|
em.addRelation(new ManyToOneRelation(posts, users));
|
||||||
expect(em.relations.all.length).toBe(1);
|
expect(em.relations.all.length).toBe(1);
|
||||||
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
|
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
|
||||||
expect(em.relationsOf("users")).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.relationsOf("posts")).toEqual([em.relations.all[0]!]);
|
||||||
expect(em.hasRelations("users")).toBe(true);
|
expect(em.hasRelations("users")).toBe(true);
|
||||||
expect(em.hasRelations("posts")).toBe(true);
|
expect(em.hasRelations("posts")).toBe(true);
|
||||||
expect(em.relatedEntitiesOf("users")).toEqual([posts]);
|
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(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
|
||||||
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
|
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
|
||||||
"comments",
|
"comments",
|
||||||
"categories"
|
"categories",
|
||||||
]);
|
]);
|
||||||
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
|
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
|
||||||
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
|
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
import { ManyToOneRelation } from "data/relations";
|
||||||
|
import { TextField } from "data/fields";
|
||||||
|
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("[data] JoinBuilder", async () => {
|
describe("[data] JoinBuilder", async () => {
|
||||||
test("missing relation", async () => {
|
test("missing relation", async () => {
|
||||||
@@ -12,7 +17,7 @@ describe("[data] JoinBuilder", async () => {
|
|||||||
const em = new EntityManager([users], dummyConnection);
|
const em = new EntityManager([users], dummyConnection);
|
||||||
|
|
||||||
expect(() =>
|
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');
|
).toThrow('Relation "posts" not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,7 +28,7 @@ describe("[data] JoinBuilder", async () => {
|
|||||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||||
|
|
||||||
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
|
||||||
"posts"
|
"posts",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
@@ -34,7 +39,7 @@ describe("[data] JoinBuilder", async () => {
|
|||||||
);*/
|
);*/
|
||||||
|
|
||||||
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
|
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
|
||||||
"author"
|
"author",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
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 type { EventManager } from "../../../src/core/events";
|
||||||
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import {
|
import {
|
||||||
Entity,
|
|
||||||
EntityManager,
|
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
MutatorEvents,
|
|
||||||
NumberField,
|
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
type RelationField,
|
RelationField,
|
||||||
RelationMutator,
|
RelationMutator,
|
||||||
TextField
|
} from "data/relations";
|
||||||
} from "../../../src/data";
|
import { NumberField, TextField } from "data/fields";
|
||||||
import * as proto from "../../../src/data/prototype";
|
import * as proto from "data/prototype";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { MutatorEvents } from "data/events";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("[data] Mutator (base)", async () => {
|
describe("[data] Mutator (base)", async () => {
|
||||||
const entity = new Entity("items", [
|
const entity = new Entity("items", [
|
||||||
new TextField("label", { required: true }),
|
new TextField("label", { required: true }),
|
||||||
new NumberField("count"),
|
new NumberField("count"),
|
||||||
new TextField("hidden", { hidden: true }),
|
new TextField("hidden", { hidden: true }),
|
||||||
new TextField("not_fillable", { fillable: false })
|
new TextField("not_fillable", { fillable: false }),
|
||||||
]);
|
]);
|
||||||
const em = new EntityManager<any>([entity], dummyConnection);
|
const em = new EntityManager<any>([entity], dummyConnection);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
@@ -44,7 +46,7 @@ describe("[data] Mutator (base)", async () => {
|
|||||||
test("updateOne", async () => {
|
test("updateOne", async () => {
|
||||||
const { data } = await em.mutator(entity).insertOne(payload);
|
const { data } = await em.mutator(entity).insertOne(payload);
|
||||||
const updated = await em.mutator(entity).updateOne(data.id, {
|
const updated = await em.mutator(entity).updateOne(data.id, {
|
||||||
count: 2
|
count: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(updated.parameters).toEqual([2, data.id]);
|
expect(updated.parameters).toEqual([2, data.id]);
|
||||||
@@ -77,7 +79,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
|
|
||||||
// persisting relational field should just return key value to be added
|
// persisting relational field should just return key value to be added
|
||||||
expect(
|
expect(
|
||||||
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id)
|
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
|
||||||
).resolves.toEqual(["users_id", userData.data.id]);
|
).resolves.toEqual(["users_id", userData.data.id]);
|
||||||
|
|
||||||
// persisting invalid value should throw
|
// persisting invalid value should throw
|
||||||
@@ -86,8 +88,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
// persisting reference should ...
|
// persisting reference should ...
|
||||||
expect(
|
expect(
|
||||||
postRelMutator.persistReference(relations[0]!, "users", {
|
postRelMutator.persistReference(relations[0]!, "users", {
|
||||||
$set: { id: userData.data.id }
|
$set: { id: userData.data.id },
|
||||||
})
|
}),
|
||||||
).resolves.toEqual(["users_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
|
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
|
||||||
|
|
||||||
@@ -99,8 +101,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(posts).insertOne({
|
em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users_id: 100 // user does not exist yet
|
users_id: 100, // user does not exist yet
|
||||||
})
|
}),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
const em = new EntityManager([items, cats], dummyConnection, relations);
|
const em = new EntityManager([items, cats], dummyConnection, relations);
|
||||||
|
|
||||||
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
|
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 { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||||
const res = await em.mutator(posts).insertOne({
|
const res = await em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users_id: data.id
|
users_id: data.id,
|
||||||
});
|
});
|
||||||
expect(res.data.users_id).toBe(data.id);
|
expect(res.data.users_id).toBe(data.id);
|
||||||
|
|
||||||
// setting "null" should be allowed
|
// setting "null" should be allowed
|
||||||
const res2 = await em.mutator(posts).insertOne({
|
const res2 = await em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users_id: null
|
users_id: null,
|
||||||
});
|
});
|
||||||
expect(res2.data.users_id).toBe(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 { data } = await em.mutator(users).insertOne({ username: "user1" });
|
||||||
const res = await em.mutator(posts).insertOne({
|
const res = await em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users: { $set: { id: data.id } }
|
users: { $set: { id: data.id } },
|
||||||
});
|
});
|
||||||
expect(res.data.users_id).toBe(data.id);
|
expect(res.data.users_id).toBe(data.id);
|
||||||
|
|
||||||
// setting "null" should be allowed
|
// setting "null" should be allowed
|
||||||
const res2 = await em.mutator(posts).insertOne({
|
const res2 = await em.mutator(posts).insertOne({
|
||||||
title: "post1",
|
title: "post1",
|
||||||
users: { $set: { id: null } }
|
users: { $set: { id: null } },
|
||||||
});
|
});
|
||||||
expect(res2.data.users_id).toBe(null);
|
expect(res2.data.users_id).toBe(null);
|
||||||
});
|
});
|
||||||
@@ -151,8 +153,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(posts).insertOne({
|
em.mutator(posts).insertOne({
|
||||||
title: "test",
|
title: "test",
|
||||||
users: { $create: { username: "test" } }
|
users: { $create: { username: "test" } },
|
||||||
})
|
}),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,27 +164,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
|
|||||||
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
|
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
|
||||||
|
|
||||||
const up1 = await em.mutator(posts).updateOne(res2.data.id, {
|
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);
|
expect(up1.data.users_id).toBe(res1.data.id);
|
||||||
|
|
||||||
const up2 = await em.mutator(posts).updateOne(res2.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);
|
expect(up2.data.users_id).toBe(res1_1.data.id);
|
||||||
|
|
||||||
const up3_1 = await em.mutator(posts).updateOne(res2.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);
|
expect(up3_1.data.users_id).toBe(res1.data.id);
|
||||||
|
|
||||||
const up3_2 = await em.mutator(posts).updateOne(res2.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);
|
expect(up3_2.data.users_id).toBe(res1_1.data.id);
|
||||||
|
|
||||||
const up4 = await em.mutator(posts).updateOne(res2.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);
|
expect(up4.data.users_id).toBe(null);
|
||||||
});
|
});
|
||||||
@@ -199,8 +201,8 @@ describe("[data] Mutator (OneToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(users).insertOne({
|
em.mutator(users).insertOne({
|
||||||
username: "test",
|
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();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,15 +212,15 @@ describe("[data] Mutator (OneToOne)", async () => {
|
|||||||
expect(
|
expect(
|
||||||
em.mutator(users).insertOne({
|
em.mutator(users).insertOne({
|
||||||
username: "test",
|
username: "test",
|
||||||
settings: { $set: { id: data.id } }
|
settings: { $set: { id: data.id } },
|
||||||
})
|
}),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("insertOne: using $create", async () => {
|
test("insertOne: using $create", async () => {
|
||||||
const res = await em.mutator(users).insertOne({
|
const res = await em.mutator(users).insertOne({
|
||||||
username: "test",
|
username: "test",
|
||||||
settings: { $create: { theme: "dark" } }
|
settings: { $create: { theme: "dark" } },
|
||||||
});
|
});
|
||||||
expect(res.data.settings_id).toBeDefined();
|
expect(res.data.settings_id).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -288,14 +290,17 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
const { data } = await mutator.insertOne({ label: "test" });
|
const { data } = await mutator.insertOne({ label: "test" });
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
||||||
|
|
||||||
await mutator.updateOne(data.id, { label: "test2" });
|
await mutator.updateOne(data.id, { label: "test2" });
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
||||||
|
|
||||||
await mutator.deleteOne(data.id);
|
await mutator.deleteOne(data.id);
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||||
});
|
});
|
||||||
@@ -303,7 +308,7 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
test("insertOne event return is respected", async () => {
|
test("insertOne event return is respected", async () => {
|
||||||
const posts = proto.entity("posts", {
|
const posts = proto.entity("posts", {
|
||||||
title: proto.text(),
|
title: proto.text(),
|
||||||
views: proto.number()
|
views: proto.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const conn = getDummyConnection();
|
const conn = getDummyConnection();
|
||||||
@@ -318,10 +323,10 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
async (event) => {
|
async (event) => {
|
||||||
return {
|
return {
|
||||||
...event.params.data,
|
...event.params.data,
|
||||||
views: 2
|
views: 2,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
const mutator = em.mutator("posts");
|
const mutator = em.mutator("posts");
|
||||||
@@ -329,14 +334,14 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
expect(result.data).toEqual({
|
expect(result.data).toEqual({
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "test",
|
title: "test",
|
||||||
views: 2
|
views: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("updateOne event return is respected", async () => {
|
test("updateOne event return is respected", async () => {
|
||||||
const posts = proto.entity("posts", {
|
const posts = proto.entity("posts", {
|
||||||
title: proto.text(),
|
title: proto.text(),
|
||||||
views: proto.number()
|
views: proto.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const conn = getDummyConnection();
|
const conn = getDummyConnection();
|
||||||
@@ -351,10 +356,10 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
async (event) => {
|
async (event) => {
|
||||||
return {
|
return {
|
||||||
...event.params.data,
|
...event.params.data,
|
||||||
views: event.params.data.views + 1
|
views: event.params.data.views + 1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
const mutator = em.mutator("posts");
|
const mutator = em.mutator("posts");
|
||||||
@@ -363,7 +368,7 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
expect(result.data).toEqual({
|
expect(result.data).toEqual({
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "test",
|
title: "test",
|
||||||
views: 3
|
views: 3,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { Perf } from "../../../src/core/utils";
|
import { TextField } from "data/fields";
|
||||||
import {
|
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
||||||
Entity,
|
import { Entity, EntityManager } from "data/entities";
|
||||||
EntityManager,
|
import { ManyToOneRelation } from "data/relations";
|
||||||
LibsqlConnection,
|
import { RepositoryEvents } from "data/events";
|
||||||
ManyToOneRelation,
|
|
||||||
RepositoryEvents,
|
|
||||||
TextField
|
|
||||||
} from "../../../src/data";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
type E = Kysely<any> | Transaction<any>;
|
type E = Kysely<any> | Transaction<any>;
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
async function sleep(ms: number) {
|
async function sleep(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -23,120 +21,6 @@ async function sleep(ms: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("[Repository]", async () => {
|
describe("[Repository]", async () => {
|
||||||
test.skip("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 () => {
|
test("count & exists", async () => {
|
||||||
const items = new Entity("items", [new TextField("label")]);
|
const items = new Entity("items", [new TextField("label")]);
|
||||||
const em = new EntityManager([items], dummyConnection);
|
const em = new EntityManager([items], dummyConnection);
|
||||||
@@ -157,25 +41,163 @@ describe("[Repository]", async () => {
|
|||||||
// count all
|
// count all
|
||||||
const res = await em.repository(items).count();
|
const res = await em.repository(items).count();
|
||||||
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
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
|
// 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.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
||||||
expect(res2.parameters).toEqual(["a", "b"]);
|
expect(res2.parameters).toEqual(["a", "b"]);
|
||||||
expect(res2.count).toBe(2);
|
expect(res2.data.count).toBe(2);
|
||||||
|
|
||||||
// check exists
|
// check exists
|
||||||
const res3 = await em.repository(items).exists({ label: "a" });
|
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" });
|
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
|
// for now, allow empty filter
|
||||||
const res5 = await em.repository(items).exists({});
|
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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("auto join", async () => {
|
||||||
|
const schema = $em(
|
||||||
|
{
|
||||||
|
posts: $entity("posts", {
|
||||||
|
title: $text(),
|
||||||
|
content: $text(),
|
||||||
|
}),
|
||||||
|
comments: $entity("comments", {
|
||||||
|
content: $text(),
|
||||||
|
}),
|
||||||
|
another: $entity("another", {
|
||||||
|
title: $text(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
({ relation }, { posts, comments }) => {
|
||||||
|
relation(comments).manyToOne(posts);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const em = schema.proto.withConnection(getDummyConnection().dummyConnection);
|
||||||
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
|
await em.mutator("posts").insertOne({ title: "post1", content: "content1" });
|
||||||
|
await em
|
||||||
|
.mutator("comments")
|
||||||
|
.insertMany([{ content: "comment1", posts_id: 1 }, { content: "comment2" }] as any);
|
||||||
|
|
||||||
|
const res = await em.repo("comments").findMany({
|
||||||
|
where: {
|
||||||
|
"posts.title": "post1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.data as any).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: "comment1",
|
||||||
|
posts_id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
{
|
||||||
|
// manual join should still work
|
||||||
|
const res = await em.repo("comments").findMany({
|
||||||
|
join: ["posts"],
|
||||||
|
where: {
|
||||||
|
"posts.title": "post1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.data as any).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: "comment1",
|
||||||
|
posts_id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// inexistent should be detected and thrown
|
||||||
|
expect(
|
||||||
|
em.repo("comments").findMany({
|
||||||
|
where: {
|
||||||
|
"random.title": "post1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/Invalid where field/);
|
||||||
|
|
||||||
|
// existing alias, but not a relation should throw
|
||||||
|
expect(
|
||||||
|
em.repo("comments").findMany({
|
||||||
|
where: {
|
||||||
|
"another.title": "post1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/Invalid where field/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,7 +205,7 @@ describe("[data] Repository (Events)", async () => {
|
|||||||
const items = new Entity("items", [new TextField("label")]);
|
const items = new Entity("items", [new TextField("label")]);
|
||||||
const categories = new Entity("categories", [new TextField("label")]);
|
const categories = new Entity("categories", [new TextField("label")]);
|
||||||
const em = new EntityManager([items, categories], dummyConnection, [
|
const em = new EntityManager([items, categories], dummyConnection, [
|
||||||
new ManyToOneRelation(categories, items)
|
new ManyToOneRelation(categories, items),
|
||||||
]);
|
]);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
const events = new Map<string, any>();
|
const events = new Map<string, any>();
|
||||||
@@ -198,24 +220,36 @@ describe("[data] Repository (Events)", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("events were fired", 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.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
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.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
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.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
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.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
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
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, spyOn, test } from "bun:test";
|
||||||
import { randomString } from "../../../src/core/utils";
|
import { randomString } from "core/utils";
|
||||||
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
|
import { TextField, EntityIndex } from "data/fields";
|
||||||
|
import { SchemaManager } from "data/schema/SchemaManager";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -15,7 +17,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
const em = new EntityManager([entity], dummyConnection, [], [index]);
|
||||||
const schema = new SchemaManager(em);
|
const schema = new SchemaManager(em);
|
||||||
|
|
||||||
const introspection = schema.getIntrospectionFromEntity(em.entities[0]);
|
const introspection = schema.getIntrospectionFromEntity(em.entities[0]!);
|
||||||
expect(introspection).toEqual({
|
expect(introspection).toEqual({
|
||||||
name: "test",
|
name: "test",
|
||||||
isView: false,
|
isView: false,
|
||||||
@@ -26,7 +28,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: true,
|
isAutoIncrementing: true,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
comment: undefined
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "username",
|
name: "username",
|
||||||
@@ -34,7 +36,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: false,
|
isAutoIncrementing: false,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
comment: undefined
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "email",
|
name: "email",
|
||||||
@@ -42,7 +44,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: false,
|
isAutoIncrementing: false,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
comment: undefined
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bio",
|
name: "bio",
|
||||||
@@ -50,8 +52,8 @@ describe("SchemaManager tests", async () => {
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: false,
|
isAutoIncrementing: false,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
comment: undefined
|
comment: undefined,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
indices: [
|
indices: [
|
||||||
{
|
{
|
||||||
@@ -61,11 +63,11 @@ describe("SchemaManager tests", async () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "email",
|
name: "email",
|
||||||
order: 0
|
order: 0,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,10 +79,10 @@ describe("SchemaManager tests", async () => {
|
|||||||
new Entity(table, [
|
new Entity(table, [
|
||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email"),
|
new TextField("email"),
|
||||||
new TextField("bio")
|
new TextField("bio"),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
dummyConnection
|
dummyConnection,
|
||||||
);
|
);
|
||||||
const kysely = em.connection.kysely;
|
const kysely = em.connection.kysely;
|
||||||
|
|
||||||
@@ -101,15 +103,15 @@ describe("SchemaManager tests", async () => {
|
|||||||
name: table,
|
name: table,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
columns: { add: ["bio"], drop: [], change: [] },
|
columns: { add: ["bio"], drop: [], change: [] },
|
||||||
indices: { add: [], drop: [index] }
|
indices: { add: [], drop: [index] },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// now sync
|
// now sync
|
||||||
await schema.sync({ force: true, drop: true });
|
await schema.sync({ force: true, drop: true });
|
||||||
const diffAfter = await schema.getDiff();
|
const diffAfter = await schema.getDiff();
|
||||||
|
|
||||||
console.log("diffAfter", diffAfter);
|
//console.log("diffAfter", diffAfter);
|
||||||
expect(diffAfter.length).toBe(0);
|
expect(diffAfter.length).toBe(0);
|
||||||
|
|
||||||
await kysely.schema.dropTable(table).execute();
|
await kysely.schema.dropTable(table).execute();
|
||||||
@@ -119,7 +121,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
const table = "drop_column";
|
const table = "drop_column";
|
||||||
const em = new EntityManager(
|
const em = new EntityManager(
|
||||||
[new Entity(table, [new TextField("username")])],
|
[new Entity(table, [new TextField("username")])],
|
||||||
dummyConnection
|
dummyConnection,
|
||||||
);
|
);
|
||||||
const kysely = em.connection.kysely;
|
const kysely = em.connection.kysely;
|
||||||
|
|
||||||
@@ -141,10 +143,10 @@ describe("SchemaManager tests", async () => {
|
|||||||
columns: {
|
columns: {
|
||||||
add: [],
|
add: [],
|
||||||
drop: ["email"],
|
drop: ["email"],
|
||||||
change: []
|
change: [],
|
||||||
},
|
},
|
||||||
indices: { add: [], drop: [] }
|
indices: { add: [], drop: [] },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// now sync
|
// now sync
|
||||||
@@ -165,15 +167,15 @@ describe("SchemaManager tests", async () => {
|
|||||||
new Entity(usersTable, [
|
new Entity(usersTable, [
|
||||||
new TextField("username"),
|
new TextField("username"),
|
||||||
new TextField("email"),
|
new TextField("email"),
|
||||||
new TextField("bio")
|
new TextField("bio"),
|
||||||
]),
|
]),
|
||||||
new Entity(postsTable, [
|
new Entity(postsTable, [
|
||||||
new TextField("title"),
|
new TextField("title"),
|
||||||
new TextField("content"),
|
new TextField("content"),
|
||||||
new TextField("created_at")
|
new TextField("created_at"),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
dummyConnection
|
dummyConnection,
|
||||||
);
|
);
|
||||||
const kysely = em.connection.kysely;
|
const kysely = em.connection.kysely;
|
||||||
|
|
||||||
@@ -192,7 +194,7 @@ describe("SchemaManager tests", async () => {
|
|||||||
name: usersTable,
|
name: usersTable,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
columns: { add: ["bio"], drop: [], change: [] },
|
columns: { add: ["bio"], drop: [], change: [] },
|
||||||
indices: { add: [], drop: [] }
|
indices: { add: [], drop: [] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: postsTable,
|
name: postsTable,
|
||||||
@@ -200,10 +202,10 @@ describe("SchemaManager tests", async () => {
|
|||||||
columns: {
|
columns: {
|
||||||
add: ["id", "title", "content", "created_at"],
|
add: ["id", "title", "content", "created_at"],
|
||||||
drop: [],
|
drop: [],
|
||||||
change: []
|
change: [],
|
||||||
},
|
},
|
||||||
indices: { add: [], drop: [] }
|
indices: { add: [], drop: [] },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// now sync
|
// now sync
|
||||||
@@ -228,8 +230,8 @@ describe("SchemaManager tests", async () => {
|
|||||||
name: entity.name,
|
name: entity.name,
|
||||||
isNew: true,
|
isNew: true,
|
||||||
columns: { add: ["id", "email"], drop: [], change: [] },
|
columns: { add: ["id", "email"], drop: [], change: [] },
|
||||||
indices: { add: [index.name!], drop: [] }
|
indices: { add: [index.name!], drop: [] },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// sync and then check again
|
// sync and then check again
|
||||||
@@ -256,8 +258,8 @@ describe("SchemaManager tests", async () => {
|
|||||||
name: entity.name,
|
name: entity.name,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
columns: { add: [], drop: [], change: [] },
|
columns: { add: [], drop: [], change: [] },
|
||||||
indices: { add: [index.name!], drop: [] }
|
indices: { add: [index.name!], drop: [] },
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// sync and then check again
|
// sync and then check again
|
||||||
@@ -266,4 +268,39 @@ describe("SchemaManager tests", async () => {
|
|||||||
const diffAfter = await em.schema().getDiff();
|
const diffAfter = await em.schema().getDiff();
|
||||||
expect(diffAfter.length).toBe(0);
|
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 */
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { describe, test, expect } from "bun:test";
|
||||||
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
||||||
|
|
||||||
|
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]],
|
||||||
|
[{ name: { $eq: "Michael" } }, '"name" = ?', ["Michael"]],
|
||||||
|
[{ int: { $between: [1, 2] } }, '"int" between ? and ?', [1, 2]],
|
||||||
|
[{ val: { $isnull: 1 } }, '"val" is null', []],
|
||||||
|
[{ val: { $isnull: true } }, '"val" is null', []],
|
||||||
|
[{ 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"]],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [query, expectedSql, expectedParams] of tests) {
|
||||||
|
const { sql, parameters } = compile(query);
|
||||||
|
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
||||||
|
expect(parameters).toEqual(expectedParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multiple validations", () => {
|
||||||
|
const tests: [WhereQuery, string, any[]][] = [
|
||||||
|
// multiple constraints per property
|
||||||
|
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||||
|
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||||
|
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
|
||||||
|
|
||||||
|
// multiple properties
|
||||||
|
[
|
||||||
|
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||||
|
'("val1" = ? and "val2" = ?)',
|
||||||
|
["foo", "bar"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
|
||||||
|
'("val1" = ? and "val2" = ?)',
|
||||||
|
["foo", "bar"],
|
||||||
|
],
|
||||||
|
|
||||||
|
// or constructs
|
||||||
|
[
|
||||||
|
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
|
||||||
|
'("val1" = ? or "val2" = ?)',
|
||||||
|
["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]],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [query, expectedSql, expectedParams] of tests) {
|
||||||
|
const { sql, parameters } = compile(query);
|
||||||
|
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
|
||||||
|
expect(parameters).toEqual(expectedParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keys", () => {
|
||||||
|
const tests: [WhereQuery, string[]][] = [
|
||||||
|
// multiple constraints per property
|
||||||
|
[{ val: { $lt: 10, $gte: 3 } }, ["val"]],
|
||||||
|
|
||||||
|
// multiple properties
|
||||||
|
[{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, ["val1", "val2"]],
|
||||||
|
|
||||||
|
// or constructs
|
||||||
|
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
|
||||||
|
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [query, expectedKeys] of tests) {
|
||||||
|
const keys = WhereBuilder.getPropertyNames(query);
|
||||||
|
expect(keys).toEqual(expectedKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { _jsonp } from "../../../src/core/utils";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import {
|
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
||||||
Entity,
|
import { TextField } from "data/fields";
|
||||||
EntityManager,
|
import * as proto from "data/prototype";
|
||||||
ManyToManyRelation,
|
import { WithBuilder } from "data/entities/query/WithBuilder";
|
||||||
ManyToOneRelation,
|
import { schemaToEm } from "../../helper";
|
||||||
PolymorphicRelation,
|
|
||||||
TextField,
|
|
||||||
WithBuilder
|
|
||||||
} from "../../../src/data";
|
|
||||||
import * as proto from "../../../src/data/prototype";
|
|
||||||
import { compileQb, prettyPrintQb, schemaToEm } from "../../helper";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
@@ -21,32 +19,32 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
posts: proto.entity("posts", {}),
|
posts: proto.entity("posts", {}),
|
||||||
users: proto.entity("users", {}),
|
users: proto.entity("users", {}),
|
||||||
media: proto.entity("media", {})
|
media: proto.entity("media", {}),
|
||||||
},
|
},
|
||||||
({ relation }, { posts, users, media }) => {
|
({ relation }, { posts, users, media }) => {
|
||||||
relation(posts).manyToOne(users);
|
relation(posts).manyToOne(users);
|
||||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
|
|
||||||
expect(WithBuilder.validateWiths(em, "posts", undefined)).toBe(0);
|
expect(WithBuilder.validateWiths(em, "posts", undefined as any)).toBe(0);
|
||||||
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
|
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
|
||||||
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
WithBuilder.validateWiths(em, "posts", {
|
WithBuilder.validateWiths(em, "posts", {
|
||||||
users: {
|
users: {
|
||||||
with: { avatar: {} }
|
with: { avatar: {} },
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
).toBe(2);
|
).toBe(2);
|
||||||
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
|
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
|
||||||
expect(() =>
|
expect(() =>
|
||||||
WithBuilder.validateWiths(em, "posts", {
|
WithBuilder.validateWiths(em, "posts", {
|
||||||
users: {
|
users: {
|
||||||
with: { glibberish: {} }
|
with: { glibberish: {} },
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,8 +54,8 @@ describe("[data] WithBuilder", async () => {
|
|||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||||
posts: {}
|
posts: {},
|
||||||
})
|
}),
|
||||||
).toThrow('Relation "users<>posts" not found');
|
).toThrow('Relation "users<>posts" not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,13 +66,13 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const em = new EntityManager([users, posts], dummyConnection, relations);
|
const em = new EntityManager([users, posts], dummyConnection, relations);
|
||||||
|
|
||||||
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
|
||||||
posts: {}
|
posts: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
|
|
||||||
expect(res.sql).toBe(
|
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" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) 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([10, 0]);
|
expect(res.parameters).toEqual([10, 0]);
|
||||||
|
|
||||||
@@ -83,16 +81,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
em.connection.kysely.selectFrom("posts"),
|
em.connection.kysely.selectFrom("posts"),
|
||||||
posts, // @todo: try with "users", it gives output!
|
posts, // @todo: try with "users", it gives output!
|
||||||
{
|
{
|
||||||
author: {}
|
author: {},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
|
|
||||||
expect(res2.sql).toBe(
|
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" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"'
|
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "author"."id" asc limit ?) as obj) as "author" from "posts"',
|
||||||
);
|
);
|
||||||
expect(res2.parameters).toEqual([1, 0]);
|
expect(res2.parameters).toEqual([1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("test with empty join", async () => {
|
test("test with empty join", async () => {
|
||||||
@@ -124,7 +122,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
.values([
|
.values([
|
||||||
{ posts_id: 1, categories_id: 1 },
|
{ posts_id: 1, categories_id: 1 },
|
||||||
{ posts_id: 2, categories_id: 2 },
|
{ posts_id: 2, categories_id: 2 },
|
||||||
{ posts_id: 1, categories_id: 2 }
|
{ posts_id: 1, categories_id: 2 },
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
@@ -138,14 +136,14 @@ describe("[data] WithBuilder", async () => {
|
|||||||
title: "fashion post",
|
title: "fashion post",
|
||||||
categories: [
|
categories: [
|
||||||
{ id: 1, label: "fashion" },
|
{ id: 1, label: "fashion" },
|
||||||
{ id: 2, label: "beauty" }
|
{ id: 2, label: "beauty" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "beauty post",
|
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: {} } });
|
||||||
@@ -156,21 +154,21 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
label: "fashion",
|
label: "fashion",
|
||||||
posts: [{ id: 1, title: "fashion post" }]
|
posts: [{ id: 1, title: "fashion post" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
label: "beauty",
|
label: "beauty",
|
||||||
posts: [
|
posts: [
|
||||||
{ id: 1, title: "fashion post" },
|
{ id: 1, title: "fashion post" },
|
||||||
{ id: 2, title: "beauty post" }
|
{ id: 2, title: "beauty post" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
label: "tech",
|
label: "tech",
|
||||||
posts: []
|
posts: [],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,7 +179,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
const entities = [media, categories];
|
const entities = [media, categories];
|
||||||
const single = new PolymorphicRelation(categories, media, {
|
const single = new PolymorphicRelation(categories, media, {
|
||||||
mappedBy: "single",
|
mappedBy: "single",
|
||||||
targetCardinality: 1
|
targetCardinality: 1,
|
||||||
});
|
});
|
||||||
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
|
||||||
|
|
||||||
@@ -191,24 +189,20 @@ describe("[data] WithBuilder", async () => {
|
|||||||
em,
|
em,
|
||||||
em.connection.kysely.selectFrom("categories"),
|
em.connection.kysely.selectFrom("categories"),
|
||||||
categories,
|
categories,
|
||||||
{ single: {} }
|
{ single: {} },
|
||||||
);
|
);
|
||||||
const res = qb.compile();
|
const res = qb.compile();
|
||||||
expect(res.sql).toBe(
|
expect(res.sql).toMatchSnapshot();
|
||||||
'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 ? offset ?) as obj) as "single" from "categories"'
|
expect(res.parameters).toEqual(["categories.single", 1]);
|
||||||
);
|
|
||||||
expect(res.parameters).toEqual(["categories.single", 1, 0]);
|
|
||||||
|
|
||||||
const qb2 = WithBuilder.addClause(
|
const qb2 = WithBuilder.addClause(
|
||||||
em,
|
em,
|
||||||
em.connection.kysely.selectFrom("categories"),
|
em.connection.kysely.selectFrom("categories"),
|
||||||
categories,
|
categories,
|
||||||
{ multiple: {} }
|
{ multiple: {} },
|
||||||
);
|
);
|
||||||
const res2 = qb2.compile();
|
const res2 = qb2.compile();
|
||||||
expect(res2.sql).toBe(
|
expect(res2.sql).toMatchSnapshot();
|
||||||
'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", 10, 0]);
|
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,16 +234,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
posts: proto.entity("posts", {}),
|
posts: proto.entity("posts", {}),
|
||||||
users: proto.entity("users", {
|
users: proto.entity("users", {
|
||||||
username: proto.text()
|
username: proto.text(),
|
||||||
}),
|
}),
|
||||||
media: proto.entity("media", {
|
media: proto.entity("media", {
|
||||||
path: proto.text()
|
path: proto.text(),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
({ relation }, { posts, users, media }) => {
|
({ relation }, { posts, users, media }) => {
|
||||||
relation(posts).manyToOne(users);
|
relation(posts).manyToOne(users);
|
||||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
|
|
||||||
@@ -265,18 +259,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
with: {
|
with: {
|
||||||
avatar: {
|
avatar: {
|
||||||
select: ["id", "path"],
|
select: ["id", "path"],
|
||||||
limit: 2 // ignored
|
limit: 2, // ignored
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
//prettyPrintQb(qb);
|
//prettyPrintQb(qb);
|
||||||
expect(qb.compile().sql).toBe(
|
expect(qb.compile().sql).toMatchSnapshot();
|
||||||
'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 ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"'
|
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
|
||||||
);
|
|
||||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("compiles with many", async () => {
|
test("compiles with many", async () => {
|
||||||
@@ -285,17 +277,17 @@ describe("[data] WithBuilder", async () => {
|
|||||||
posts: proto.entity("posts", {}),
|
posts: proto.entity("posts", {}),
|
||||||
comments: proto.entity("comments", {}),
|
comments: proto.entity("comments", {}),
|
||||||
users: proto.entity("users", {
|
users: proto.entity("users", {
|
||||||
username: proto.text()
|
username: proto.text(),
|
||||||
}),
|
}),
|
||||||
media: proto.entity("media", {
|
media: proto.entity("media", {
|
||||||
path: proto.text()
|
path: proto.text(),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
({ relation }, { posts, comments, users, media }) => {
|
({ relation }, { posts, comments, users, media }) => {
|
||||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||||
relation(comments).manyToOne(posts).manyToOne(users);
|
relation(comments).manyToOne(posts).manyToOne(users);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
|
|
||||||
@@ -308,40 +300,38 @@ describe("[data] WithBuilder", async () => {
|
|||||||
limit: 12,
|
limit: 12,
|
||||||
with: {
|
with: {
|
||||||
users: {
|
users: {
|
||||||
select: ["username"]
|
select: ["username"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(qb.compile().sql).toBe(
|
expect(qb.compile().sql).toMatchSnapshot();
|
||||||
'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 ? offset ?) 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]);
|
||||||
);
|
|
||||||
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns correct result", async () => {
|
test("returns correct result", async () => {
|
||||||
const schema = proto.em(
|
const schema = proto.em(
|
||||||
{
|
{
|
||||||
posts: proto.entity("posts", {
|
posts: proto.entity("posts", {
|
||||||
title: proto.text()
|
title: proto.text(),
|
||||||
}),
|
}),
|
||||||
comments: proto.entity("comments", {
|
comments: proto.entity("comments", {
|
||||||
content: proto.text()
|
content: proto.text(),
|
||||||
}),
|
}),
|
||||||
users: proto.entity("users", {
|
users: proto.entity("users", {
|
||||||
username: proto.text()
|
username: proto.text(),
|
||||||
}),
|
}),
|
||||||
media: proto.entity("media", {
|
media: proto.entity("media", {
|
||||||
path: proto.text()
|
path: proto.text(),
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
({ relation }, { posts, comments, users, media }) => {
|
({ relation }, { posts, comments, users, media }) => {
|
||||||
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
|
||||||
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
relation(users).polyToOne(media, { mappedBy: "avatar" });
|
||||||
relation(comments).manyToOne(posts).manyToOne(users);
|
relation(comments).manyToOne(posts).manyToOne(users);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
await em.schema().sync({ force: true });
|
await em.schema().sync({ force: true });
|
||||||
@@ -351,7 +341,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
await em.mutator("posts").insertMany([
|
await em.mutator("posts").insertMany([
|
||||||
{ title: "post1", users_id: 1 },
|
{ title: "post1", users_id: 1 },
|
||||||
{ title: "post2", users_id: 1 },
|
{ title: "post2", users_id: 1 },
|
||||||
{ title: "post3", users_id: 2 }
|
{ title: "post3", users_id: 2 },
|
||||||
]);
|
]);
|
||||||
await em.mutator("comments").insertMany([
|
await em.mutator("comments").insertMany([
|
||||||
{ content: "comment1", posts_id: 1, users_id: 1 },
|
{ content: "comment1", posts_id: 1, users_id: 1 },
|
||||||
@@ -360,7 +350,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{ content: "comment3", posts_id: 2, users_id: 1 },
|
{ content: "comment3", posts_id: 2, users_id: 1 },
|
||||||
{ content: "comment4", posts_id: 2, users_id: 2 },
|
{ content: "comment4", posts_id: 2, users_id: 2 },
|
||||||
{ content: "comment5", posts_id: 3, users_id: 1 },
|
{ content: "comment5", posts_id: 3, users_id: 1 },
|
||||||
{ content: "comment6", posts_id: 3, users_id: 2 }
|
{ content: "comment6", posts_id: 3, users_id: 2 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = await em.repo("posts").findMany({
|
const result = await em.repo("posts").findMany({
|
||||||
@@ -371,11 +361,11 @@ describe("[data] WithBuilder", async () => {
|
|||||||
select: ["content"],
|
select: ["content"],
|
||||||
with: {
|
with: {
|
||||||
users: {
|
users: {
|
||||||
select: ["username"]
|
select: ["username"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.data).toEqual([
|
expect(result.data).toEqual([
|
||||||
@@ -385,16 +375,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
content: "comment1",
|
content: "comment1",
|
||||||
users: {
|
users: {
|
||||||
username: "user1"
|
username: "user1",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "comment1-1",
|
content: "comment1-1",
|
||||||
users: {
|
users: {
|
||||||
username: "user1"
|
username: "user1",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "post2",
|
title: "post2",
|
||||||
@@ -402,16 +392,16 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
content: "comment3",
|
content: "comment3",
|
||||||
users: {
|
users: {
|
||||||
username: "user1"
|
username: "user1",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "comment4",
|
content: "comment4",
|
||||||
users: {
|
users: {
|
||||||
username: "user2"
|
username: "user2",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "post3",
|
title: "post3",
|
||||||
@@ -419,17 +409,17 @@ describe("[data] WithBuilder", async () => {
|
|||||||
{
|
{
|
||||||
content: "comment5",
|
content: "comment5",
|
||||||
users: {
|
users: {
|
||||||
username: "user1"
|
username: "user1",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "comment6",
|
content: "comment6",
|
||||||
users: {
|
users: {
|
||||||
username: "user2"
|
username: "user2",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
//console.log(_jsonp(result.data));
|
//console.log(_jsonp(result.data));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||||
|
|
||||||
|
exports[`[data] WithBuilder recursive compiles with singles 1`] = `"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 "avatar"."id" as "id", "avatar"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "avatar"."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""`;
|
||||||
|
|
||||||
|
exports[`[data] WithBuilder recursive compiles with many 1`] = `"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""`;
|
||||||
|
|
||||||
|
exports[`[data] WithBuilder polymorphic 1`] = `"select (select json_object('id', "obj"."id", 'path', "obj"."path") from (select "single"."id" as "id", "single"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "single"."id" asc limit ?) as obj) as "single" from "categories""`;
|
||||||
|
|
||||||
|
exports[`[data] WithBuilder polymorphic 2`] = `"select (select coalesce(json_group_array(json_object('id', "agg"."id", 'path', "agg"."path")), '[]') from (select "multiple"."id" as "id", "multiple"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "multiple"."id" asc limit ? offset ?) as agg) as "multiple" from "categories""`;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { EntityManager } from "../../../../src/data";
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -22,10 +22,10 @@ describe("Connection", async () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
order: 0
|
order: 0,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,14 +54,14 @@ describe("Connection", async () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
order: 0
|
order: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "desc",
|
name: "desc",
|
||||||
order: 1
|
order: 1,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,10 +83,10 @@ describe("Connection", async () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: "name",
|
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: true,
|
||||||
|
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: true,
|
||||||
|
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 { describe, expect, test } from "bun:test";
|
||||||
import { BooleanField } from "../../../../src/data";
|
import { BooleanField } from "data/fields";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] BooleanField", async () => {
|
describe("[data] BooleanField", async () => {
|
||||||
runBaseFieldTests(BooleanField, { defaultValue: true, schemaType: "boolean" });
|
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new BooleanField("test");
|
const field = new BooleanField("test");
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, test } from "bun:test";
|
||||||
import { DateField } from "../../../../src/data";
|
import { DateField } from "data/fields";
|
||||||
import { runBaseFieldTests } from "./inc";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
describe("[data] DateField", async () => {
|
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
|
// @todo: add datefield tests
|
||||||
test("week", async () => {
|
test("week", async () => {
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { EnumField } from "../../../../src/data";
|
import { EnumField } from "data/fields";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
function options(strings: string[]) {
|
function options(strings: string[]) {
|
||||||
return { type: "strings", values: strings };
|
return { type: "strings", values: strings };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("[data] EnumField", async () => {
|
describe("[data] EnumField", async () => {
|
||||||
runBaseFieldTests(
|
fieldTestSuite(
|
||||||
|
bunTestRunner,
|
||||||
|
// @ts-ignore
|
||||||
EnumField,
|
EnumField,
|
||||||
{ defaultValue: "a", schemaType: "text" },
|
{ defaultValue: "a", schemaType: "text" },
|
||||||
{ options: options(["a", "b", "c"]) }
|
{ options: options(["a", "b", "c"]) },
|
||||||
);
|
);
|
||||||
|
|
||||||
test("yields if default value is not a valid option", async () => {
|
test("yields if default value is not a valid option", async () => {
|
||||||
expect(
|
expect(
|
||||||
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
|
// @ts-ignore
|
||||||
|
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
|
// @ts-ignore
|
||||||
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
const field = new EnumField("test", { options: options(["a", "b", "c"]) });
|
||||||
|
|
||||||
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
expect(transformPersist(field, null)).resolves.toBeUndefined();
|
||||||
@@ -29,9 +34,10 @@ describe("[data] EnumField", async () => {
|
|||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new EnumField("test", {
|
const field = new EnumField("test", {
|
||||||
|
// @ts-ignore
|
||||||
options: options(["a", "b", "c"]),
|
options: options(["a", "b", "c"]),
|
||||||
default_value: "a",
|
default_value: "a",
|
||||||
required: true
|
required: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(field.transformRetrieve(null)).toBe("a");
|
expect(field.transformRetrieve(null)).toBe("a");
|
||||||
|
|||||||
@@ -1,43 +1,50 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Default, parse, stripMark } from "../../../../src/core/utils";
|
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||||
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
import { stripMark } from "core/utils/schema";
|
||||||
|
|
||||||
describe("[data] Field", async () => {
|
describe("[data] Field", async () => {
|
||||||
class FieldSpec extends Field {
|
class FieldSpec extends Field {
|
||||||
schema(): SchemaResponse {
|
|
||||||
return this.useSchemaHelper("text");
|
|
||||||
}
|
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return baseFieldConfigSchema;
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldTestSuite(bunTestRunner, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||||
|
|
||||||
test("default config", async () => {
|
test("default config", async () => {
|
||||||
const config = Default(baseFieldConfigSchema, {});
|
const config = baseFieldConfigSchema.template({});
|
||||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
|
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (specific)", async () => {
|
test("transformPersist (specific)", async () => {
|
||||||
const required = new FieldSpec("test", { required: true });
|
const required = new FieldSpec("test", { required: true });
|
||||||
const requiredDefault = new FieldSpec("test", {
|
const requiredDefault = new FieldSpec("test", {
|
||||||
required: true,
|
required: true,
|
||||||
default_value: "test"
|
default_value: "test",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
|
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
|
||||||
expect(
|
expect(
|
||||||
required.transformPersist(undefined, undefined as any, undefined as any)
|
required.transformPersist(undefined, undefined as any, undefined as any),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
|
|
||||||
// works because it has a default value
|
// works because it has a default value
|
||||||
expect(
|
expect(
|
||||||
requiredDefault.transformPersist(null, undefined as any, undefined as any)
|
requiredDefault.transformPersist(null, undefined as any, undefined as any),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
expect(
|
expect(
|
||||||
requiredDefault.transformPersist(undefined, undefined as any, undefined as any)
|
requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
|
||||||
).resolves.toBeDefined();
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Type } from "../../../../src/core/utils";
|
import { Entity } from "data/entities";
|
||||||
import {
|
import { Field, EntityIndex } from "data/fields";
|
||||||
Entity,
|
import { s } from "core/utils/schema";
|
||||||
EntityIndex,
|
|
||||||
type EntityManager,
|
|
||||||
Field,
|
|
||||||
type SchemaResponse
|
|
||||||
} from "../../../../src/data";
|
|
||||||
|
|
||||||
class TestField extends Field {
|
class TestField extends Field {
|
||||||
protected getSchema(): any {
|
protected getSchema(): any {
|
||||||
return Type.Any();
|
return s.any();
|
||||||
}
|
}
|
||||||
|
|
||||||
schema(em: EntityManager<any>): SchemaResponse {
|
override schema() {
|
||||||
return undefined as any;
|
return undefined as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,11 +23,4 @@ describe("FieldIndex", async () => {
|
|||||||
expect(index.name).toEqual("idx_test_name");
|
expect(index.name).toEqual("idx_test_name");
|
||||||
expect(index.unique).toEqual(false);
|
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 { describe, expect, test } from "bun:test";
|
||||||
import { JsonField } from "../../../../src/data";
|
import { JsonField } from "data/fields";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonField", async () => {
|
describe("[data] JsonField", async () => {
|
||||||
const field = new JsonField("test");
|
const field = new JsonField("test");
|
||||||
runBaseFieldTests(JsonField, {
|
fieldTestSuite(bunTestRunner, JsonField, {
|
||||||
defaultValue: { a: 1 },
|
defaultValue: { a: 1 },
|
||||||
sampleValues: ["string", { test: 1 }, 1],
|
//sampleValues: ["string", { test: 1 }, 1],
|
||||||
schemaType: "text"
|
schemaType: "text",
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transformPersist (no config)", async () => {
|
test("transformPersist (no config)", async () => {
|
||||||
@@ -32,9 +33,9 @@ describe("[data] JsonField", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("getValue", async () => {
|
test("getValue", async () => {
|
||||||
expect(field.getValue({ test: 1 }, "form")).toBe('{\n "test": 1\n}');
|
expect(field.getValue({ test: 1 }, "form")).toEqual({ test: 1 });
|
||||||
expect(field.getValue("string", "form")).toBe('"string"');
|
expect(field.getValue("string", "form")).toBe("string");
|
||||||
expect(field.getValue(1, "form")).toBe("1");
|
expect(field.getValue(1, "form")).toBe(1);
|
||||||
|
|
||||||
expect(field.getValue('{"test":1}', "submit")).toEqual({ test: 1 });
|
expect(field.getValue('{"test":1}', "submit")).toEqual({ test: 1 });
|
||||||
expect(field.getValue('"string"', "submit")).toBe("string");
|
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({ test: 1 }, "table")).toBe('{"test":1}');
|
||||||
expect(field.getValue("string", "table")).toBe('"string"');
|
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 { describe, expect, test } from "bun:test";
|
||||||
import { JsonSchemaField } from "../../../../src/data";
|
import { JsonSchemaField } from "data/fields";
|
||||||
import { runBaseFieldTests } from "./inc";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonSchemaField", async () => {
|
describe("[data] JsonSchemaField", async () => {
|
||||||
runBaseFieldTests(JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
// @ts-ignore
|
||||||
|
fieldTestSuite({ expect, test }, JsonSchemaField, { defaultValue: {}, schemaType: "text" });
|
||||||
|
|
||||||
// @todo: add JsonSchemaField tests
|
// @todo: add JsonSchemaField tests
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { NumberField } from "../../../../src/data";
|
import { NumberField } from "data/fields";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] NumberField", async () => {
|
describe("[data] NumberField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
@@ -15,5 +16,5 @@ describe("[data] NumberField", async () => {
|
|||||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
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 { describe, expect, test } from "bun:test";
|
||||||
import { PrimaryField } from "../../../../src/data";
|
import { PrimaryField } from "data/fields";
|
||||||
|
|
||||||
describe("[data] PrimaryField", async () => {
|
describe("[data] PrimaryField", async () => {
|
||||||
const field = new PrimaryField("primary");
|
const field = new PrimaryField("primary");
|
||||||
@@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => {
|
|||||||
|
|
||||||
test("schema", () => {
|
test("schema", () => {
|
||||||
expect(field.name).toBe("primary");
|
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 () => {
|
test("hasDefault", async () => {
|
||||||
@@ -34,4 +39,28 @@ describe("[data] PrimaryField", async () => {
|
|||||||
expect(field.transformPersist(1)).rejects.toThrow();
|
expect(field.transformPersist(1)).rejects.toThrow();
|
||||||
expect(field.transformRetrieve(1)).toBe(1);
|
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 { describe, expect, test } from "bun:test";
|
||||||
import { TextField } from "../../../../src/data";
|
import { TextField } from "data/fields";
|
||||||
import { runBaseFieldTests, transformPersist } from "./inc";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
describe("[data] TextField", async () => {
|
describe("[data] TextField", async () => {
|
||||||
test("transformPersist (config)", async () => {
|
test("transformPersist (config)", async () => {
|
||||||
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
|
|||||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
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 { describe, expect, it, test } from "bun:test";
|
||||||
import { Entity, type EntityManager } from "../../../../src/data";
|
import { Entity, type EntityManager } from "data/entities";
|
||||||
import {
|
import {
|
||||||
type BaseRelationConfig,
|
type BaseRelationConfig,
|
||||||
EntityRelation,
|
EntityRelation,
|
||||||
EntityRelationAnchor,
|
EntityRelationAnchor,
|
||||||
RelationTypes
|
ManyToManyRelation,
|
||||||
} from "../../../../src/data/relations";
|
RelationTypes,
|
||||||
|
} from "data/relations";
|
||||||
|
import * as proto from "data/prototype";
|
||||||
|
|
||||||
class TestEntityRelation extends EntityRelation {
|
class TestEntityRelation extends EntityRelation {
|
||||||
constructor(config?: BaseRelationConfig) {
|
constructor(config?: BaseRelationConfig) {
|
||||||
super(
|
super(
|
||||||
new EntityRelationAnchor(new Entity("source"), "source"),
|
new EntityRelationAnchor(new Entity("source"), "source"),
|
||||||
new EntityRelationAnchor(new Entity("target"), "target"),
|
new EntityRelationAnchor(new Entity("target"), "target"),
|
||||||
config
|
config,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
initialize(em: EntityManager<any>) {}
|
initialize(em: EntityManager<any>) {}
|
||||||
@@ -24,11 +26,11 @@ class TestEntityRelation extends EntityRelation {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildWith(a: any, b: any, c: any): any {
|
buildWith(): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildJoin(a: any, b: any): any {
|
buildJoin(): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,4 +77,15 @@ describe("[data] EntityRelation", async () => {
|
|||||||
const relation2 = new TestEntityRelation({ required: true });
|
const relation2 = new TestEntityRelation({ required: true });
|
||||||
expect(relation2.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",
|
method: init?.method ?? "GET",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
headers: Object.fromEntries(init?.headers?.entries() ?? []),
|
headers: Object.fromEntries(init?.headers?.entries() ?? []),
|
||||||
body: init?.body
|
body: init?.body,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Response(JSON.stringify({ todos: [1, 2], request }), {
|
return new Response(JSON.stringify({ todos: [1, 2], request }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
afterAll(unmockFetch);
|
afterAll(unmockFetch);
|
||||||
|
|
||||||
describe("FetchTask", async () => {
|
describe.skip("FetchTask", async () => {
|
||||||
test("Simple fetch", async () => {
|
test("Simple fetch", async () => {
|
||||||
const task = new FetchTask("Fetch Something", {
|
const task = new FetchTask("Fetch Something", {
|
||||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: [{ key: "Content-Type", value: "application/json" }]
|
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await task.run();
|
const result = await task.run();
|
||||||
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
// // @ts-expect-error
|
// // @ts-expect-error
|
||||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 })
|
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
new FetchTask("", {
|
new FetchTask("", {
|
||||||
url: "https://jsonplaceholder.typicode.com",
|
url: "https://jsonplaceholder.typicode.com",
|
||||||
method: "invalid"
|
method: "invalid",
|
||||||
}).execute()
|
}).execute(),
|
||||||
).rejects.toThrow(/^Invalid method/);
|
).rejects.toThrow(/^Invalid method/);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" })
|
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
|
||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
|
|
||||||
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
|
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
|
||||||
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
|
|||||||
method: "{{ flow.output.method }}",
|
method: "{{ flow.output.method }}",
|
||||||
headers: [
|
headers: [
|
||||||
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
|
{ 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({
|
body: JSON.stringify({
|
||||||
email: "{{ flow.output.email }}"
|
email: "{{ flow.output.email }}",
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
const inputs = {
|
const inputs = {
|
||||||
headerKey: "Type",
|
headerKey: "Type",
|
||||||
apiKey: 123,
|
apiKey: 123,
|
||||||
email: "what@else.com",
|
email: "what@else.com",
|
||||||
method: "PATCH"
|
method: "PATCH",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flow = new Flow("", [task]);
|
const flow = new Flow("", [task]);
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
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 () => {
|
test("Simple Subflow", async () => {
|
||||||
const subTask = new RenderTask("render", {
|
const subTask = new RenderTask("render", {
|
||||||
render: "subflow"
|
render: "subflow",
|
||||||
});
|
});
|
||||||
const subflow = new Flow("subflow", [subTask]);
|
const subflow = new Flow("subflow", [subTask]);
|
||||||
|
|
||||||
const task = new LogTask("log");
|
const task = new LogTask("log");
|
||||||
const task2 = new SubFlowTask("sub", {
|
const task2 = new SubFlowTask("sub", {
|
||||||
flow: subflow
|
flow: subflow,
|
||||||
});
|
});
|
||||||
const task3 = new RenderTask("render2", {
|
const task3 = new RenderTask("render2", {
|
||||||
render: "Subflow output: {{ sub.output }}"
|
render: "Subflow output: {{ sub.output }}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2, task3], []);
|
const flow = new Flow("test", [task, task2, task3], []);
|
||||||
@@ -22,15 +38,13 @@ describe("SubFlowTask", async () => {
|
|||||||
|
|
||||||
const execution = flow.createExecution();
|
const execution = flow.createExecution();
|
||||||
await execution.start();
|
await execution.start();
|
||||||
/*console.log(execution.logs);
|
|
||||||
console.log(execution.getResponse());*/
|
|
||||||
|
|
||||||
expect(execution.getResponse()).toEqual("Subflow output: subflow");
|
expect(execution.getResponse()).toEqual("Subflow output: subflow");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Simple loop", async () => {
|
test("Simple loop", async () => {
|
||||||
const subTask = new RenderTask("render", {
|
const subTask = new RenderTask("render", {
|
||||||
render: "run {{ flow.output }}"
|
render: "run {{ flow.output }}",
|
||||||
});
|
});
|
||||||
const subflow = new Flow("subflow", [subTask]);
|
const subflow = new Flow("subflow", [subTask]);
|
||||||
|
|
||||||
@@ -38,10 +52,10 @@ describe("SubFlowTask", async () => {
|
|||||||
const task2 = new SubFlowTask("sub", {
|
const task2 = new SubFlowTask("sub", {
|
||||||
flow: subflow,
|
flow: subflow,
|
||||||
loop: true,
|
loop: true,
|
||||||
input: [1, 2, 3]
|
input: [1, 2, 3],
|
||||||
});
|
});
|
||||||
const task3 = new RenderTask("render2", {
|
const task3 = new StringifyTask("stringify", {
|
||||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
input: "{{ sub.output }}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2, task3], []);
|
const flow = new Flow("test", [task, task2, task3], []);
|
||||||
@@ -51,41 +65,6 @@ describe("SubFlowTask", async () => {
|
|||||||
const execution = flow.createExecution();
|
const execution = flow.createExecution();
|
||||||
await execution.start();
|
await execution.start();
|
||||||
|
|
||||||
console.log("errors", execution.getErrors());
|
expect(execution.getResponse()).toEqual('"run 1,run 2,run 3"');
|
||||||
|
|
||||||
/*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");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Type } from "../../src/core/utils";
|
|
||||||
import { Task } from "../../src/flows";
|
import { Task } from "../../src/flows";
|
||||||
import { dynamic } from "../../src/flows/tasks/Task";
|
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 () => {
|
test("resolveParams: template with parse", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: dynamic(Type.Number()) }),
|
s.object({ test: dynamic(s.number()) }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}"
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
some: {
|
some: {
|
||||||
path: 1
|
path: 1,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toBe(1);
|
expect(result.test).toBe(1);
|
||||||
@@ -22,15 +22,15 @@ describe("Task", async () => {
|
|||||||
|
|
||||||
test("resolveParams: with string", async () => {
|
test("resolveParams: with string", async () => {
|
||||||
const result = await Task.resolveParams(
|
const result = await Task.resolveParams(
|
||||||
Type.Object({ test: Type.String() }),
|
s.object({ test: s.string() }),
|
||||||
{
|
{
|
||||||
test: "{{ some.path }}"
|
test: "{{ some.path }}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
some: {
|
some: {
|
||||||
path: "1/1"
|
path: "1/1",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toBe("1/1");
|
expect(result.test).toBe("1/1");
|
||||||
@@ -38,75 +38,17 @@ describe("Task", async () => {
|
|||||||
|
|
||||||
test("resolveParams: with object", async () => {
|
test("resolveParams: with object", async () => {
|
||||||
const result = await Task.resolveParams(
|
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: {
|
some: {
|
||||||
path: "1/1"
|
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 }}"
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
some: {
|
|
||||||
key: "path",
|
|
||||||
value: "1/1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.test).toEqual({ 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");
|
//throw new Error("Error");
|
||||||
return {
|
return {
|
||||||
inner: {
|
inner: {
|
||||||
result: 2
|
result: 2,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
1000
|
1000,
|
||||||
);
|
);
|
||||||
const second = getNamedTask("second (if match)");
|
const second = getNamedTask("second (if match)");
|
||||||
const third = getNamedTask("third (if error)");
|
const third = getNamedTask("third (if error)");
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
|||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: any,
|
params: any,
|
||||||
private fn: () => any
|
private fn: () => any,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
super(name, params);
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: n
|
|||||||
return new ExecTask(
|
return new ExecTask(
|
||||||
name,
|
name,
|
||||||
{
|
{
|
||||||
delay
|
delay,
|
||||||
},
|
},
|
||||||
func
|
func,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const first = new LogTask("First", { delay: 1000 });
|
|||||||
const second = new LogTask("Second", { delay: 1000 });
|
const second = new LogTask("Second", { delay: 1000 });
|
||||||
const third = new LogTask("Long Third", { delay: 2500 });
|
const third = new LogTask("Long Third", { delay: 2500 });
|
||||||
const fourth = new FetchTask("Fetch Something", {
|
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
|
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Event, EventManager } from "../../src/core/events";
|
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 { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||||
import { dynamic } from "../../src/flows/tasks/Task";
|
import { dynamic } from "../../src/flows/tasks/Task";
|
||||||
|
|
||||||
@@ -14,19 +14,19 @@ class Passthrough extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutputIn = Static<typeof OutputParamTask.schema>;
|
type OutputIn = s.Static<typeof OutputParamTask.schema>;
|
||||||
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
|
||||||
|
|
||||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||||
type = "output-param";
|
type = "output-param";
|
||||||
|
|
||||||
static override schema = Type.Object({
|
static override schema = s.strictObject({
|
||||||
number: dynamic(
|
number: dynamic(
|
||||||
Type.Number({
|
s.number({
|
||||||
title: "Output number"
|
title: "Output number",
|
||||||
}),
|
}),
|
||||||
Number.parseInt
|
Number.parseInt,
|
||||||
)
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
async execute(inputs: InputsMap) {
|
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 () => {
|
test("types", async () => {
|
||||||
const schema = OutputParamTask.schema;
|
const schema = OutputParamTask.schema;
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
|
|||||||
test("output/input", async () => {
|
test("output/input", async () => {
|
||||||
const task = new OutputParamTask("task1", { number: 111 });
|
const task = new OutputParamTask("task1", { number: 111 });
|
||||||
const task2 = new OutputParamTask("task2", {
|
const task2 = new OutputParamTask("task2", {
|
||||||
number: "{{ task1.output }}"
|
number: "{{ task1.output }}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2]);
|
const flow = new Flow("test", [task, task2]);
|
||||||
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
|
|||||||
|
|
||||||
test("input from flow", async () => {
|
test("input from flow", async () => {
|
||||||
const task = new OutputParamTask("task1", {
|
const task = new OutputParamTask("task1", {
|
||||||
number: "{{flow.output.someFancyParam}}"
|
number: "{{flow.output.someFancyParam}}",
|
||||||
});
|
});
|
||||||
const task2 = new OutputParamTask("task2", {
|
const task2 = new OutputParamTask("task2", {
|
||||||
number: "{{task1.output}}"
|
number: "{{task1.output}}",
|
||||||
});
|
});
|
||||||
|
|
||||||
const flow = new Flow("test", [task, task2]);
|
const flow = new Flow("test", [task, task2]);
|
||||||
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
|
|||||||
const emgr = new EventManager({ EventTriggerClass });
|
const emgr = new EventManager({ EventTriggerClass });
|
||||||
|
|
||||||
const task = new OutputParamTask("event", {
|
const task = new OutputParamTask("event", {
|
||||||
number: "{{flow.output.number}}"
|
number: "{{flow.output.number}}",
|
||||||
});
|
});
|
||||||
const flow = new Flow(
|
const flow = new Flow(
|
||||||
"test",
|
"test",
|
||||||
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
|
|||||||
[],
|
[],
|
||||||
new EventTrigger({
|
new EventTrigger({
|
||||||
event: "test-event",
|
event: "test-event",
|
||||||
mode: "sync"
|
mode: "sync",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
flow.setRespondingTask(task);
|
flow.setRespondingTask(task);
|
||||||
flow.trigger.register(flow, emgr);
|
flow.trigger.register(flow, emgr);
|
||||||
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
|
|||||||
new HttpTrigger({
|
new HttpTrigger({
|
||||||
path: "/test",
|
path: "/test",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "sync"
|
mode: "sync",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
flow.setRespondingTask(task);
|
flow.setRespondingTask(task);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const flows = {
|
|||||||
back,
|
back,
|
||||||
fanout,
|
fanout,
|
||||||
parallel,
|
parallel,
|
||||||
simpleFetch
|
simpleFetch,
|
||||||
};
|
};
|
||||||
|
|
||||||
const arg = process.argv[2];
|
const arg = process.argv[2];
|
||||||
@@ -32,7 +32,7 @@ const colors = [
|
|||||||
"#F78F1E", // Saffron
|
"#F78F1E", // Saffron
|
||||||
"#BD10E0", // Vivid Purple
|
"#BD10E0", // Vivid Purple
|
||||||
"#50E3C2", // Turquoise
|
"#50E3C2", // Turquoise
|
||||||
"#9013FE" // Grape
|
"#9013FE", // Grape
|
||||||
];
|
];
|
||||||
|
|
||||||
const colorsCache: Record<string, string> = {};
|
const colorsCache: Record<string, string> = {};
|
||||||
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
|
|||||||
console.log("done", response ? response : "(no response)");
|
console.log("done", response ? response : "(no response)");
|
||||||
console.log(
|
console.log(
|
||||||
"Executed tasks:",
|
"Executed tasks:",
|
||||||
execution.logs.map((l) => l.task.name)
|
execution.logs.map((l) => l.task.name),
|
||||||
);
|
);
|
||||||
console.log("Executed count:", execution.logs.length);
|
console.log("Executed count:", execution.logs.length);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ExecTask extends Task {
|
|||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: any,
|
params: any,
|
||||||
private fn: () => any
|
private fn: () => any,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
super(name, params);
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ class ExecTask extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Flow trigger", async () => {
|
describe.skip("Flow trigger", async () => {
|
||||||
test("manual trigger", async () => {
|
test("manual trigger", async () => {
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
|
|||||||
"test",
|
"test",
|
||||||
[task],
|
[task],
|
||||||
[],
|
[],
|
||||||
new EventTrigger({ event: "test-event", mode: "sync" })
|
new EventTrigger({ event: "test-event", mode: "sync" }),
|
||||||
);
|
);
|
||||||
|
|
||||||
flow.trigger.register(flow, emgr);
|
flow.trigger.register(flow, emgr);
|
||||||
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
|
|||||||
new HttpTrigger({
|
new HttpTrigger({
|
||||||
path: "/test",
|
path: "/test",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "sync"
|
mode: "sync",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const hono = new Hono();
|
const hono = new Hono();
|
||||||
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
|
|||||||
|
|
||||||
test("http trigger with response", async () => {
|
test("http trigger with response", async () => {
|
||||||
const task = ExecTask.create("http", () => ({
|
const task = ExecTask.create("http", () => ({
|
||||||
called: true
|
called: true,
|
||||||
}));
|
}));
|
||||||
const flow = new Flow(
|
const flow = new Flow(
|
||||||
"test",
|
"test",
|
||||||
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
|
|||||||
new HttpTrigger({
|
new HttpTrigger({
|
||||||
path: "/test",
|
path: "/test",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "sync"
|
mode: "sync",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
flow.setRespondingTask(task);
|
flow.setRespondingTask(task);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import { type Static, Type, _jsonp, withDisabledConsole } 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";
|
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||||
|
|
||||||
/*beforeAll(disableConsoleLog);
|
/*beforeAll(disableConsoleLog);
|
||||||
@@ -10,19 +11,19 @@ afterAll(enableConsoleLog);*/
|
|||||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||||
type = "exec";
|
type = "exec";
|
||||||
|
|
||||||
static override schema = Type.Object({
|
static override schema = s.object({
|
||||||
delay: Type.Number({ default: 10 })
|
delay: s.number({ default: 10 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
params: Static<typeof ExecTask.schema>,
|
params: s.Static<typeof ExecTask.schema>,
|
||||||
private func: () => Promise<any>
|
private func: () => Promise<any>,
|
||||||
) {
|
) {
|
||||||
super(name, params);
|
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);
|
return new ExecTask(name, params, this.func);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,12 +37,12 @@ function getTask(num: number = 0, delay: number = 5) {
|
|||||||
return new ExecTask(
|
return new ExecTask(
|
||||||
`Task ${num}`,
|
`Task ${num}`,
|
||||||
{
|
{
|
||||||
delay
|
delay,
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
//console.log(`[DONE] Task: ${num}`);
|
//console.log(`[DONE] Task: ${num}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
//return new LogTask(`Log ${num}`, { delay });
|
//return new LogTask(`Log ${num}`, { delay });
|
||||||
}
|
}
|
||||||
@@ -56,9 +57,9 @@ function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number)
|
|||||||
return new ExecTask(
|
return new ExecTask(
|
||||||
name,
|
name,
|
||||||
{
|
{
|
||||||
delay: delay ?? 0
|
delay: delay ?? 0,
|
||||||
},
|
},
|
||||||
func
|
func,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
|
|||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Flow tests", async () => {
|
describe.skip("Flow tests", async () => {
|
||||||
test("Simple single task", async () => {
|
test("Simple single task", async () => {
|
||||||
const simple = getTask(0);
|
const simple = getTask(0);
|
||||||
|
|
||||||
@@ -228,7 +229,7 @@ describe("Flow tests", async () => {
|
|||||||
back
|
back
|
||||||
.task(third)
|
.task(third)
|
||||||
.getOutTasks()
|
.getOutTasks()
|
||||||
.map((t) => t.name)
|
.map((t) => t.name),
|
||||||
).toEqual(["second", "fourth"]);
|
).toEqual(["second", "fourth"]);
|
||||||
|
|
||||||
const execution = back.createExecution();
|
const execution = back.createExecution();
|
||||||
@@ -263,7 +264,7 @@ describe("Flow tests", async () => {
|
|||||||
back
|
back
|
||||||
.task(third)
|
.task(third)
|
||||||
.getOutTasks()
|
.getOutTasks()
|
||||||
.map((t) => t.name)
|
.map((t) => t.name),
|
||||||
).toEqual(["second", "fourth"]);
|
).toEqual(["second", "fourth"]);
|
||||||
|
|
||||||
const execution = back.createExecution();
|
const execution = back.createExecution();
|
||||||
@@ -324,8 +325,8 @@ describe("Flow tests", async () => {
|
|||||||
const first = getNamedTask("first", async () => {
|
const first = getNamedTask("first", async () => {
|
||||||
return {
|
return {
|
||||||
inner: {
|
inner: {
|
||||||
result: 2
|
result: 2,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const second = getNamedTask("second");
|
const second = getNamedTask("second");
|
||||||
@@ -361,7 +362,7 @@ describe("Flow tests", async () => {
|
|||||||
"[event]",
|
"[event]",
|
||||||
event.isStart() ? "start" : "end",
|
event.isStart() ? "start" : "end",
|
||||||
event.task().name,
|
event.task().name,
|
||||||
event.isStart() ? undefined : event.succeeded()
|
event.isStart() ? undefined : event.succeeded(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -389,7 +390,7 @@ describe("Flow tests", async () => {
|
|||||||
const second = new LogTask("Task 1");
|
const second = new LogTask("Task 1");
|
||||||
const third = new LogTask("Task 2", { delay: 50 });
|
const third = new LogTask("Task 2", { delay: 50 });
|
||||||
const fourth = new FetchTask("Fetch Something", {
|
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
|
const fifth = new LogTask("Task 4"); // without connection
|
||||||
|
|
||||||
@@ -405,7 +406,7 @@ describe("Flow tests", async () => {
|
|||||||
// @todo: fix
|
// @todo: fix
|
||||||
const deserialized = Flow.fromObject("", original, {
|
const deserialized = Flow.fromObject("", original, {
|
||||||
fetch: { cls: FetchTask },
|
fetch: { cls: FetchTask },
|
||||||
log: { cls: LogTask }
|
log: { cls: LogTask },
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const diffdeep = getObjectDiff(original, deserialized.toJSON());
|
const diffdeep = getObjectDiff(original, deserialized.toJSON());
|
||||||
@@ -414,7 +415,7 @@ describe("Flow tests", async () => {
|
|||||||
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
|
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
|
||||||
expect(flow.respondingTask?.name).toEqual(
|
expect(flow.respondingTask?.name).toEqual(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
deserialized.respondingTask?.name
|
deserialized.respondingTask?.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
//console.log("--- creating original sequence");
|
//console.log("--- creating original sequence");
|
||||||
|
|||||||
+52
-23
@@ -2,8 +2,12 @@ import { unlink } from "node:fs/promises";
|
|||||||
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
||||||
import Database from "libsql";
|
import Database from "libsql";
|
||||||
import { format as sqlFormat } from "sql-formatter";
|
import { format as sqlFormat } from "sql-formatter";
|
||||||
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
|
||||||
import type { em as protoEm } from "../src/data/prototype";
|
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): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
@@ -17,7 +21,7 @@ export function getDummyDatabase(memory: boolean = true): {
|
|||||||
afterAllCleanup: async () => {
|
afterAllCleanup: async () => {
|
||||||
if (!memory) await unlink(DB_NAME);
|
if (!memory) await unlink(DB_NAME);
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +31,7 @@ export function getDummyConnection(memory: boolean = true) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
dummyConnection,
|
dummyConnection,
|
||||||
afterAllCleanup
|
afterAllCleanup,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,25 +39,6 @@ export function getLocalLibsqlConnection() {
|
|||||||
return { url: "http://127.0.0.1:8080" };
|
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", "warn"]) {
|
|
||||||
severities.forEach((severity) => {
|
|
||||||
console[severity] = () => null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enableConsoleLog() {
|
|
||||||
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
|
||||||
console[severity as ConsoleSeverity] = fn;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||||
const { sql, parameters } = qb.compile();
|
const { sql, parameters } = qb.compile();
|
||||||
return { sql, parameters };
|
return { sql, parameters };
|
||||||
@@ -61,7 +46,7 @@ export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
|||||||
|
|
||||||
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||||
const { sql, parameters } = qb.compile();
|
const { sql, parameters } = qb.compile();
|
||||||
console.log("$", sqlFormat(sql), "\n[params]", parameters);
|
console.info("$", sqlFormat(sql), "\n[params]", parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
||||||
@@ -71,3 +56,47 @@ export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): En
|
|||||||
|
|
||||||
export const assetsPath = `${import.meta.dir}/_assets`;
|
export const assetsPath = `${import.meta.dir}/_assets`;
|
||||||
export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { App, createApp } from "../../src";
|
import { App, createApp, type AuthResponse } from "../../src";
|
||||||
import type { AuthResponse } from "../../src/auth";
|
import { auth } from "../../src/modules/middlewares";
|
||||||
import { auth } from "../../src/auth/middlewares";
|
import {
|
||||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
mergeObject,
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
randomString,
|
||||||
|
secureRandomString,
|
||||||
|
withDisabledConsole,
|
||||||
|
} from "../../src/core/utils";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { getDummyConnection } from "../helper";
|
||||||
|
import type { AppAuthSchema } from "auth/auth-schema";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -16,25 +22,25 @@ const roles = {
|
|||||||
"system.schema.read",
|
"system.schema.read",
|
||||||
"system.access.api",
|
"system.access.api",
|
||||||
"system.config.read",
|
"system.config.read",
|
||||||
"data.entity.read"
|
"data.entity.read",
|
||||||
],
|
],
|
||||||
is_default: true
|
is_default: true,
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
is_default: true,
|
is_default: true,
|
||||||
implicit_allow: true
|
implicit_allow: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
strict: {
|
strict: {
|
||||||
guest: {
|
guest: {
|
||||||
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
|
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
|
||||||
is_default: true
|
is_default: true,
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
is_default: true,
|
is_default: true,
|
||||||
implicit_allow: true
|
implicit_allow: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const configs = {
|
const configs = {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -42,31 +48,33 @@ const configs = {
|
|||||||
entity_name: "users",
|
entity_name: "users",
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: secureRandomString(20),
|
secret: secureRandomString(20),
|
||||||
issuer: randomString(10)
|
issuer: randomString(10),
|
||||||
},
|
},
|
||||||
roles: roles.strict,
|
roles: roles.strict,
|
||||||
guard: {
|
guard: {
|
||||||
enabled: true
|
enabled: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
normal: {
|
normal: {
|
||||||
email: "normal@bknd.io",
|
email: "normal@bknd.io",
|
||||||
password: "12345678"
|
password: "12345678",
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
email: "admin@bknd.io",
|
email: "admin@bknd.io",
|
||||||
password: "12345678",
|
password: "12345678",
|
||||||
role: "admin"
|
role: "admin",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createAuthApp() {
|
function createAuthApp(config?: Partial<AppAuthSchema>) {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
connection: dummyConnection,
|
||||||
auth: configs.auth
|
config: {
|
||||||
}
|
auth: mergeObject(configs.auth, config ?? {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
@@ -75,7 +83,7 @@ function createAuthApp() {
|
|||||||
await app.createUser(configs.users.normal);
|
await app.createUser(configs.users.normal);
|
||||||
await app.createUser(configs.users.admin);
|
await app.createUser(configs.users.admin);
|
||||||
},
|
},
|
||||||
"sync"
|
"sync",
|
||||||
);
|
);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
@@ -94,14 +102,14 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
|||||||
if (mode === "cookie") {
|
if (mode === "cookie") {
|
||||||
return {
|
return {
|
||||||
cookie: `auth=${token};`,
|
cookie: `auth=${token};`,
|
||||||
...additional
|
...additional,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Authorization: token ? `Bearer ${token}` : "",
|
Authorization: token ? `Bearer ${token}` : "",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...additional
|
...additional,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function body(obj?: Record<string, any>) {
|
function body(obj?: Record<string, any>) {
|
||||||
@@ -118,25 +126,35 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
login: async (
|
login: async (
|
||||||
user: any
|
user: any,
|
||||||
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
|
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
|
||||||
const res = (await app.server.request("/api/auth/password/login", {
|
const res = (await app.server.request("/api/auth/password/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
body: body(user)
|
body: body(user),
|
||||||
})) as Response;
|
})) as Response;
|
||||||
|
|
||||||
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
||||||
|
|
||||||
return { res, data };
|
return { res, data };
|
||||||
},
|
},
|
||||||
|
register: async (user: any): Promise<{ res: Response; data: AuthResponse }> => {
|
||||||
|
const res = (await app.server.request("/api/auth/password/register", {
|
||||||
|
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">> => {
|
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
||||||
const res = (await app.server.request("/api/auth/me", {
|
const res = (await app.server.request("/api/auth/me", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: headers(token)
|
headers: headers(token),
|
||||||
})) as Response;
|
})) as Response;
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,8 +165,8 @@ describe("integration auth", () => {
|
|||||||
|
|
||||||
const { data: users } = await app.em.repository("users").findMany();
|
const { data: users } = await app.em.repository("users").findMany();
|
||||||
expect(users.length).toBe(2);
|
expect(users.length).toBe(2);
|
||||||
expect(users[0].email).toBe(configs.users.normal.email);
|
expect(users[0]?.email).toBe(configs.users.normal.email);
|
||||||
expect(users[1].email).toBe(configs.users.admin.email);
|
expect(users[1]?.email).toBe(configs.users.admin.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log you in with API", async () => {
|
it("should log you in with API", async () => {
|
||||||
@@ -219,7 +237,7 @@ describe("integration auth", () => {
|
|||||||
|
|
||||||
app.server.get("/get", auth(), async (c) => {
|
app.server.get("/get", auth(), async (c) => {
|
||||||
return c.json({
|
return c.json({
|
||||||
user: c.get("auth").user ?? null
|
user: c.get("auth")?.user ?? null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.server.get("/wait", auth(), async (c) => {
|
app.server.get("/wait", auth(), async (c) => {
|
||||||
@@ -232,15 +250,72 @@ describe("integration auth", () => {
|
|||||||
expect(me.user.email).toBe(configs.users.normal.email);
|
expect(me.user.email).toBe(configs.users.normal.email);
|
||||||
|
|
||||||
app.server.request("/wait", {
|
app.server.request("/wait", {
|
||||||
headers: { Authorization: `Bearer ${data.token}` }
|
headers: { Authorization: `Bearer ${data.token}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
const res = await app.server.request("/get");
|
const res = await app.server.request("/get");
|
||||||
const data = await res.json();
|
const data = (await res.json()) as any;
|
||||||
expect(data.user).toBe(null);
|
expect(data.user).toBe(null);
|
||||||
expect(await $fns.me()).toEqual({ user: null as any });
|
expect(await $fns.me()).toEqual({ user: null as any });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should register users with default role", async () => {
|
||||||
|
const app = createAuthApp({ default_role_register: "guest" });
|
||||||
|
await app.build();
|
||||||
|
const $fns = fns(app);
|
||||||
|
|
||||||
|
// takes default role
|
||||||
|
expect(
|
||||||
|
await app
|
||||||
|
.createUser({
|
||||||
|
email: "test@bknd.io",
|
||||||
|
password: "12345678",
|
||||||
|
})
|
||||||
|
.then((r) => r.role),
|
||||||
|
).toBe("guest");
|
||||||
|
|
||||||
|
// throws error if role doesn't exist
|
||||||
|
expect(
|
||||||
|
app.createUser({
|
||||||
|
email: "test@bknd.io",
|
||||||
|
password: "12345678",
|
||||||
|
role: "doesnt exist",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow();
|
||||||
|
|
||||||
|
// takes role if provided
|
||||||
|
expect(
|
||||||
|
await app
|
||||||
|
.createUser({
|
||||||
|
email: "test2@bknd.io",
|
||||||
|
password: "12345678",
|
||||||
|
role: "admin",
|
||||||
|
})
|
||||||
|
.then((r) => r.role),
|
||||||
|
).toBe("admin");
|
||||||
|
|
||||||
|
// registering with role is not allowed
|
||||||
|
expect(
|
||||||
|
await $fns
|
||||||
|
.register({
|
||||||
|
email: "test3@bknd.io",
|
||||||
|
password: "12345678",
|
||||||
|
role: "admin",
|
||||||
|
})
|
||||||
|
.then((r) => r.res.ok),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
// takes default role
|
||||||
|
expect(
|
||||||
|
await $fns
|
||||||
|
.register({
|
||||||
|
email: "test3@bknd.io",
|
||||||
|
password: "12345678",
|
||||||
|
})
|
||||||
|
.then((r) => r.data.user.role),
|
||||||
|
).toBe("guest");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user