{"_id":"@synvox/core","_rev":"143-51b42c58d617f7a46ed66bc922461bfa","name":"@synvox/core","dist-tags":{"latest":"2.5.0-alpha.2","next":"2.5.0-alpha.83"},"versions":{"0.0.1":{"version":"0.0.1","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/jest":"^24.0.25","@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","debug":"~2.6.9","dotenv":"^8.2.0","express":"~4.16.1","knex":"^0.20.4","ms":"^2.1.2","pg":"^7.15.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","tsdx":"^0.12.1","tslib":"^1.10.0","typescript":"^3.7.4","test-listen":"^1.1.0"},"gitHead":"347ca0d2f5fdd38465cfad0c8e4dd33eff3a67f3","_id":"@synvox/core@0.0.1","_nodeVersion":"12.3.1","_npmVersion":"6.9.0","dist":{"integrity":"sha512-zbc4Nis99MGT1LtK4jpnSENvVHBI305b/FkwvbCQeXgVXktEDnWD8ZUHEXx1qiHO0/4UOFS17AVpgPFrAiqdqQ==","shasum":"136048688435f8f1cf2508f9157cef26e5955208","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.0.1.tgz","fileCount":14,"unpackedSize":293868,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJeI0YoCRA9TVsSAnZWagAAEEIP/jMaisfB2fsFVshmDqf/\nBF5wGZTmhiw5ggNMT/J/49zKDRYGPGyeuv39Peyn4TaVQrHeWOdN9Pw7ZBwr\n337oCr0MRAqT+Po0+xBZ2SrgQ7TLYzBFLgkoW+iZwFfZE5VRyr8sJXlWm6Yn\nxm5XvYKV/wf57Jkeg/NUQWR16R2oBLW7Ge6USp0wC/CO4My5fnbDleokDJM6\n3zRYk2x/gZwQQRX9jQ50nh2eg6o/Pm6vpBnxsV/E6rljycCeX7CQNikNHXxx\ntEopkI2UhpwXA6znPtFM6V+xG1ck374FrJ/NFLLJfCObq4GezfDkF/oDP7yk\ncJDpYKE52AXoQQt9r5s6wCAyMMQmR+sFEwYXe03/XGUKZrAR7AJD5o+HQB4m\nYVRn1SZhDeAhVwWqyoEBN9eV0XBaAcrpQwVAsVPHcGphiZeLleKSkqf9jqsF\nexYLI2wEPP/21MPNGLXQQk1UcXTcAh8Q2rdhfZrsTpYT66sAa40a0nuC5qT6\n4JSNVABZQEPGCTYa6GETdB1l3g4SiRyJUbh4kEGNcaeVz8JkqJ2xv3WGEACQ\nFvkIOYzHGUllTvezN9iZV2hNBlfKfpglg4jd8u99B5HdvqqTq7FQObuevLfd\nd9J78Ig6n3X3BKNTw9t0sfNAKZyjGKWsqFqIjwuPt/CjxnX1xk6aEK8njgCj\nhPb7\r\n=BP2v\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCJgZL/bLvtBiy4EnqG2yI8h6+5HXDnhp5yuPZiJDFWRwIhAJtJOU74CWytl2jgtBo84yQfqh1N/plgaeQSJx2YdRSM"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.0.1_1579370024046_0.21444893231675377"},"_hasShrinkwrap":false},"0.0.2":{"version":"0.0.2","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/jest":"^24.0.25","@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","debug":"~2.6.9","dotenv":"^8.2.0","express":"~4.16.1","knex":"^0.20.4","ms":"^2.1.2","pg":"^7.15.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","tsdx":"^0.12.1","tslib":"^1.10.0","typescript":"^3.7.4","test-listen":"^1.1.0"},"gitHead":"97c4f220c498291de20332ec6c7d13f24cfdd72c","_id":"@synvox/core@0.0.2","_nodeVersion":"12.3.1","_npmVersion":"6.9.0","dist":{"integrity":"sha512-JeytPPrOavhd15HBTe9ZA8f8d+ODcsh7yueo7z4pTRfY1wk5hp1uQF3JLB/y3G8/SvDesYbJzEs30wtvPNdIDw==","shasum":"973a2cf25ff06ff587e85a7521a865839a37124c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.0.2.tgz","fileCount":14,"unpackedSize":294732,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJeJOVKCRA9TVsSAnZWagAAifsP/RgsmcYOa41Xdpyix8Mw\nQLi+S+Pp7PPVDn0fguUU3CuSNkQzQ9fiOYWcco+yeaHSiB8AgjA1qReZDiC3\nLDDPq953DGRwYTWEw438KdMrEimACqBo9AF1HcDtAKBBpQmyJyjUWtxKJe4X\nroSr/tHGMAIovoeg1vfk/GWZd0+Bbeh7m9LR+J5Eszb73qZXHGiCwxA6xlB9\n7J2PEjb5aIs0ChB28B5yxeHA10P/Z4Y2VUvQeQG454YvzeMJ2Dlt72/idJNE\nggCCWqoQ04h7o7nPhGki7LD4dPl2paxWSKVP+xAwMGCsghhDR+QpCCq9tnii\n9U3JKHcLpTzX/3vTA4nqe1mlkVq9N2Qx7Iw2+yd+hBLUzz+GJ0L4SnifcxoC\nv0oHOcJtZ52pbLWpj70HOg5kHfCSg7mBL/uU5jEjJQpRD8YpIDDbLk4epA3k\nWjPE7+/mXjLWP1uCZIjRzjPL26zlijqdHRCGnZcgxNDktKkOdHCGFG8cr00f\nQ1nniZCEbl8lPQCKp4msWRdq94jdz90X+tPg80GM9RQr7kKE8m+D5G0uU7AN\nZl5+8xK802jJjKd2vm0Ueq4KJXUC8Ospz8U6QvN/J97wK4cSRI//KoJCHnOG\n/Z0hRp44FHBw4875ufxrV+kDokZtikgkZncdZ5Qb2tMGk4CRH2OBxMOO6fvo\nilO7\r\n=dgmK\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDrx6kGOWNdz1+y49/yhrommJL92wQhVWCWWEnv6Qg2TAIgNInpWug1h8MZiGnzsPDTSy5kvaXcx7UJfhlbERvT3cY="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.0.2_1579476298063_0.33543099031500856"},"_hasShrinkwrap":false},"0.0.3":{"version":"0.0.3","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/jest":"^24.0.25","@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","debug":"~2.6.9","dotenv":"^8.2.0","express":"~4.16.1","knex":"^0.20.4","ms":"^2.1.2","pg":"^7.15.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","tsdx":"^0.12.1","tslib":"^1.10.0","typescript":"^3.7.4","test-listen":"^1.1.0"},"gitHead":"b790da9724783bfa0f25bbc3433aff5695b43279","_id":"@synvox/core@0.0.3","_nodeVersion":"13.7.0","_npmVersion":"6.13.6","dist":{"integrity":"sha512-2QMEWgurPUSW1YDncVateWGrpNUSL+RP9qanW3YwCNl+NoRCp2qEyapPuf6TjYot0osjo8daK/erlAQeZouhbA==","shasum":"5e04b4d570a39726e57367ac5291d7997f34884b","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.0.3.tgz","fileCount":14,"unpackedSize":303353,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJePkuHCRA9TVsSAnZWagAAkjQP+wXB9Dj1bIbyHxZSg5h9\n80bjrMUl2Jr8vq5P0DwGoZ+Xq4L8htVNWZIOId2p0m6/GP6MMqeiGldOSeXz\n/3lLx+QoBLCT1JMBH3t3G8GySLQlW1mZDzFg7pxo/roySZ2XBcbhNuRlEHkr\nBZpcm/U9s3DgYL7kP4E0emF3hReTd7ZisNZNfIGWXsvg5RiJqNEERUfTZ1+Y\nTBLNSzZmxy7cZJYw8gKsBTLHu4oUViZPt0TGr9ZR9EKDtEPVjhSy+0LpvSPX\nJIymzhAqwvhN06dKNqW9my4XZoh24EUSKqRojL37fSkQhkKQuoySSjiBvDuA\nkQjTmmEzp1Fa9j8U2PZJGar5iCaJpeaGROjgPtSHnRGzOL86O9MTapMlrsJi\n6UdGLDR08k8w+2LeNG4T8l3b0h5Ivrj8N9FVHeT+22lSiM7q2YfMHz8CsuEm\n8BYKRIP2YvZRVE5hFAifoANinLGqp1FhJXPfQH7pkAmBYROPpQvTXrPkZbm8\njTtelD7zx9LGOpbBu0UqKSl0QEjZbAXoI3cLzc4vYhKgYYNcODAVi6fK35iv\nNh/CPSzKevV+p11W/Ve/OAhiEO+uol9at/sFd1/cl+DGp+fo/cGiMC8miXUM\npmA7sGJNI8lahO42FuF7VPbmFw1MRWfG0DTrfXml2msi/Q1fQegs/9XAIpw9\nTVar\r\n=cz+F\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDzbjQBGsXmDx7il6JiZv7fnenHGfA5IhbNGiCVY0x5gQIgaxxQ77ilHbc6oxJgTDwpHK0Ig3oHoIcngwgyHNoY/fA="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.0.3_1581140870876_0.22030930846008578"},"_hasShrinkwrap":false},"0.0.4":{"version":"0.0.4","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/jest":"^24.0.25","@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","debug":"~2.6.9","dotenv":"^8.2.0","express":"~4.16.1","knex":"^0.20.4","ms":"^2.1.2","pg":"^7.15.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","tsdx":"^0.12.1","tslib":"^1.10.0","typescript":"^3.7.4","test-listen":"^1.1.0"},"gitHead":"7da52c060e383aea140cb9190a416de132682a4d","_id":"@synvox/core@0.0.4","_nodeVersion":"13.7.0","_npmVersion":"6.13.6","dist":{"integrity":"sha512-X43T0wUUsG6hjp5ROfdx9p1HbpmC2bRltQOn5rxirO2XI14Pn8CO+Tk8ymxdiD6Zbv2YLZmyjpwtjIxeQQVCxQ==","shasum":"ccd2f3d1c7b76b4b6b17ad2fcb0473ab34b4db0a","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.0.4.tgz","fileCount":14,"unpackedSize":303495,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJebTC/CRA9TVsSAnZWagAAYVcP/1D5hPdmq5b4k9Fljj33\n60jZfvZdbQizJU739BOvlAo1X4Yggiv4RQCfWmGtxyD7XZq5eOErcTFbyq0M\nIoNfZ8gUhx8YKH/VWJ+hYd6L/20/xnEs6qWB/s0d7cDrtG4utyfrFCoJJ8bk\n7q3S6TiRsscyd0eC/liXP0Wv46N1HDShUILZ54PsWJTmfpnOSTmIKoXCMPa4\nzC84/xm6BKBzJsWAVNOB2KyZELqk9PFpLDX7uWOaVgq95tExaH1db1Gnldu+\nRWUq5VuxOAGZGof+OwSUc6eu3K99hxJkJNbTDPIpxZfNUep1aiheMT7s0BDz\n4P3xLYw6hs7dhRwrmg5v7arpYyO7zzvWjMzVBtObA47SH2leFNYVT4CslD6k\n03+f036gkEaSIx5l5Yjt5gRH1JrDSuK6zVuhSE3oLDLSvPcQ+ZMwkyFJdUJ+\nvsxS+eyfh/gvHN8IHjX2gxXg8SfotzbTIAcYpswUdTHSocQngRSJHpfLx2En\nqLyga2bQdylyKzUXK6Lw2R8YMS3nK0Vio4a7GJyZ6EA5Z+wJ70MjrAMTfq/R\nSbIE7tNEvIvibzn0WGTY7Zlb6uAVUgtyDeeKcNWkxbBSm0a2nlH2a2xZU0Zt\nIkxiVFYblE9HP4+yHjKMzn/tgb0JmDNdVh3CV0owo4fyCSbgPmt8jWz8zKZM\nEv+a\r\n=lk0V\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIHRa34mJaeEujNDC2XV0/vm+eK3r+9GZK8eG8DQ18js0AiA/o6CNJrrPEYHgICMIvD49mejqCisCkyYoA3ChIR0SeA=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.0.4_1584214207508_0.40793846912397247"},"_hasShrinkwrap":false},"0.1.0":{"version":"0.1.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","debug":"~2.6.9","dotenv":"^8.2.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","ms":"^2.1.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"0c1ec2f0c684e9d877ce649ef208033fe9be5621","_id":"@synvox/core@0.1.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.4","dist":{"integrity":"sha512-Ar33X6raOvF4Bx9qyUjlm+5JpTtdXSWB5eYJNAjJ8dO8cvX5D1aDb/7VMqrzpSzWZ/8C63MBBenLqUiSjN334g==","shasum":"2a64ae6dd6c6bfb4ade4115db54d5de91e48332c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.1.0.tgz","fileCount":14,"unpackedSize":462932,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe3ZlmCRA9TVsSAnZWagAAduIP/2YAiHdNXufWpNcBfN87\nqAaNknlRkPzybiDs2omJSoJdQSPkVwhFjYHmSA4LKpU33aTdfq52tLkVxB3v\nATG6EgdrFAm8/tzBF9vniFJVG+LrYsi5YOQKz3K+EZ7G0HVCDTS0LgkYVkgn\neY2dUczfqyPXQURoRZ03cHYsHqYSGcpCcVI7wt7nxvw5Yu62NwqGANoMPmbq\nBGFIpE0An0M+AfoYlY8+JZqIlsCr29p/hi7awMACKflxD82uMBwvzGURmrV7\npDCvpMChnYKCpatc5u7bFeYoFIR5VKcc7eTUyLFO3L9XX31fMash5VPrTBTB\n6L4qt+saHTIwUZ7Es1jVyBjCunRekFw6QfwW3ogLhjsxQgIi70vFNJ6dRu0A\nE/AWOiHjEr+lyC5ndjQ8bVeH3ON5DqfhGMWhOWRbaEZEovzayJDWoQtycvTd\nOzYdvSrMk+Vz66EaCZpAd0PxxeOMrUnKi4JFJfCgSi2SwPOoi+apS7ZR4IXE\nmjotg1enYUF+9Anot85Ryq231f7bu555asayshh/XXIXsDrn4dik5otpw06Q\n1LnR+vyHiiEio8sKrQ6ZmJeX33wizBJ8op40612rjoqYLRRl9X3cx/ap4vD9\nisgANUr9Xs97c+7su6PT1zXswR16bZmI3iZoMipBPbPF0i30k2l+QRl/+XQ0\nqJek\r\n=KmMG\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCsbW/feBbYloF7Qao/Dqk4l/cYBlgsZ2stKz234RWJ4AIhAOepDQQOH5tj0TTgsmA8RXAB9VwTA/5hvR4BWcii+1UW"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.1.0_1591581030210_0.6695207171506501"},"_hasShrinkwrap":false},"0.2.0":{"version":"0.2.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"bc596594b0a1724559d23d488bf7f69636c2ae84","_id":"@synvox/core@0.2.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.4","dist":{"integrity":"sha512-BJ2OCyBMqrMH5iOHTeI/+ba7EtTKuWN4xgXfwBnemfH73007FWTr+mgqHUo+oKAnmwNpYyI6Lte0J5bCiP1KfQ==","shasum":"df643e850facfba9e4cf8049237bfcda64760d78","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.2.0.tgz","fileCount":14,"unpackedSize":494386,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe6B6oCRA9TVsSAnZWagAAj9QP/j9A+A9egKIPtJF7cW57\n3NwoS7zCt4Exfif7flBFykGAHz5BYGgVNicQlMebwy7u0LdCWpAk4V7UZt/C\nABGnom8tO0AyFN3OlVdH9pgZNjaiXSVE5xO+ap2hLdfDRpzaIQyi6hj6pqGB\nBX0LbwBNL5O6UMDG27iJldoA0f4fyikXCJBn+Ua8MHjdWd66P9qdoxtfs/OO\nUHwup/IBhLcknm2snw6eNi2SQxEPmqj4QqTH26KRNvjvGB3tck5gHjsiKKK1\nGwTpNxmL+RrtwZo2DWGofiihdoZOiJG+AtlU1Zsdrl1YCbGCydprs2VQRSdG\nRUOqYbyme+8oPJmXb33W1M14EeCbb359usCzmvWy0XYdJ/q/4US4KSOXIw2C\nR2emICC7JL8jVFeHeA3z6rc/BMhgdp154Am0JIvW+bBqVWwLmmad0u3fBSj2\n0ApG3z62onY2erkOtLXuNrEcbSLwxDtj1gIWUHi0n5VNh9mlajYHDYdnOZDW\n5N0Xdx3MfY8QQvjPei6jHfSG1JIHLNwlPNzmiUUhGX++24kD7tD3/qOpfmGH\n7WqVZ6xKMIY2AYoprAftug2uQStSSCgElQCL9b/ksxI4R+Yy1O1Na8kuosEJ\nNGc53pxULWgGNDWkTvfH8cXkQkuElns/3nNmIcy8sJJZBqV5qxn/fzJppBm3\nRVi/\r\n=31SO\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDz5m2EGX0wnTcIGUGyG+yIME1FZdk+96i7iJZTtv1VTgIhANM+uLxCv+vIaDipdUMN0HGwpj5HgI9flnW7sbfFK4rN"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.2.0_1592270504461_0.33711301447284625"},"_hasShrinkwrap":false},"0.2.1":{"version":"0.2.1","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"e370ff94b053b96131e0d7c9d0c3e403517f07d6","_id":"@synvox/core@0.2.1","_nodeVersion":"14.0.0","_npmVersion":"6.14.4","dist":{"integrity":"sha512-3sL3JQzcUKm8aCVghjBhMTKpLVmm0zil1BwAwIwjNspEUR75WUz/roEjnZZioGQU/OmPykAutlj2H8g1+MhnWw==","shasum":"99dc626ef994a6198a5577001f9b1d5ed28ce8fa","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.2.1.tgz","fileCount":14,"unpackedSize":494723,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe6FiyCRA9TVsSAnZWagAADkAP/RgGbeUnKPoKRF7ZzoMb\nbGg7GevDhnRSsKoNyNx5askdWs0VWkkqInFcUBPiLWPtdwveM1ToeQSbUq7M\nHWBdjePdpzIiiGEGoCzoMvPwNmCq50WJpXgUKBgjCepnYa+E68saJGt3vOF4\nRzqHVsGRh1tMqDdXnHve/xswF1sF6XqZcJv3nRAYhoTgbI3B0VkgTbQoKKSj\nHKEvUitV8rfTXXF2mY9U2wJaZMQlYHSbVGy6F3DD9oandUcZp33yHn/octoP\nl+wVpI5bbxG69cH1j2m2Rg/2tEPBpgiBw5GGgJgPjV6B/gmMoYtLWYdxIuvJ\nAMpFkf10HBXEjlnXIukDOnPopTUI2eN8WmvKa8bscz/AiNoH4IlmthjRkcze\nQOzq6cKmP0GUC/zKzdLRho6OIn+N7CIIUcJ22NfmlpoMlABbw6dBthqR5RjF\n6QW+UwKFrQ+F1tvIhe9lCm44CdK4ln7yZzUMEySGdVEwmenezkT8FXufdTfI\nXdhnNFhColjfw8gyqKL0r24S4Bb+q99b+DEo15PtWsZHPnAFghR0bcW4E/XL\nyKW1ussY9L38LrF0Cf7yiydh6KKdMhoIRg6q5BL7Kw6+FBzfAgs5fKOFelT4\nNQDSaTEUYzvcVRzO6daNmrmGI1ycvrZTg0lEnNjgPc1114mAQp8Pn0hUQnJk\n/bht\r\n=gEd9\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIA6Gu9vb2FvdCP+AMRopTpoMxv8sgkkjujgg4CiGiZ5MAiB2NfxTtpq14Of//KA7EZToy6PKRyVAeC7tjTQRaatnqw=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.2.1_1592285362235_0.4920218427744967"},"_hasShrinkwrap":false},"0.3.0":{"version":"0.3.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"eecbd139f00f56ba2334eba7c9e67f7dfb3793c2","_id":"@synvox/core@0.3.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.4","dist":{"integrity":"sha512-XHez1sGVSywa9vx7iWBiIMxbYFIkZ/aHEqOgsoMSqoKMoY6xzWPVfM0yVkZY45T3qt9e+0z2bcsQKr5IzkgiFg==","shasum":"86245e3602255f40f1dec884e83fcd3fb14df433","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.3.0.tgz","fileCount":15,"unpackedSize":507466,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe7pxDCRA9TVsSAnZWagAAiVwQAJjyWvzHoJFpHuEPensj\nLHt9Fv36l5ggQFPWjHpLHkv7f/4mTfuymeNtqGlgJOSDoFWIN6LnVjDIpItM\nPiO/xm0xuL3V9yIPwNCBufUopb7qJitDoNARw8Zvvcmbr9r+2T5RNsxqX5JF\n4sYj9V7sKDJmMSTnFkwewjO4MokZ6FRnTA9ZpwJJQ6Dmbee+1DKY6YMpiEZL\nUUYHcmI3yyTo+SAp25m+EbIqKZ4QdesOZQYY2h5KGZAO54IrluTl+5KDBAen\nMeosadCyM9PUzILEmNeXKRP5lqelZuOaCrhAXdS3AiKE9MjfwrQjvQaQ9AWu\nGQ2y01bi9hrunFdIZBzAZVKjA0qb3P0p/UhD0Oq3CImnLU0r4k2+6ib8AmB/\nUEWoGBo+PskOp+p/7uFGxt/fBvJrhvYbbBYUtrMdBpQ4FQPEFZul1eQdVs2f\n1BbDlha9BjZaLEmKKsZCnNMK3cRaF0B9VuxClSPTzrFas+BQMIChrSzVj+km\nM6FIZglGTBTNm+qRPIur7JlSlkPWyk7eKZMBqr6/R/yUASAi4UNbIaU5ilro\npZKvZhhmDY66Nbaf025gl/SIYWomOC2WT9DB4hD+3SW/9L5x1bPP8xK/qzUn\n0VIoGyrXv6T3H/P+AgyFFvdKveRNuQxUJhGZyc+FSUZcHnrIC5RAUZRgXsUa\nImC2\r\n=BA/Z\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDCNIfu5BEmYMTFkVFPNZKAP2By2n8lLVswM9MpUIScFQIgAoF/JiZju5RYmPmsH2kM44tWpNUIU+v/25NkSqtQIOY="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.3.0_1592695875508_0.834232752476515"},"_hasShrinkwrap":false},"0.4.0":{"version":"0.4.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"2b0cc13aa8b061cc7754711e2be1432f02eb75cd","_id":"@synvox/core@0.4.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.4","dist":{"integrity":"sha512-RB4by3VdTPe3bW4fLmUWiduXFkdywWnQ0/bCAKHMb08riM6zMZCb1/Xmv5aI4HSgNUYCqBOvmcIXpoc4HigDKg==","shasum":"25daedcfa8673886defb97f9d7f53197edf4f761","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.4.0.tgz","fileCount":15,"unpackedSize":507575,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe8YeWCRA9TVsSAnZWagAANB4P/A7xAxO0v3/gIqBssdXU\n2DJcgwIs63rkdwNDtOVRgr4Ujb1pi0FuJ7++wZvisMpIK4nQIfyKH6KyqDRX\nYWdhHNeqjxpEMdd5Mw3XfcVw/0Y3Y5mA/JoKA3fwdFUoLR/twL42s1Ztgl3e\ngNgl1vIxKQEdNLUybUNUy6OgftdfR0+21FpavVdXfdfR9SStuC4WfH/4KYQX\n1QP+U/PA8MGQTRZ1rFn5Pv1P/08kXEOn+6Isj7tdecsMYSL6qtAfgZI9lMVm\nT3oxWkXySjYEitEv4l5fsR1fTqrS4RzAUrPvgEqC+FH02NQ/xOwyYrz35I2U\n1GlRWF4ut5kef0vxkWEYNvBHBxo+lw1W6P3Ub4h1AriSRL/ON4fHXbx8Tqi2\nTB+yDde5dd0ic9oHTA2iCIkuGkl8p4rdnvVtE3JR1SsfdvCH4K2WUXLKmMfN\n3eVCrTdQjLGX/TcWIztBEVQxhEGoWzGiEARnplzlaGBT4FUs1MerEiPFEc7y\nRQg1gieBPjxGy8ybi53gZIYzNHgylVSkdHhQiQhrNYcpWDNeIjkGp0LThv6I\nAs8hJPwn46C7t+R6KrVKPdTHnwPXfcDeVdv3Ilz2mnNuvJ5RABVZt6Njs8FO\n4KVwSxyGcWIAM9gPJSaozPje/oYtgVKIA+7mRti74wXoFcDAuKqBwmM0++mm\nPgeR\r\n=pVVb\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBclacw5PGwcBKD+Jz2DMloFlRUWJCjOlfo4TgSnzAayAiEAz72H7FVL8bHcF4n7HQOvmH7lgpjk8f7BamITxSGBL2I="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.4.0_1592887189757_0.4704538053197478"},"_hasShrinkwrap":false},"0.5.0":{"version":"0.5.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"a8e87b0e022c902b100b27310f53936c2dfdf7cc","_id":"@synvox/core@0.5.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-mzuE6W3l9xGHkXPV5BShtkMuzSrxT9IArCiQLjzFasZ1SykMuvehBz679bbd3YD8VwP/cIBN4blzl1OMQCI6gw==","shasum":"152a97e381a3d4b57ba7cf4bf284868a9fd1e0a7","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.5.0.tgz","fileCount":15,"unpackedSize":543052,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe9B9LCRA9TVsSAnZWagAAJbcP/38BJBrCUpl2gz7VnFuN\nnxEQExBz7nmx3IkpeiVtgNGz4e6ZdCaLKY4YnwcXQ4u63nPSNYwNCShUtZLb\nQt2Hr6G0JaOoe0uEQvlJe6dWEZfetiiPZfc+0ShI0LNMuB4RB9DvlhK0OCP6\n1cau4rXAgLcEb7mJzImX1vlfjpcdBsDghVYxTSJB/i0QJ9ACNpQNPFPLy8mC\nZr3QY3qvtb2wjv+4Zjpm24vv2lAbPNmfg1tfNxMsY6uiKktqEw3fTa+PZdAi\ngxzLhXIcrUHBQM58S9HK8U0Qz4DEfL0xI2jI/BJcJPlP6KZ88HdW8uh4womL\nd47ckz3DAwcr9dRSfBfytqxeV9Wjoq4pLGAm7wedEUSauAGZyku0BujIwzo8\njOKV8R0P+54wGd8kK4WuAmz2xzwydtANcE+egyRupz4Xn3dmu00p1uU8gMPN\nn1ZfvYuDRrTKTt875ubb5yspBw5cEFWoU/DOXKwMF0Fr+rxSX62FLO5pQoTs\njVCDp11XCIA7DXAjQZzOMbeGOeGsZBtMvXaFbrnud0CDSYu40qBzCQ/eAlX4\n29pQGIAtDsTww5ZGJPPih35qvJrvkzPkbi+PF/yjrrk+Qju+NAxEppJfOCwT\noB6SMhRWbnzSjs8O+sed5zHUs6QeRE6NWBgItwQeeVv7tv9MhYjisucOnY2R\njApy\r\n=HMzA\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCFSRMvyXgcDHW3+7/RHiBzsriagz5Q8HMzRUdBYF0/hgIhAPxu41q9rTuPx8uvrf9I0wKD24GSZT7/teovcD7OfUR6"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.5.0_1593057099499_0.8428082913349948"},"_hasShrinkwrap":false},"0.5.1":{"version":"0.5.1","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"24e168d7b3bc7610f060e4b39a8537151e4693b9","_id":"@synvox/core@0.5.1","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-30oj/pgni5bNrKSy5wb7V13c1JELGIl7LqLxNBIAqD+Fl8CoZmFqNQ5h3+ykGkDaiwmst6wkdtWaPcNGrR1VKA==","shasum":"7754478dde3938f2c93c2a638689719fa6879450","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.5.1.tgz","fileCount":15,"unpackedSize":544959,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe97XzCRA9TVsSAnZWagAAl+IP/0Cdo9vxV8fT9fctseTN\nHyszRKMtWthh9caecX6xNM0hNVTgHMiI2YYkL9Gsl2nJQ58MsCezCVRs3GFe\n0P77h7XkmLp785S66kWx0Vtsk4Mj2KzRZJbmoagdstk8QfMXsshAhY9u1qnm\n46zxK2CtRuGO9tzxymRS+5YiX6LZP1ntjHqtKMiaU9uHgnsjOOzpicWjBXxi\nnrD/3D2K0SKT/Vir5bDFNzsZut/0aBwah5AKT3F+DNu+/89YXx3aLMp3X/i0\nwmYWijAIMjWtjnHxRDdbGlDJhwpGf2UIEJx0JotpQ1VlyvRDWxnkPhT+CKQF\nfSa0DMz0Vt2O8Yl0auVfEZVqVSVfRaH7Gf9hmvXL4r/DHXxR2362EumJ2pFr\nyMCVql0wcRCWgRh2CcumR9G4nRMWHpNCo1YmJVxNUpm9J6hQmPB/o0gh1A0+\ne3LyrjTVVcEPo4G8sFTHwDRa3VbHm14rETHXX5WZG50Xeo3U8tv6G/V012zg\n10/5hZ5QsPd2I7bfmNWbZZHJKEh+aUxvwa/+nNeCAWHFhgm2Bm++bINvRfYT\nwCEJVNeSm0iUdv1jnjOEW2UD7oU+O5I7u1rucIm3V2RJ7UpfaXJSDVOTSml0\n9nhlkjoXXCB2x7Y4imIYyPWOPm/v5dkigRTvfl/r56if5D2pKI+dIQndgHsE\nGBEY\r\n=YVN8\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDf+RxKlJxg9D9LtHxgDxFaRCUyyMxHpAr7GLEw8rovCwIhAN/40N4zKVErgODeRDCRzPIwUYks2ZXonV6vtOZHhDaN"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.5.1_1593292274991_0.1047563348089231"},"_hasShrinkwrap":false},"0.6.0":{"version":"0.6.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"a5db42952c7c38be3251256b7af0d1e06f954692","_id":"@synvox/core@0.6.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-vD6ii9uEHUpgX8HmQXRzMhg4qA/W42hGAkfQd024xWlJ5zk3hcsv+jXyaLkofxzY5BrlE3q1SnTkyWubBBt4Mg==","shasum":"a5041d3a7adcf04bd7a955886dd839ec1bec3d80","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.6.0.tgz","fileCount":15,"unpackedSize":548897,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe970RCRA9TVsSAnZWagAAxrYP/1BwvmN9YLEbhuPHMG82\nAI+2cvP+Pt54k+0jZchQRFBbF2RIv6ERwX0v2MoaioR+err3qNwpPkKu0tfW\n7Dmut0vh/nSTzLDvYH/J5+juWEDaLCRfJy69986+nyqs4nv91bzDyJa09Km0\nx4Wvh8yEICtADr26vEHVUpCbOXLbFXbDQ1UQPezVB/cu1lCFGOOBHmKyqLij\nm5rUmVYLBmeYQwxi2qgqc8kWkq1xPfSJ2k3FbGKEfht0f75c5+wIZoRl0S/L\nKJeaLP9yTIPEHzHqNFjgHr5MSlrS0NIWOhpL7HTJQhl3vsjPmeo4QXLbwHkh\nfwc0szlMDvQTHJCWrNtNT0gYVnH9VJAc3b0K1Uz7h4ig/yseLrocvEF9irGo\nF3LZB7xboYp3EnlJLldKIYunwYOOWdKyPMHJOh8KjGwfFf03qfVelVDyPzlo\nmWSZruhfS04gCXJe4Zp5h0gPVxrmtOPcMqIcnkBrv6Gb0+ODZbZNxrcOa9EC\nxuKaRKCYnEg6SGMJTiTHoLo4PcfK0yTYiUETsXoY+8SG50xfD5pRplm3BRlx\n27+R9L6DF5kR4JbRjb+Zr1AuLj0tGVVpNph81U0OJKp+xxyUQnrcPXWtur6J\nrHBP4XvQ7MxV3TEc0PGx85u6FamT8Vly0we+o8d/Do1v9cvclxs01ETFzFZw\njAgt\r\n=ytXQ\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDAKeg032h5/So5gQlyY9bhiMPUIvLv1y6ASodhr0SdTAIhAM/s6CfAsWAmbz85rFLE4xwHNpMBBLwbceWm9YpTVgUJ"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.6.0_1593294096905_0.9692747415342295"},"_hasShrinkwrap":false},"0.6.1":{"version":"0.6.1","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"a5d0ab463e31a861502a43e43832e0c94ff15682","_id":"@synvox/core@0.6.1","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-UGUvEAuySeOh1jbjy0+bEu8g60SCP/p4Co4Sqv2Z9QsSabggymvwjOe4UXl5DdsJ8WuTPw3IcBRlvYQ3S7YzPg==","shasum":"fdde157ffb6308cd40b16ec2ad34b553996201dd","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.6.1.tgz","fileCount":15,"unpackedSize":551019,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe+lzQCRA9TVsSAnZWagAAcN8P/0+r6fdERG1cunRhHTqK\nnXsgjuba1XRFbhRxTA2QwQHj8PAhXK9Shm5qxnOfeALACoWZkCO7+lnZ8KGI\ndxpT66eXdYJ4sefIUjN5mhAPapskOS16Vhp5p1aE5dDjvwaMMngWdLS9iEl2\nshM/6TqHVGAC9hNi/TM1XrtuBwnVbSDStm5JRsRKXLooQEUMGnoki0S8E8+z\n/6ijceeE5Vg0jYMuxWZd/Tr1jS8zu32kNsEnuPtWugbIplMflkvwAPOZqbod\n/RhoRhNTwvC4HJ6AeLDPCsJZ++PNgjHXgrDlTeKkr7zLRLmDdpsaPG0xXW79\nzFOi7gxL62YM6oW1bY+awU+Fuw1qwefAHnLT80W+63iiGp+8VZLXYish5CQ3\nBrsDtt1YkYzi7nxRK+JBuBOIrKA/8qftC7gzXtyTz820rswGRBH4d1lqX81u\n4oF5waF1qwZEdSuz4EAC0OuIp7PlTMJPnx3pGFQ+3TcTJrVkP9WYkLATPhJx\nww1SjpFLYgyJH0q3PvEbeC0auPlYXsWd8U4q42xdpsJHc6OHncp1HVbpZyNk\njgLmU/774A9i3z0BfyUu8e9ruXwL2r9y8cP88OS7SitGFac5tMlH7BDexzca\nUhHw9PxSsoUISqnWaM4WrTS8ErdjdGKdar59KLYGsEXQLTZ+ny6AwjlP6v6a\nK76Y\r\n=tHzc\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAXJ/c9iJkij+1OTOrbcnMPqR1XNFM576Tl2J7U8qFRQAiEAyocqVwAWm2VE/Muw4zvlHAbLmrSUUWFgOwYFUze2W6M="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.6.1_1593466064138_0.44836171421836024"},"_hasShrinkwrap":false},"0.6.2":{"version":"0.6.2","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"2670ebcfc07a40dc3de59eebc5d6d03a04f7a924","_id":"@synvox/core@0.6.2","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-4d5Z9kHxyuVZOH3JP7SAXTo9LU+itaMcTLZtj0g+FFyZaTQhLeHSbZpQvpEl97mLxPfk28fgVSxYkYmrkp6KiQ==","shasum":"af2e6bdc1c47ca116de2833d0c4264fadbe74bbf","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.6.2.tgz","fileCount":15,"unpackedSize":551075,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe+mJ3CRA9TVsSAnZWagAA8V4P/RpmMAJDPuWEUdL4ug8Y\ntVrk0V3SfUndUM2wDoQz0kH8TMWkaxBMaSfzFsIttrN6lnWZvSzPw9mf81Gz\nOwYU7cg/ti+alvdmlh5gQmlpkNFkJ5gemEwUpqNcx04u/eRb3K65QEx8n93v\nsD7SjdXJCjm9XdnbZWAij+SYaF5WOEgZtigZhCGvL5LnAZD2zWSv+vFJi3WH\nHcADSjsAmXjxJ+zvRl2RoMhyAdxdK+7+dlFCGCYnFrV9+7IhxzPIeJANcMA6\nNurMqirIlTO+nDtEdKIVUw4VT8J5RKWvPEXurAuM0DAgwL+/vqse6YfCh4V+\nxeZgAUW+H5Yhgog+VXbrjjotHdc9Gnh/BXZHTEU0mCkVmLiDne2DDG2Jk8k2\neSsLf6ZNMjZ0M9mJWUpd+Jxq8jKpL0eVrrbgPVRjVwiWEf/CaJ315/TEdABA\nZz00em1d352aPVYoeFqvlwc37TMBWOP9YkDGoJTRvFO3Y87PjaVSxdLVnzyr\nvY1jnp6KFTX8732jqsSdVg+8N1m3Z43RYF39p2dJwm7BjEJT0eD9A4rQ66Ei\nQZ5X9yif3XNyS61pNOUULWF21lEklS5JjIuWGQV+0CFClrPx5jHiR4bzBNrf\nGoHVuAp+FGF/zJn8EalIadEBLU6wABt16epFnMhl3InVW+DzueGcFcq8YL8w\naPfy\r\n=t3Fb\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQD7uJNJv6vnPjz+FgDV6crAwbTvLovMKnTU1IEgWr9Y8gIgCFlFnC8v9skkdZ51A9fVfaHB7Ge5Sn3N9FX1dwhym48="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.6.2_1593467510849_0.6137506462600382"},"_hasShrinkwrap":false},"0.6.3":{"version":"0.6.3","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"06dcce9b2e5e32ae77dce5ced4b8f474348e0a30","_id":"@synvox/core@0.6.3","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-SMJpbeN5GdnfeUMsITzRYcQMOGrbg46HC739/pZtBh7n8p4dE5/ajK2c4CPeXySymBMDaONMj8SWxH+71C7LMw==","shasum":"4bf3b0b043f10bce59ec5ff8df502c4e2bf44599","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.6.3.tgz","fileCount":15,"unpackedSize":552675,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJe+3mbCRA9TVsSAnZWagAAAB0QAIiia3PUZrx2oSOyFjNv\ngc3A86mVZXVnXmEcU/60bfHtmWMS1606GiPJY16H2inL4zEtjc8+C7fCjBPk\nfRt9WRwfCjadoACll9GSCiStKKDNS5AYVXZYUIg1f6vWSIwsAxS0SfarfaEA\nZjNQ0F0LcpSngj3Yj3yxsGhI8FhDk1qrrL5fNcIDBqwDp3MxE99aZEASw0V0\nSMfkuXK5+F2dzgOZGmwGt/jyw649c0/b65zD9W5H3Je2PVVFjz2QCQacRPzL\nawVqOg+dw1lIadKCWP/of2Q5h1MK+fPdBMEmOE4vUBDHHOlq+HUDtdBqDFk0\nTUN5u/esFtOOkHvLUGT90X0VfjcaBWbjlOB8qq/vlsgjw3frJWoGQBdCb+uY\nSr3wFiyUY0iw6IJmW3h2otwU4T/fCidzC/aiy81D30w6pr1x8hMHovwJfQk/\nJ9ZWeU25oQTI8SEczbjdvu8EU1FFYTWJO6pVuiOM70RBgV2erpu9ExbyPzk/\ngPhD+KLPEzyOdwWladeus5n5lRPAIleoUg09Ejjwahark/jB4MxhhChgdz6N\nm1XwLMQCoIY9MQptzmSwoPPFsKYjKzeDUDCuqOmEKq2YeDWf9pAcfxff56pY\n4tLrYpLGZ0/rszPFkfYqj8foOf483P3bzqM1hhLHI3VKbAY5/v8BqO8KcyE6\n6Uf+\r\n=tX5e\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIGhsKCNFBE/w3W1uQn80Y1YmGN40EGGFUSQl0Yz3jJDLAiBR3j4IajQctOCNB2Q+NGTDxNhgkGUl0bz+a3gsvVwjzQ=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.6.3_1593538970719_0.4426156396653451"},"_hasShrinkwrap":false},"0.7.0":{"version":"0.7.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"73c2a35d50d2f16ff3928b41f619beb53d4f6b97","_id":"@synvox/core@0.7.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-ev+qJSIXOVvQAAk9wEQ3MGetcDgW7RJWEBoC1iAwxanXEZMgJJW5F0+qt9OkIBQnMj6qrhfCGPuNn1xJ84zURg==","shasum":"0275a4ea3757dcfa48dc0df1f9e0f87439f8c286","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.7.0.tgz","fileCount":15,"unpackedSize":559980,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfD6cECRA9TVsSAnZWagAAGG0P/0Lj2izwzKG4P1aK5oHo\nBeHgrKoFQv1tE6HHtlZFM5CfiFYMGaboQ++Mka1C+36r47VUt25sp1Uz5yg8\ntY+YDDxJ3nZvH3y99rTkwhbGtmnWP2Lz2W3TyxoI6AkKFqZR1jV7VUQc7fvk\nTfceEyLIY4SsO+7eIqeb9E+wXiwOBQvAi9TbfQxU5yisLqjRwFMqEpIHOR5+\nB/sVYsXtTTvi5N6h8SeCgQoc1gtyVPWsIxGqvvqyGz1w6R9hL7cr5NWzIOFM\nYZEFef843Dp4zC6K2Jc6Q84nGlmIMm07GY6WP/ThocAqrXd2xIl7u0r2+QGq\nIWiI6QpEGC9YQ0SUBaQumdGf1jspOP3PWkCcZ5VqvJrTeDovhiTk9NxY9L/E\nhAsq/CrzOZ6iErbt8spYHBQh3JQxZQNBaLG5dSW3mbZYCkwNCwkvzCloyXwb\n0gJqEIBOtsieSHAs3pYuzSWFppD8657r3smgsVHOZTHzIWZLqPO1nq3HtCEC\n2FbM5NJGi/NjrhVaFJGCLI4AGo464sS9151PtxbK2sNa2xUuQPAQgbw82DF5\nf8tk85e5XNQHhzQn/e5a0WFLguuTc0NcQTEjgpp6adNVxQpYoYPBuNbgBIf2\nB9I/KR3g4mr8yer7rLCPjRmS/XWOLfMTNOHN8kKV3Op4gGRuvpWGzig5LLTK\nfUik\r\n=1DfO\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIB1qltPaD8BQJx9NgYg1423tIeQcz5Tl6qsKgSLMHw+PAiEA6rgwDsgM7tYvVce5SvDE7fCqNO0fF1fbZsHTekia01U="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.7.0_1594861316253_0.8697012652076459"},"_hasShrinkwrap":false},"0.8.0":{"version":"0.8.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"021dfbe986d4f05c7ecff13be6130b97433bc9a0","_id":"@synvox/core@0.8.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-ZPbJ8vP2evy+CKWxlYsD96iPFj6YpQb7FyX+2uNQCbP12IFaicMFeTRqykyBUKoHYFhwqnFWIFqLUi+lkf2GMQ==","shasum":"8d2e2a87befdfe3e6ee69c61b59101c350ef042c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.0.tgz","fileCount":15,"unpackedSize":579093,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfD+Y3CRA9TVsSAnZWagAAZygQAI6jcgvK4GSfGciZFkgo\nqYMf0qI5zWUQG+Pf0XwJmpxNGwP6B1t01aHnLS9x6QhPQLa/3+UN7Pnubpx7\nHIS7T50qDuh9cZz+KIZLifG981dvOnD8robAUPKtaRusMmcsc/s/NNuy3gfU\n/ajQU5J6lT5Tbvzo0tUVwYlknJQiUfniS5m4eVrY55rYg9+oxthShDRmffSo\nicE+d6fnfnrP6wgCDIYI1LO3uXkEPIpE4RAXRFZNFtM1Qy+TKC5ixKyzslNO\nVPcWFZaKUmMGxWDWx8RR9wd/guFkAyArP3X7CLLjo71uzqMF1x28cKxNo/yH\nDCTlslPVM+3T3MVTNKXT2PoiqnuWH2ljOzg9x8ZIQdyreky9xF5LKYQurIx6\niSFi3Ckl2WWQrinEpoUlyENT9Xos0kS/Ic/ugZOIiDeKBRlCsW/0dcJDNxvG\nEoLeGQkovIKOHQiu0IlohdLLVIxoFD6evzfOw+ffwRlf0IDhlPCy3e8aeEbz\nfa43kESQRIQEBk1CILoZteK+eE/llDt/dU8lvD7BSbFSlcR8IdenBpq8Ywoc\n+RZ9MWVrcXWW7aSm4AE98yM6AiB951AcMffq11oPIKq4F/LnMPKiSS7QiTN/\noL8fFiOVjPdHFvDFj+KnQ2GWVCvCAPv4uP0Kjax4vF5HlbwYYOm4tGZG00xA\nwQAi\r\n=NPfT\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCkoAuO+lWRFcXrBQjafcjZvt3UtynwZhxwbC84TmIu/wIhAPfl1FO8PRoxfgre/+xo2NnLE8PPJ0GHCjVYP2fNIpPC"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.0_1594877495071_0.3251062510994662"},"_hasShrinkwrap":false},"0.8.1":{"version":"0.8.1","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"12603a710745fff42b8cda6126108cb205512ff9","_id":"@synvox/core@0.8.1","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-uuCva32oZoryfvhI5e8v1irP8OKcyfZvO11F9I/9nK2+zZTNwBGjJJAZkbM8NEWMHSKejrCCw7mPuOrXa0SRuA==","shasum":"5b48dc6f5b4d12068180259df0e45d8b1c5ccc99","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.1.tgz","fileCount":15,"unpackedSize":578769,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfEoEzCRA9TVsSAnZWagAAxQgP/1yapGkNquP2yVrFqRmi\ngVl+8E0XoFqI3WDM2GA0P0o1eeO2UrPJe9lTfv3ysfDLPewFxTXNyOpz9U4N\n/wwm399ucXhCorn2oTwBK4J4PG8jj13IIZvDr+8KuQbQfIbw29S/TAd8wlYP\nO4GQJPCMOcyiPW4hYzrJ2RHyvlWTqClAjetEs1aLVIhZ1cpxxfisjBrHKqHT\nNo0Oh3ortESPnhKrgVJFBW0WphhxZ0i5aEp+TKNA+eNT8vbhAbpst27E0/aM\nnhm2phMsACUzroBKpPiBeo8sMmTU59volTltj24gTn65nl1J+O0VZYApt7l2\nZDJwGHu+DqaPx0c5VRGitPAFVrjjanQNH0gAyD6Htv+gDKJTOL2gaThjU96W\n9QxlETMXvvB4clgp3lIqWAZuq/DohKfMvj5IahmK6bL840qSaMkz2VvqfKs3\n/wbd2z28pr7VOl6QjBy1C2Whqe000ps+h8Nl7tI1RxKSnllQmgDPb+9NfMP0\nbBFzYz++uj5sznNBg5Bm3wjl55WCGPpCeIDgRzGM0DDdVr/Ef67oaC6YZTyn\nrvnT+6TpkmPVd0r9AMiaxKTp3JrGutzmCZ+c3ee9beSc5/INdq1UXNZVb2us\nJY/3qfmshh3V4Cha7WgFpO1eEujs5g20Okp7TGa1TwTUAatubr9umVGCV+uk\n3Rl6\r\n=uTER\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDaecScoIJ5k2RP6cmc2pBMsS7fSS58HlJSA+UzhX1k8gIgC8V+XDV5CZS+Gb3pFeIAlthdm6dv4qz76z0nbZHbweE="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.1_1595048243197_0.17778981235342783"},"_hasShrinkwrap":false},"0.8.2":{"version":"0.8.2","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"0a208c289df6ac3e79442cfed4cff89d1e0511de","_id":"@synvox/core@0.8.2","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-ujD3cYIsGi1rP6sEz3w5grvJTwF9i0yENaCyveIoYsL3qTWUiEwQq/NVCyZKsLrHP1XktvYp772bE2HuDJAXcw==","shasum":"fe3887ab25a57c6617cc0632c1577724a97f790c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.2.tgz","fileCount":15,"unpackedSize":578441,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfE9xQCRA9TVsSAnZWagAAKOIQAKPh9MBCJoat0xRjxp0z\n403tSP8FXlVWLTjGK+VdjEXzXBpxnHrwW/wxkpjER9PCG0ULndD8LdsVvFKy\nNS7OuCLVs1AnH8yr9EVLU7ptKlLqbKOjtw8+NsWY6ovAMvqSR/6q+LzwmNa8\newX/nGfX3uqugQKWvk8xmAbK5ljdzSyyPj30vzt/fkm8hgHkRPPVWxgqF4HN\nItIODIcKSZuNfQQm6XmEhR04Y0EMVJwO0tveNUJ05XAVNUvSFg+r57802lWr\nau+kEfxGLhUuOmqJJi2PvDzrLkOHktvNIt/12aGBsDgi1YK3C/QITaNLFjyi\nfpOZwTBRWeszGfXciDGdew9Xyl5l43wb5sbLQDwqQScbH8HBfx223Bmljgv0\nNaWrAyCKK0HoL5i53pKP/tg2oVkgvJ33smWZHM9sLK1J18ynVI+3HxcAzqIq\n1vmx6zB4dOPbzW2d2geiQXudJPMCtxKDNr3LewHRTyQMNv2lEIwZKzMm+5s6\nzK79ezV8xWG4gk4oPXuYOcqfSX5Q94gOGL1kkxY+kmy/UcHOp+eyR4T9Srsf\nWH099E0X6In+WtCYgTLK9RwLXeoqsft/oGXjK0d/uP8hC6gQxcWWVsal8Lpf\njuKfkE5zRAJS1q1PcKKon62zp9EW+NY1rMZ8zH7e24vWRldy+/Uhkso7BjGh\nyCsI\r\n=in0Q\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDMSiY5mtEZge2Ko/uoM0Y8i5LDFEcCFjGdAxItVpKrpAiBPCnlZoV5UEXD34+hFnOwgG8GVFFS+5bandVX/PyBkaA=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.2_1595137103932_0.3048025275052866"},"_hasShrinkwrap":false},"0.8.3":{"version":"0.8.3","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"51b99315288a53d6af7c9344be5401e53ceb764a","_id":"@synvox/core@0.8.3","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-UJpKidSmLj7mw9eq4m30ZLuTMt/8/aeTfPItYIzqlWVK3hCEvd2VSPuMGG25xCEXlhgZAYvN7f317lowPlJ+Zg==","shasum":"0bcbd7111e1d4c8447020ab1a9893ff495fda1b2","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.3.tgz","fileCount":15,"unpackedSize":587049,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfFMw+CRA9TVsSAnZWagAA07AP/ir6Y3FqZqsOM5zHQbMO\nQIisKBAva4jk4Z5FHwtQQHpNHMjVmXHF8JKOKCnWcSDZwxqcceLAVNfFop3L\nl+eHo6JsTyQ5Cl2gXPGm3AKD7FpDS5QcrBCLzMVdQqjs3AA10cQUFhthJYO0\np9SWyMyKNjDTEtefzToJwvntX0lLT1DiL8UDA3HjUaleDl5RqN3wZlBE/4W4\nz0lCR1DhEfmHipVYEoJYtLHBgkiKdxdxk8Lw1OxV5ZOyVXdozqLmvA8hHrYn\nlrn1B0a00EVYqofAvHUBBl8OjHynfl2aW+ak+psbgNUYXmYF4BNda2LiV+i7\ncdnAO3jSlyY1WMqui5H/32olo5X76GKKzWOO/WcgaqG4rJaTGxlRP3ZDvktt\nl+igULfoFjZNc/09CuQRo/zt8SmIaTd9qZj6TEaS5ZGOAtC5PTEbTDNV9EEG\nuogs+XaR5J/Ieo7mmrqCx79Cqhdwi7HZ0OL4QqBfmRQnh/3boiJKeld0Gupb\nVaYHRmWEfbh2G3eUXjM0Zc6K9H1VemgvJu+/B4Zbj137HxLo0YI8bHLln4j2\nNom+qS50RczikTyTHagVatoaqIyQS9APcnZu4hBVoSN20GvWalWofZcGDQGB\nQTmL+OOixeHQFJNVc8muxm+1kTrrHNbmmb+IA5/jdPHFxcQFOd4cFTiIVKdu\nKX5n\r\n=Ef7n\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDjjiW2vLowh1BlxGoXDrxjSl+m5aRgDDYSapFoIpofVAIhAO8Z4H+6Zuy+gIICt4LSVKgboitzi6LwVffYTNhhAqSH"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.3_1595198526234_0.12137925987950249"},"_hasShrinkwrap":false},"0.8.4":{"version":"0.8.4","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"e03fa13eee7f19987299aa1c0f9abe87cb235ebe","_id":"@synvox/core@0.8.4","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-BZEBHyFYsT8aNHP08smNhpxv4NcDwRZ8cJIZIZYkglRpExtyrRRV3EY8S9gobCFvUEaWzIJRM9JOf1sDQAMH4w==","shasum":"1461bec67717b0f8db45acda722d95957745a599","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.4.tgz","fileCount":15,"unpackedSize":587182,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfF5kACRA9TVsSAnZWagAAsRYP/ialELoMkaI0rWGpF42E\nH/nVgQMXOiX6ZQI7CWQeEgRoEBDNHVX+Bxwd0lDiXleJR88SyQP79I6y9MQn\nnrngG9lK2BF5TVvJnwVYm3ZuNa1Q+YCDMKLFex7Uihb5V0JwkxWhp4vHokUa\nTjmCT5Wmar/8sa0T9RKoRogFRMqLMoXWpOGV/R7rr5mgFcehhEttq2dZ0ryV\np4mIzWhTdtrdNGH4JIU6pBPBFWWckBuFdI8gC2uq2EN4noBMnVB5Fvbus66w\n5xJYJsbzMhd4as20SeOPesIqhEhyvQNfmIEXoyAYoeIFoOhuOYU/Ivbk73bX\nz/ZitI2YExF/K5+XuwI8xv1MwMK9BagCHKnD/rSIWYrHo/SlCIB81wQRamYb\nYASb9YABrwHhgw1Kuh5CfzIBfwsYFdMRlnDo/BY0nvQGn/fkogZsISbt/oUW\nN9hIwozhMp+PQqFz0LGN/QfEWCAIJMvUiTkJe12Zv8XULmv4UPcoFXEcrhft\n7FdvybILpuQ7yMghytd2eLMQrwpe3c6Ov+e2ZdL7K6ErydryWlw+LrBjkAlV\n9ImsM4PB5bPWJQYKjFyyqQLFNSOFoGDpgf95Az4LE4DSF9VbdkVlL/ikD35e\nu323L20Wlt2UUKnjn6dfHTjssj/30cy8CTrjYT/yJxW++EE2wufrxkHnmcew\n2y25\r\n=HQ/Q\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDCQuxccG+9admUtFf3YrrvyPSqi5fbmYN9emngDJoZrQIhAP6LZ7KEYkmwXVEVZebfYaH31heqtjDgvbEFboSuqaVT"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.4_1595382016342_0.28254735715992263"},"_hasShrinkwrap":false},"0.8.5":{"version":"0.8.5","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.20.4","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"b7a08b2d73401fe1c590739d86510e551a987f13","_id":"@synvox/core@0.8.5","_nodeVersion":"14.0.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-/Wl4gwoKVu6dAv2uzaUDd4NplFSuAO0r9QgSn9yL1TTwEpEA3BDJzcFj4fv0AATZnxzt4VMBdR2Oo95v74vUxQ==","shasum":"77c1299dbe83cf20be8878f862b3452aeef61a3f","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.5.tgz","fileCount":15,"unpackedSize":587001,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfF6/YCRA9TVsSAnZWagAA4igQAJPq6dbDWCUul+If90uR\nqy/HTUGdBkHDqfAZWxjI4tQjNz/W+teAyLIovDU+Qze900fcOhFoXOv5xFXQ\nwBuzuSah2skJIs1GM6AxyUwr5i/wswToL6po1Qh6hWsoS7lCgGB6evzv8DEo\ns9P37gD70yF1GGzblcUz3FKnPzDvz83L8DZmcdicr+Tu72lgGCQfR/Ab8uYv\nHNFY54XLWNgpryxULR7/sWmyv5qdqnLkidQAvrnhCjpLTQ+arjVcxGB3jrcq\n9GN7QwiHk/fAI0JsPqt8pU9YaPNCwq29vMhmdsRx/VeLG1o6ufs8IY8kwgGh\nevYTSuXUzFgmX+wDJaTAn8pORGFS2CA11bOH2zwi5DawHurNbPt8x8oRvF6f\nJG+q+5sncdRlOrz8RF8VAQNq/SEaTkq9oGrrzPqnklwuvFUVKiA5g5pUZjeR\nVzrKON8Lpt6q6RTebjM1Ds/2bsZTdvwl69pWjCtKw7e2f92RFg/oH1yTd21E\nM7BpCteG2wsq8HePTmCVz8t9mBLlYr+KHY2ZXTzNXU0L3Opixu3HCcVAKbZb\nrLLNUpwO2zH/lyISFAS++TKMbcf7iGalkUYVBr+mfrKLty746Zt7KXQtiC6a\nMb+DtC6+K0ioorUJHp+lR2Rf+1dF1Zdbdq8sBrwZPInAK+V84Iuk+GltvnfG\nvU8r\r\n=Wxt7\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCrMzSaF9nu+HjGAl9b7ll7bWyyAVmYwYtFKRop6HTgeQIhAO1KYqLdhjvMefGnne3E718s/viTT1m8K1CNdw7UL6c9"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.5_1595387863791_0.5164040811991168"},"_hasShrinkwrap":false},"0.8.6":{"version":"0.8.6","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"46588cd43e6dd472fc90890fb506269efe97046f","_id":"@synvox/core@0.8.6","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-7pMMt9YfuWo6N82XTL2acvAlVK0cTAY4tICxwNwUbX5sUfrHtUgSE+3Ug6pgfTZEmIeWxEbW8PQeYnanQu6OLQ==","shasum":"1469b47f81fce6616581a4c105255e0a5c115398","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.6.tgz","fileCount":15,"unpackedSize":587003,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfLOdACRA9TVsSAnZWagAAhykQAIVSUb4Kv3wjPPKr6lSh\njYT8wuSZcaGtnSV2FUyGi3ncWp0pLA8IF9o7HXfTJRipOu+rlITxIp+xsJAo\nTKobxY6Z1sSA4CA6xuKp5Q8ApHhKnS8BjvT2sAXUgAxdGXHaNFE66O+FV7dR\npeaV/2ph/Z/uNL7wltNvp5R7X/uDHsWiinZpUZ5Aj5RoZfEH3rZneeVemeth\nTr1ri8LCauybAT6TlLQrp12QLPDeg3RfOt3srVsI90vuJExzd3VnMMReJ29g\n6+oJyU8kN3fgKOY+sSxiFaeEHBX2Tg0MiAR4g0hbosGkRQ2TYUSY1IloxtS6\nfQ6Vl5xhXEyx+SnEvBgi9BMhWT4i6XJLpwmXHh6kTHvo/A3C9b6ZaVIhVJ9/\nI17IPTyIkE+n9Fx3GmZDYnfcd+G5kzIM7g/K3lNaSTymzGvyOwLVlNc1fqsy\nMWJwm06IJdHlzo5vFr1IuS5fQUbd0L3PQySy0ORvrS02vnLlIexEAYNVcIUy\neyaeBAg6xPMFHwuQgcwPm0i7xNEulyJQy3hIDIzRALvivCScGEQSjuuh2p+N\n3SprSC0hinnUqJtJWAQNyqXI1RJ+5YGcSNim8G45imJk+hKc2M3NRDNF4WJ+\nAfX48EC5VuDQpMYzkgjqDlI42oW6zGKiU2QJ5aYttWRmNRGRw4YcqRqfP8eh\nRHFP\r\n=Pynt\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCdx5L7aUpxz6wfZKOwSUVPpgA7ADQ6/G52ZwFbLg3VwgIhALDUwYr466QbtATV/Xr4RKDiuI0QpoGOEB5xdWdSI5/g"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.6_1596778304256_0.34234985213468927"},"_hasShrinkwrap":false},"0.8.7":{"version":"0.8.7","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"e0b7f7491ac2a4237416d352311184bdfbaf538f","_id":"@synvox/core@0.8.7","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-qSHKDTKmLuHMdSbaEjFS0AbNvs4CtLM872+VSNwy4eKfx7jryPBY5zmg3Dlaapbz6CBa0Opo903imzBI6qlt1w==","shasum":"0c1ec96c5d85f3ccdb314b7f62e6d99f2544182e","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.7.tgz","fileCount":15,"unpackedSize":586595,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfLhZwCRA9TVsSAnZWagAAn5QQAIwSEyoSV8hxMFEIH7R3\n5LtyBirrUC1GGLYZ4p5QUfYVdU8BIuhbvoP/ln5uopL2QPiI4KwHzZQdHVhQ\n8CeYhm1gDbV1WUZZbRM9CGVi0tHaHNEgYYe73UwX7UQjJByg7GwnoRXq9LvE\nGwHdipgMhk7xibTRSbU55lfBcWK4W1uqPBRnMNTWFfqX/6d9ef6lElSd+unc\n7dievh+MKNKivgq4nJmjwtbjlOGPqnBHXMpGCxSTPXnEY+f8/FWzPjzNnfiv\nhlBOwN82XtU/sdTXnOUakWRp2MNuqFpwmx7cWjCLtG1RigjdY0wD1DX+LC25\nVXkGL/0nHoJnJ+32CNq0IMoeHej8ZFlsHL/QQndCGIp+qqrHImj4tYQlCw+6\nK6vOxxiAmrEiDTzchyqtYRxsFK5w44WQKry65kBG7H7brzpGBIc5Pm3c3Ivn\ntqm5CDN78Y70db02QwsbJsmThatwQLsvMXm/4K2bWYoMJJHYDqsjzWWioEH2\no4eqVPgac42VvZrrKiIgfYHWoZBS+bENxr87Yl74ZfbGr7IVnmbPvo77XCeG\nDw/khN2KmuwMbEYi2Ow9A0DcIiHDt58K0tpsPkluFtm9fY7WkcbwplXZZiIX\n2zIqdh2lW4xaRFmT+I1H0VkTwnBdjiivS/Jt7kwgWyXEwgx9VbGKoD0id+eb\nDdaq\r\n=ZJ8h\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCdNS8MP3U22EKDySQbStl9ElNfqA6DiyHcyHMsbvrpjwIhAODzbnOiD7sOt15LDFKhDiTvSgp2XuumJELlMwnPIhWa"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.7_1596855919696_0.10847006818574845"},"_hasShrinkwrap":false},"0.8.8":{"version":"0.8.8","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"044227280b34ed62b755eba8d624312826a86bea","_id":"@synvox/core@0.8.8","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-nzWaXXauSWmqDHxPThvY5iI8THSplrUieHEybPWyRj2k5edf14MY4WvpD0J5Sbz/NowsgohfuIzP2OHi1wIrQg==","shasum":"47dc686a07d71c6dd5369e8f24ffe7a7420017fa","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.8.tgz","fileCount":15,"unpackedSize":593749,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfLw27CRA9TVsSAnZWagAAjYIP+wdYVi0ROUvuPSl9IAOu\nfwVlkybkyKK0o5zgt7Ageo9bAyFvafndKaLBz+u9TYakH/rKVkGT4N089Una\n/hBYBfIgCy39gYpS6CJIprK/ljhK6crb/GnvA+cHtrRRPWldqnUsef3y0uHK\nFAKhy6VipRdoGHLXLHpTuTOz1MSz1ayw3dR0r5u1a+oAnMhmR9VR2bpy8Bbr\n2ZSed/uJ6Gqas853LVwafPgFgPP7bkM0Uu55wHQ3+8R6nzF3UA/o4B6iaCur\nLljz1as+SD3PO2xnJe3zo+kQiWrXzkgQoewGw+i1JHFNOsf374exHU3uv+g3\nvhqwuqMattFhF/nMBtJF1yjoS7wr4gx/YLS9hk3JKFSy6nssfgeU3yjivrW1\nMMUGarBSezAl+ne+MeJuOVVO2KIbLhTVrv07xhOFOcDUkqd+u+yfYwzHLeHU\ncYALhJNR+bxPK/cI028rQMojNz18zn0Oj+B+dISJf75eOcFa4u73hloHEWD+\nyTKw1TviEQD8UDChB/LA/HruSW5O+TApx4zZtls8E0DCahk6L5TW6QxKWOW8\nuOBI0QSoX35rd5PepiTv/6SPhg/bjbVyefPbpYOClHAZCR4I0/4rSWubxTyc\ns0bk82N1+s1h7o4kfkVtEOyna4ubh2+zGTIcySA6Q68RZASOUIanp+VCeBVI\n5hAj\r\n=uTuI\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDY8NRkfJYzqP0MGY22PwvCXoJ+BvFzjFVf2cesR7zyJQIhAJwcikbbEm+U+h2+Z/OUid/wPBXlV2nZeKJItmbynC1S"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.8_1596919227077_0.39664858647578427"},"_hasShrinkwrap":false},"0.8.9":{"version":"0.8.9","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"04cd55262b09a63df269f2e8451abc28b33d7799","_id":"@synvox/core@0.8.9","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-HXPRFxrdZTixz3BfJlWbMtgGlZxU6qW5Le1rm+kc2GGbidygYduU2krgIcur4Xa9+7ftGJ9xGc41f41qLyjyJQ==","shasum":"ae865726ea90385e03c434732a19a43f6f568c01","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.9.tgz","fileCount":15,"unpackedSize":593850,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfLzp8CRA9TVsSAnZWagAAT9EP/Ru0VWebvAMm2EALaXlF\nZJYE2zPY3mX69VuuEVKSseaBMxkEcrYs8tMWu3X4o7vN0oFgXzG+UfQaAyHU\nd0HPKdipKN92af3uEgJXpJa9l6zG38pZVnuKLO8UE3oA/otS6IHvI8MHobds\nlzk0+w/CfFm/QHr553zNYly+2MFrEW8UF9sb8gSvGKnL3I3b1zW88JCIY2GH\ngUBcqxyEjBxjaZI0XIgygRW6zXPMUcw8H5oT3CWEj56/c5QBFa+hcSeecSgm\nSc2ETFoXId4yOUiaOaQKFo0lQ9a/ubITEKcoLNY9vJRQBx21/9qwh+Vx95Pf\nCYlR4NtYM+eEH4qwjvoExndk4CEVVehR41ATMv8V8ANP+ptpDjnvJ0oofrXa\np8pLC8dcCikXaXhW/qj0J+DlGU5HIbfWPWyxz/bmZcLE4T760FO780Nllmlp\npDxzK4fYpdps4DszaN2jZlpjHhyMbehCCJcwq4kC4esWcsNMR44ncC43X0Ll\nZHd7a8vejey7mrsjvAKZ5kJdKq3DmeeHgXjgLv7VcVfPqN0k2qliOuWgzg7o\nIYFuOrdjSc9A2t8CxDVnYWfVuOW/6Jm6t9fnyM8xTOxDKvYoFfynsiRZKBCS\nWRFRSRXTLAvcQRYHEHtq4uRTHT/NF+VH1SwVzawzUeCvolrQYi/ubc2IFuaP\nethF\r\n=t1Di\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIEEgTCWuNJ6S80uWDxTG5ezO/YjisJy7asqa/vbvxTJ6AiBpmn7JT/phfWaPHXUPIjQLOHHeGGSbOpgKhVsVOYHlHQ=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.9_1596930683924_0.17964665112433886"},"_hasShrinkwrap":false},"0.8.10":{"version":"0.8.10","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"73c412b5809af4b64d247a3ebefeb0e7c50bae9d","_id":"@synvox/core@0.8.10","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-xnSVZLO9JqyMhzVJXgoXP4cg5EcFZGU3Duh/X+muMfZzc3WC2mbqdo+dxy7AC/p8u4uKqbR4FmhkXtOQAWQ3gA==","shasum":"c265f94b4827f059e01ee52dfa6e1a16c3c4de3b","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.10.tgz","fileCount":15,"unpackedSize":593867,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfLzt+CRA9TVsSAnZWagAAcLIP/1HLYqga1dz+DUdqEfVi\nfIEC9mGcJncoJIxUPTGzQy3N+pnMz/r/7PRb+KaOCpK2QAG2U5+zJWbVT2Hc\n39e0bzhM81Hlov0bkc4/cIWCZk/o8vCGoz4HRKb0htl/DBgrt1SQ2lGPH2Y2\ncY6MMhK7F6PfQTzY8dPO0gwZJp3pYUt9JU/kZan4gEIxwPu0o4cKc2E7AKTz\nBzJDdq6YKJXsvIkdwwUfZdPWQUWMtmoGN2AtvnGfEz/YuYJL+IZVbUjm1zBX\nbIY6cnDjUwU6/vISQFNkQjp+dj96xK+di9CwEUXz2dV/1EF1eEDZJ3EsAAeL\nxuRmQLQb20I0DQMUpNA+1BUt0QTFAKNsRu7e56o06hwWNU7W5FjivCIw2rPI\nUAhgc7Qd2BnZ+ayyktRCSN0pyoEdhENkwZJyPGMe/ZJKuyDSBY8Y3P+Sy8ln\nAEmUZawqivAgAYxKsmZU+QksUNqWOjN/4ourgjAJMCdz73FB2x6uaTNNQRya\nDeNjRARheYgn1toMlBkuN8A0gNsjRTQxlOdsDkZenrheLVIB+6I/i3irF/N9\nIe/ws/EwoEeTQqyoWWQdkA98F7diucdAc+WE0mC7KquoTQuxtBUSI1gk91n9\nPLaeQsVLK9rkaa86Y2OtyslPAm8XGupcWMA6LdUQyf2MkKk9YER4ro9oxMi+\nG0xz\r\n=gLm6\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDZxTgGIB+S8mE7Q4jQPLoG2PXGt7sCqXpnq1Ay9cSFiAiEA3VyMxeh7TOFTzBqi9Mp7v4kjXT2xTFfeoeUEghuaH6w="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.10_1596930942022_0.2958677831533574"},"_hasShrinkwrap":false},"0.8.11":{"version":"0.8.11","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"ca25796cc54593ab1042fdd4dcb8213e1c725868","_id":"@synvox/core@0.8.11","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-I1GqXKWcNl1koJnxDyxMfORavupnMriTRKt/1DXHDaH7i/QBFAnSvFCU+cFKk5flv1xIbsY5EaOqBoxbD4Hpsg==","shasum":"427b74cb74776f1e947e90e349aef93bdeacf4a8","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.11.tgz","fileCount":15,"unpackedSize":606651,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfL2+yCRA9TVsSAnZWagAABXgP/0or8tKJ0x9LDyfrPMX+\nd1vetMHzQRJb9wXDscSjRGsq/TlbBR2HSGIsSJQPJuYTSYv/WW2gGepQzl8Z\nslmKS0tIm+rueYH4m00U69XRtIDXfzhOdpxSNKG8jLbItkx210ZvVlG5WEgC\nCI5NhnD6/UJBvdMOmaDqIQsLkngnRAz0Z5SqssFF2amfM5gjRInlyNgN79u4\n8Tb4tVRD1glgqVDs1yCQzeMv7X7MS6k0hL5z0wXkvoQpqQuI8eEWyu7CTkGn\nkuTiFFHCzLahJQ/4P7kj47+2/4kDl8Nv5RhWbGG6bzrBBZ6cF98URodfdqLX\nwNFSuQRIsvdC48tckg0QgwO4NTzFAP8AXZKNViVucA5QZQKXPTPdfziuV8+0\nSDXesIaF9V2dRL+TJx4ABfn7Fhezfj1hH9/QKIGK+/CudF3K3M9TV5dUAn6P\nUefsD/9kwvqJVvYgfWWcTMJv5qjY8s6+fWzoVRYPW4n6Odyd8v0KApy3ClRV\nxh9aRfUCWM/mFXoun5JMmXpmU7a6xPQ/5hvvJXYdjEVRah1LTs99AjtX3uCT\njDXR82laDEVNfCwoCuBNHf5wapjLRq52rEWsNpuD1li8qhg3hp93uiUglKvv\nAf2FJTqEi9E1LJ6l/Rg3jfZWyHWrkGsTgdyL8SGRXWEOS2Mwb13luUGu+uKL\nOAkq\r\n=acDZ\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDyS2ETiKnzxSoDasSL1gREVMcSisNLhEjR9aRdVin6/AIhANm+OjC/tzqOR/g/cOLVK0nqEHsqLmNXnW2lvlh58tQK"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.11_1596944305924_0.7871632657585848"},"_hasShrinkwrap":false},"0.8.12":{"version":"0.8.12","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"97efb450ccafaf8c6d0524eb37e491b9fa72bc77","_id":"@synvox/core@0.8.12","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-rrHvEwUp9znDmXE3u43MFfNE/M73EusV9jGSaDv6mt2UVDvlNFhWqN9mGBYMhuqPOUxJXYLASm+zu+d+7hCeOg==","shasum":"22bcb710bc9f858e49677876f6e2a7ac9825461a","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.12.tgz","fileCount":15,"unpackedSize":606813,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfL3PtCRA9TVsSAnZWagAAdNgP/1BcWNbKBipxOFKIFukj\nG+huOFMYV+cvB9AUWQBlfGTZg4n9YNBNO/bjPmheumkwrk1oJT69+XKLU84i\n+Oi6q+EMi6huuvbF9CprBjrayNA3oQXO1RPqS/T8wKCkl5ESabaNmycV6twm\nvuXPJ+8oHd0EB1WeRdbvGKyiyxI2MyenIurF+1lLn8/AlBoR3q1NRhaS5FdI\nFEN3pVIeIGprN/hTH6FuaesjRUVpr+v+UtPRtsHYPuGaKCXVISJOt4eHWZ+V\nMCX2kYH0LFaTEjZVQZhIxSZA6vYIUuYO6pO6UyxwV7PqSAuGv6Ja+xzYzSae\n+PpTFt7iQ6fIKZZOZCvjUZi3jm5t9nh+6MCSO8kjbnvH/5GJxCjf/LGfCfQz\nEhgVQtFTEsIbj7LVaXDGFfZm7h0h2B1EE6+cu5nZQUhA+3TzZysnDm1A39XM\n/LDcnXQMRAoaNAKmuRMnupLKfIJekp2eYWo9Cim5w/JBtZLG2pTuIMuwU935\nE00k5ttO2N72oKFr/bZ8nH3bi2i3vSu/RT0Myr2x7YJ9Bsvyy5gQfFeuzncr\nkQ846IkiFmbkZmbXbWBHOdNhm0FZuQT4HFAids2xyUvobNY9EOV38eFJupyO\nXA8FI/wLu34yip52cGu/RjS8TyK4AV2lPa3z7pppLmqjE5Z1XIqXgz8J/ODh\n82Fd\r\n=zNnm\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIHVic1sBNt3WlnAn30ezj2H6j7km2s0efcWnZ/mrV1EYAiEAsHqq0VmO/JhdN7wIOs+HxmYCcwj8vd58JXYwO3Psp7Q="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.12_1596945389411_0.9801260453662886"},"_hasShrinkwrap":false},"0.8.13":{"version":"0.8.13","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"80a78df8bae2eb1cc7b05e895bf2cbb1829ec70f","_id":"@synvox/core@0.8.13","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-qQ4ZhddS/R+3NmKC4sq6bWPwDAfwt6ISGpxqiZumty1FiAF1nVlpKB5fElNyc4FibmTMCjbpOvwl3CE1uDnzrg==","shasum":"659e917ad3af3ae974059c927cc59c89ca89e9b6","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.13.tgz","fileCount":15,"unpackedSize":618653,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfRwjBCRA9TVsSAnZWagAAUNkP/04VBcAbs99SwgzTYAvk\npuDi1s5RM3c079EEPnP+DKH47Brd3XIbbtcCh1YfvnBu8GZARTDDTj+iN003\nuMohPV5mBqFXRrIth6P+zIGSfn/YkVr4BYpU2/p0wlR4VNlJEvYEktTUmZ2h\nrxxLzd/P1TsTUZ0r8UuEpC4ObD7eaUif1e2znSkbOK2Np4ugjbRJIbJ2Jq/n\nBa2sj9AyliLTaSWTEaF62Bz3jTgDbC+XZfwXzMrJqzRKzI6ikj3IvtbPRFO+\nzCobPHEvkAHNTeBJdL5+8oznXBznugXfLyNAZmnMihJeWL/xFpKWaE5NDH1D\nRujcTVTSOGiIOerZ2t7BUrhqzV1Zcq0ZIsJ6TJAnn4N84So6jWS1OoeXd0fm\nIGTqjTBYRRIrQq0ZfhLkoJQ+fpqbVlgYGPH7NSUBpCr1BzmJYPXsfYF/euyA\nl5X+hy/NSyybOljuFkmeT10Er790HKIUQ7KVmsCVMm4E6xJgTwigPbt80UiL\nkfTavuJ5eXKuL2OJ9RCigkhwwvgYtKPx4b3NUDeL8sNq0+OKvFGR615ZxfrO\nxUPvI2aCfYBgQbpHkowUsvsD/DjS5GPOxvdhjHn9zGJK5xPg/ax/09RO2yLL\nG7x0TAWrhBiq0C71APlMR3ofmHYyV/EVxs4IRv+ZcWLBPPyFBHGgjl1pTA3S\n+pnH\r\n=oLPc\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIANmPZeSGJtAk39QKPBVzB+RS84akboEmoKkfOYftU+4AiEAu2VHq70vK8KZvX07MnmG3mJch8H19iidYVG4ADVtmYg="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.13_1598490817239_0.9602706577827345"},"_hasShrinkwrap":false},"0.8.14":{"version":"0.8.14","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"65c0044fb841dd385e49c525f52b3abcb54f2316","_id":"@synvox/core@0.8.14","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-Oxz1fWLJeyUku1WdKlmQITFEvzE4rmc++tXwoF1FBIJLpg9FpnecYMCSI6U1IrkHvYfzAcKIwk1nUAMDvsSiTg==","shasum":"9782822ac62642d87fbbfc3701731fd7a0f9e18d","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.8.14.tgz","fileCount":15,"unpackedSize":621803,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfRwzmCRA9TVsSAnZWagAABNAP/0c1OIOTKDA1GU5O1pgE\nu1/dfmYQA29U3t+4ME6i4DNHQf4pmhZPzVoursK7GxjXqmGFwHu7p+ch/NWv\ncnKbsgwJ2+uaexzf8+n2EbOddRcLqBrXNm4KKmbP9dO2OWFWmWF0CNY3Wlgf\nu3VmONPpMD6zsn0udwk4Ph7hR2QDvv/6mc49qtqR/tsqLMk+G5QaahBx70h8\n5zWVzMhmKpb5U0pBFVtHTJSuPkINmPH8AZiicp6t9fz6U/999Vrpnl7MBel7\nawbe4uzKct5924yaS04W3qWBbqYZ5iBFxpKKw8TngJ3Wo8n4KZqLK8cpVIGo\nKtqeyJJfZLqYFMgIXSGBWPrtjvBIM50SB9Ug5Y0+GywQd7sGrlIDyEk76Wfn\nm7gSi66+XFHpyo7BNxI/hcFZeTbcOLZMke/nQl0Sm5hLQTPU5BC0vNyj6yki\n1IH4vC+p7zgwoNhepbu6sLI3MKcHeI+OXIi3ZB3OfLYisZbYCGhBYQpa8/y8\nCd76jbSnIQFdRoof4pvn2ByjFSUaXknrt+jEF26NSeFFemkHZnzu3Seg5j0O\nyzytHNr92xpYH+YKKaxEoHJBaIlHdtVl0quulXnlNUE9gnWXRvmdHVX3MxA1\nrfjyUnl9RWslgJZG7b+epOv3XFCuLdO6vcCSWpIcdzxCrLgunuXhzUdKdJEd\nGu2W\r\n=oeu2\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCSkhkISqBwdWmlyJ3W3W1875tmXk+rhrl6CD1XMAw6MwIhAPv2ZLn2eZwtHOd0vNmdxGvk6X2Z//O3V9j8CpOsaHif"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.8.14_1598491878285_0.7403949168826751"},"_hasShrinkwrap":false},"0.9.0":{"version":"0.9.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"aws-sdk":"^2.751.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"f64545172f6449c5c811ecaada9e0a8f0608db43","_id":"@synvox/core@0.9.0","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-6JJGCdAbvlcv+1bcJXbY315EfqQCNSiR+BhPCcicazGUCDjXYMO/3+xq/EoS24WJjvE/1kI3Tk0yoh/wPsk+Sg==","shasum":"88025babffd1925b2605591aff4f5887527960e7","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.0.tgz","fileCount":16,"unpackedSize":640353,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfXoD/CRA9TVsSAnZWagAAHaQQAIBSLoX1QLzW0FlKNZf/\n9vTi9VYxyPWfHS/bW6phYTRGAB/jMxWIHJ3QYFHvTiIxqsjQDoosIg1/9fMd\n24U1n59jZaLIKhxdqzvGAHE0/EHiHDv4pyoGkIXxsJAcJJqF+9NIXVROUSu0\nkGlxBhnC4PqHh6gf62/0ACf5V2ln9Js9Cj9JrwwBH4jC6DoJvZwWKmZf0hxU\nUC3S1TmRpSoAzkwFjkadMdB+RliXkkACv8uMUo9/qt3NUsBu/2RzfWFPubLl\nepCQNj+FIRtYECZxnq7PBworBp/sbvffJGfgvCeCOYoP91Iiqb+PdInphPRJ\nQ2/y/o+7NvcY/22qZ4rZO52fMUsTQ2b40sFapmTJ1H+9tPWHZr6cpyzixZ3g\nxTK+BC8knznGUDFBIt/T7FsGT5na4WXJeV0CumEuKL/l3wkwqFqhHoSFoiNa\nMLkiWyBqyBAOO3dVrJadjWArWsJamUuW3kOlJcH19ner08f8EniF0u/R7uah\n9H394DbA1/wituNyMfld0ti7T9ouRPAN74wQr+ECfW4O8ZywQ/Jw4NARBi9G\ni0hgqyF5ceTLcWumifcnewSnLi93TWcK4AJytxzzzRulVD1W51ehfUgUArzK\nlRZlV0vh7/QJVaKaSZ6UOkal787fJmlmu01QkWPaef4GvSpjqncrJlRmlUw0\nywIu\r\n=jQ/m\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDYZ/czLhWN3t/lYfVOG7mo5ZOpnvzHB31CToWmpiqwVAiEAu97Q88/MDK3+0ZDOKIego6oW64XeWvXFOmf8r7D2+hg="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.0_1600028927041_0.930576684303901"},"_hasShrinkwrap":false},"0.9.1":{"version":"0.9.1","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"aws-sdk":"^2.751.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"90a0f336b47b73bc6d2dc2bed3f72deadd5efe0e","_id":"@synvox/core@0.9.1","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-DRW72h5mg0pDvE2uYXorq3Ah+pHLR7/JYXNl1/JM3n248P98tFJS7GOC0urg97Dao2qyWdi/bZPEHgD9EkfsWw==","shasum":"fe561fa359e5135e7c7411ef575252571451fda2","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.1.tgz","fileCount":16,"unpackedSize":650501,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfatxLCRA9TVsSAnZWagAAGyMQAInQu0koznD8pkPz0MPQ\nUzVXl47Q1O0DAzwtaReM9YdmHtGyjAdTncG2ItrXlyi6jn57V9VaG9HI9uOd\n1ZsmANmHp8xWiPiwwXWOgdvuCh2S/swagv/6ZE7Hgbe8HM8QLdynaG43Pxbu\nfwC5iXSwuOGN9lVuHq+GRYtQNFXTY5PIfguPh4369geXb3V285hjorXVCQxj\novcbGHGPmZ5TopvBLBaUSWYFnMQaZzXa/Irb39fK3o3P6OPzRj3fSzcd7ndV\n874UVNJK5/+TuHrf9kE1TtFVP6PRCuRrKQdB+JMTrCuRxnmyyTmcb7A7MhYx\nJvCZMZvjDWALgz8axjD4yAEh4W7N9Sqj7GV5/Qw2Tx1+3qOInBnY5hd/J5JP\n9fyvRcucvcGY+euwZ0UWXmzpQaWAZChUynIWVRPkeriUD+kTB5Esc+l6ZAFs\nFeqCCHVPylDszKllF5GdGKn1gVTvSZtb6Wv1aYVg7cYZdORSEsuQ+XG7+vE9\nO4caZR7iPsSR5Wlr/2+Jvkc+KcWIf+h7fd3+IjYvkDlWx0LSK8koA9tIiNUd\nI9uk+M6/kRNBWD1EEImvf5gQF9GB58fxoAcDC7OZH0/+A0CQR2HXEq7xSFBr\nlgEctYO8MCmkx+4GN10vvlUKxiPJf2MXGN9GRTzi4vgD2R+A9oSmFThotbug\n3/Hs\r\n=0U1a\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAuWZzGfi5LFG64jpNF/thyI7VZCfkGlUts61mhlyYOcAiEA4wq8fhPPh43OPHVj5YqhwJeE8T3yqvyK/xFO8PgwF6c="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.1_1600838730534_0.3102469938421839"},"_hasShrinkwrap":false},"0.9.2":{"version":"0.9.2","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"aws-sdk":"^2.751.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"3e3b3aa8847e77753c62bd4010434890aa966908","_id":"@synvox/core@0.9.2","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-P71tYNw2GLEiWAwLLpnolWv+0KINX6nYKjP1vCbqWNZSrEv9/6VaMJ1aY0blEyMxMLru5TaNwx2ZTP9DgIEssw==","shasum":"0c6621e34f41538ec8d00738c8e51a31ee324fee","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.2.tgz","fileCount":16,"unpackedSize":655554,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfcBObCRA9TVsSAnZWagAAXQMP/1Rczs4Z0qWbmGIxWKdE\nraKPdRYgVlFi24mAVBj43Lh7AEMIoYD8VbuCTd1ya/ipo6BD0OpJEtAvxo6n\nUg0mXKgnzCcKTwexy4bt822VPeZCEk9cwJTUbT/EyCKLM2SUDx0Ho0u8t/hz\nfr56wmvHWJ/Xtp41cLvKTeOuULp2cw622fUPR41YmbNwZ9aV6VO3wNVbKsFH\nyqFiWIJ/2Y5aIiwj6s042GYDONJtcj54LG5kDRIY0qN9X0OYmYLbAtjnUz33\n87KAl5rXkavVJYHaGyU25eeAzeokp1D2CRiuGEADU8lt1tozJjpsiHTw7p8C\nIDQf3MKJo/5lKGFpUu3hbefxel3Ud5QMSdNmvZdOXcO7Sremm8Jg/ZDelFIV\nqLiFFdfWlXYs+pehy/y4rQh7pToXACEbJdKJ+nWvglnSSleuVKBUIng9eotE\nTej8IkbCGzIjcp3xD2vpjVecJGjBepM5naqG2d8mgNp+xyhXqFIU9XtIuDAz\n+zUTksYJsvYh328A0XIrt9B6YCNG3zjf5+0hPrumGqYuOVmte1jfnnLyVyL6\nmOXO63eTGM/Nrd6mG80QJeFFxvj3hY1eg4oV8fyrQ93SP06v+se0iYbb3c8/\nGicuKmitlrmasTKy2pD/Pk/Yo2X6HVB8CMgwMbP0JD+zFE9GqO+EvlezSfq1\nQ8mP\r\n=5Ovx\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCICJgpAnb37+2gLufaf8L8FBudzZtpkYt3UTLvQ3E5YrGAiBnbOu3X1Aw7FWkiHBivT986jYTVgCiYuw112ESv+kOXA=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.2_1601180570570_0.3562506589051675"},"_hasShrinkwrap":false},"0.9.3":{"version":"0.9.3","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"aws-sdk":"^2.751.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"f2a9f17e76a1ffb4cb4a09b6861ea0326571e29f","_id":"@synvox/core@0.9.3","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-AibO317cPRbiriek513a94xFEOc63GeiVuJuhZK0Y7BEPB46Hb89+HwuZ2MVGatePw4xxX6sK3CJbqaql+6t8Q==","shasum":"891eb6e14d27fb2648142a1b8c45dc08380f0929","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.3.tgz","fileCount":16,"unpackedSize":663612,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfcpY8CRA9TVsSAnZWagAAD3oQAJU+wIik0Pq1pTWOhuB/\nU7NmNMMAJr68yTgrdL55pwWcPpsE/Ze/aDvFAO/sb/lSCkrT5FSFfHC9CZcz\nIIgf4AbeSEUMcGPrzlttt7W/LMS1EfZD6JODI91F+nyMJLW2btESYArjkYcb\nG4byPK+g9Lw+O/shJuHfV7UXDQe80hxY4n97xLAWDQxWT5wK6d/QOuxAbCr+\nmU+Hf/CBbxoY52sxOViGjMQ1kRkkVl413+d5zyTfG+lPFs3dpb5UXFkankFK\nyr2VBi4TFm+k1oOcT/DyCY4RoKHKsx0GmyuYYIMHOYV5LZkTKHcHQbxhq2WS\nafB5MMJBiP3FFAhAPr88Wihq7/lL860iqGHzHPMMWRHQ28W8OafqPSDsmg5n\nUr5C51UPZ5Cp1u9wz8zM7HwnZGjIvBKxP15PW7sBg2DQsgc7tDjBPJ9yCoMX\nV17MUv4GYPQ+H4gV6Hf7fJxxBt2tNVCMth0bS/rH2ksUxmn0ZuDn2r4Z/qME\n1CEwcIoyICeKAfPH6IyJ+tD1MS9yrXz/daFaqnkTn4vLs98SWO3xDof9bqR2\n0b7nFJRh/i8dwUwK/tEib7MUqT/V+7qfPoCJd6xq6ePeWdyAaBVjYUgTHzb/\ni7bDQsZODVGUMvYgnByi+S+aaWB8wJGLgdiZzo3/wgQ1JOpQt/JBKA894Bz9\nctOh\r\n=Whsm\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIQCyRL/0uzF3x4zceH7kc0EoT5XMDwnzFxpQ7BNoomeNSAIfTjIqitddw+vTg1FZIvjELuNdwSanJvGRknDK96XaJw=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.3_1601345083698_0.32016326030135933"},"_hasShrinkwrap":false},"0.9.4":{"version":"0.9.4","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"aws-sdk":"^2.751.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"366fe668cc9cfcd8e088cf178c34509eceab7e54","_id":"@synvox/core@0.9.4","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-Mv0lvRgOJzoyShIpu6bEE6ZiZAUYOXtMXLG1JMc/4NlRh4KbceITCpjJax+xqcf3MY8loCG+WIiVhdDRU7IO6g==","shasum":"2ab51f723fed9ce1f77af7b9d4ce3bc3c538e63c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.4.tgz","fileCount":16,"unpackedSize":664588,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfcrwDCRA9TVsSAnZWagAAzvUP/0L0TBHWc9fTRL0QsPpj\nZZT0hyCDnsOdgAQZp5FP/XwxIkRF4w/IKS+Fn8MbKbl57ru8jRZFHo+HdBcj\ncsJDgvtG7qhNCss4/4fzS8cAVKRo35Bd5/qWxgNlTMnTaF5pV6RXmUCP5nAz\n/xNypHId865i06NIgiv5307qm6PxEBrdgESf5N0Q5jFPHOYzcqZr5J2SHBCX\nTArmiZRzONR0LxO6N/TDsUju30YHcnep3SzQG681pSHve6i5ISqVi228UF2m\nH/Ui8qnyY9YNfNrzBQQtJlEFY7vsJjIVLvaB35guy2VF7Kvr3PO9s79XurrC\n/tlJrdq4sCjLzqiEsB8XNKQOV6bNNfXLs5dQX7VEXLGNkgAcHiacZMkTGxkT\nbFIns0g6PY+VCTzc1Xa+GXDl3bYZmXC0y0AgYXxdGo0NqUh+7X5vIxXmAIPb\nyHboXbGZIlfn233fdsv9oCUViluGm3P9zxZoMyeiBs9iP8ac0HLRhMffHqEv\n3s00Kdz7rb1BtpvXQenBRhhsGG80WFbSD0+ey3DF0ZuCTnReFTrkIRjbX1PW\nMv6nn2gDy/G9SOAZ3tabMjAhhvOvvJFIqiU789/ho7jm8VHHkK1VcjISRRjt\nhOb82tlv8dOxxpAZWCK0Iosnt1IrdSfODSKbw+KnOoP5Va7J1dXqAgsgECGn\nVBW8\r\n=ebzp\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQD4Jxn9dAs6W2u2Td1nycoFk5hibocIJF+dYiVglRNfbgIgDZaWpeSJKl8WxmEVtE641G82yu+pO3Nd8mX9SB3GLiY="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.4_1601354755544_0.1608447412367735"},"_hasShrinkwrap":false},"0.9.5":{"version":"0.9.5","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"aws-sdk":"^2.751.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"b9ab746f86d86786846d56d1ae7b208a11c1033c","_id":"@synvox/core@0.9.5","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-qPTcD3ZfHTTHEz93jwZ2w8zdj0XnMYVVPu9EL7osp3a9FtSGzSz7lxwvtR8b+cpF6tfAwJ+58aeY4v1r001CJQ==","shasum":"5236493555c9ca08e1a7af80c6e49b27f8c5bbb2","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.5.tgz","fileCount":16,"unpackedSize":665617,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfcr+oCRA9TVsSAnZWagAAJ0EP/iGaI5db5y3v7ovk1wMK\n55nMQ2cekiocMTTRKRskH99ku65t8u5GJ0OjGR+aII2v+3PnchdL8EQr3i7B\ngJWKL8GlROPlWY9o/AjRRGWxvXZg/Zp/zbFNr0hdoYOiizza2bYbqy6FWY0F\nNkbRUbx3+vceT0IYtUmCQdsq0tKOA8re7IzuKdtp4842tMq05jMyljYBmphX\nnpPgbrER/nAY7kt6OBtlcKIDU3TdZiJ7A9A1zydwc91rJKFjTlFKgWnxeoQO\nlzOtC9iSE93cQGufn0DlZTOVodn9BczAMz60aDEpIt4B2hBpXs1EGyzeVaG/\nSm1fk7PNcYcJtiKZ1B/S6h6ajwnTNtGt9vba2SVgYd/igXxVXEhbMcCvaS3O\nlDHm+4iJ9TUW8YIcb77xpkkhAT6B69Y97Wn7TBiEzqDb9FDqVQJ7nOVhy1cc\nmXXMkQwcr3r3s6sBw1swqykk1x5Jn1wrNUXJzn+3UKRDzjufDzt0WWNWN5GE\nYq0gBCKOHO2EyhvNPjSLcoCHmV87z6Zho/xPRfNIaOcbil5buJslUj0wDf3E\n5j8I8pB9L4EgLgBpecmdj/h7rQISndF9Xa3n5KVnEnM+OdB5PKu3pfiMcmg4\ndKZL/qtpgUhQTd0/gsSeg65nHXDTU1bgQda7/ZsrkjQMZF3bKmKnfgzB/ovP\nDUeh\r\n=8Qa2\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIHekBZ0dAWeMhPZR2VRRFPWvQRcxUAXP61hTzmL0Ru3lAiB71GIeOFio4e+7tk7p9qGsOndmksW2sfFD+rDYecFrtw=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.5_1601355688203_0.6150037195164402"},"_hasShrinkwrap":false},"0.9.6":{"version":"0.9.6","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"aws-sdk":"^2.751.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"7dbfd7331cb9e4e888b5ad5107fc9215211a193c","_id":"@synvox/core@0.9.6","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-9lXwxZB1h8z3g2ExUV19tAH6fBsr8hdiwr7K6uE92ngQwxLJh7VyfiFSapFJ5n4UIraW+hou4CCL6beLzrs26A==","shasum":"ca4a393c3c8a99a3b0330fb92a36336709e2dbf2","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.6.tgz","fileCount":16,"unpackedSize":665902,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfe+aHCRA9TVsSAnZWagAAaLIQAIgTBfQbqU8K8gGCHLDB\nd6hlUC/1ztsDzsWwV5/e1p7MUVLs4aX/+oBRTNO0eebMVc2UVGNCvn+FhuB7\nW9rNmlq3lWsSAUCRkxN3BodQ/XISG2TsARwuggkeHt6JUjOeOSUzGFzQak6O\nWp1Gkdsm4kDg2hCJ/UK89/B2vSpYZ2yD5zOzN/P8kfW1b59kT5NwHSxDjfjH\ng/9zpaQPZBaE/Rc9fqtnn+gm13YqkIg836Ye4SllWUsQtLdMxf2U5J8SCcyt\nIryAEh/0M3w03+eNODwWTmaxku/eMI5z4xP7ehHuvjadzJAflqKmy02Qh/9P\nfm7Ddbv2xf4YUIn+r+MDielOldzh49Pzu2mpkDaC1YioxjXnlbVKWyP+psL/\nmtRnT1tVS5wdHzbhtLcLG2pRVSkxqfBglL1+me07aHfA+eLezwrRN8tSpBZD\noQBWotEe/VSkmkyhAHrd0h1yAW4NeDA0Y8iCDECUh5NYMTs8fpU5YbiVQCrB\nYDREQ46qSdHus/W5DOMjNU8Bm7DRiTPgQgvH9b3007N0AWnfg5byT7CKZlNq\nu2nYeYpinWrhHVR3p17/T5eXU5ghrvhw/LaKewHpz1xWkFTPW7PTIf3HrXJx\nNzgfi/RchgqHwLA0766mm1MgchBKbM+oqGMNiBlzn3fHrREZQ1v4aHVLRE2R\nqUcE\r\n=0pKk\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQChjTDHpk1AIF10qCQNtfG1S1RtSWPdpbkVDGBZzoTp0AIgdZ7wpY9Bx126iT8c6Uyg51yriiIvulQypHuUbfBGGn0="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.6_1601955462585_0.5448709658488111"},"_hasShrinkwrap":false},"0.9.7":{"version":"0.9.7","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"aws-sdk":"^2.751.0","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"dc1cee496e51e732101aa0fa247774b8f3a76699","_id":"@synvox/core@0.9.7","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-a83XN+IjLZcfQ3iaNPy/0JVeDmqO4askA/pCJ1wqQlXx7rkjn7X2hyGrBkMXHAelOtQ17u6HZgz3gPO6XGOktw==","shasum":"462be2e97b1441dbd4078e72a6e85eec84825cfa","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.7.tgz","fileCount":16,"unpackedSize":671370,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJffTApCRA9TVsSAnZWagAABsUP/39Stjm5dtYId+osHFxc\nkGFB2MzyHE6jXhuPAgJDPh5e5o+dSt18jRlY+J7aMz4hJRRAeoZg4oACPLTR\nUF7Y/m1lzGoasuHTA+ZM155bjinZckqvBNDuh0kDmsWHyH5xeHZXWznEr0AS\nQsJLoXi6vc8P7nZ1rb7IQ4EhQR8DGOF1wVTNfPQY5NaVNoX87tR5KTmFj3Db\n4omnE8yYVWg986gFQ/mEh31c3mTi/H0r0tn/EuVA61AOB4q12yNWJ0UmIKDg\n2xw4WicJT8HH9IZ0IstLHJPQ7J/gBiI5ZkHxuHjAG/oH/L5q0cChr7jwhc9x\nW2aKZRw72sEVhTLSn7vi1CmEihmfK4QjQNFvMll1ZZFH+Gx5+7irA2do8kCZ\n6miHGU5wc4cl6dkyLgUWndbte72D9dgggZGzETcMXZIrpR5jFzRUzK0u1zDF\n1Qz6o0VtehX64VFmYCHKz9Cn2F6a4ngqE7EKvjwmf/ToxRVpDCZ6nwwPzBgs\nORocWhk/tQm11p2DY46GMrGasU/ExCcA9SvADfxDow07gddyg4KRWpWXr2BB\nvgIWm17GAp6noVKMrojCdXY/UGKUE/1AipqyridCltJPwu8oLrdF99LXpb4Z\nb/S77ladF+Oo4NN53uCfz01kfYgjeFCzWFCMws3PTo1NL/Gp2hTiH6WA7+yH\ny31m\r\n=nlpU\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIFHLDAWtwUY2LMpr0lY/8ftHfntPd2j9RRJLseEY76QKAiB+zNI/LX1x+8EbbDCuFPlhiuRNAwzK0qcfvXjCzqFh9Q=="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.7_1602039848975_0.7211275549696676"},"_hasShrinkwrap":false},"0.9.8":{"version":"0.9.8","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.751.0","btoa":"^1.2.1","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"fabfc2c3f9af098a79ad6b42bc48a5aacdc760a5","_id":"@synvox/core@0.9.8","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-2OyuYjVGlS0DVkhD07P+TcpPfCe7SwoqgoDsi2ubqWVYDrPxJBTsSn6QDv+5Uikva30a8LqZTXveK5FZDFz+nw==","shasum":"60bd7df14a4ac162071b392ee3a4cd0b83c4040c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.8.tgz","fileCount":16,"unpackedSize":671808,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfglfTCRA9TVsSAnZWagAA13EP/0plTfim6Coa3eLeDWnY\nC4DgP/5+D48GGizSZHTQCBK4Azt2BVJSvoKeih214HuTae4gDx7JTkpVs4SH\nS8is+eb9vri11j0DyrhsNBhg0g2Pw53+kYukFHAzDs7CRNMveCnBPykzEPu3\nGbB8QIQY2/39p964GQiOmOxyvUrBWThZ86w4XWeODZigrUdcS8bHBWkOQN1I\nd7r1F8dRwDYg0R94Eh+/4tCnqQ9Njok+uXv2fGnbGjumOrO/w0lpt1HsevGz\n5LMDxszmry/Z+d+DVrHTNKkUMJXtRCkZsMJPaT8Kh9sqn91TQOi/tZKaWUjV\nRiz1763yWH2oZydgH5rAs8UMD4hNwbV6ySDe8I07g5KRL9/asua9gUavgS4v\ncG8nF+Y7VIqd8LoUXVVm5Ga4fNGNuI9BfWU4SlhkgtFmpbGHEvZRBrMRgzRN\nUXfsQaolS+Z8/MhC8iB/xtPnIoIj+LI9J+LponRdBo+zEai10vEdDJUrbxkn\nG5dZ/axtPU1Vawmq+QY4EcVF+eHWoWZ866Zsp/hg9AP5OuuhorY4GE8R2hhv\n6+ONNHDXLAEdTIm5lAZ6ilY48/bWGda2J8SiuS7rLkgvdomh+o8DqNOpOjr2\nB+Pwfkc60Tec/FGhs5e935qzbG/dAkmJynPbqzXtW+9c8gpNH/dLM+Zo3yaN\nxttf\r\n=RoZO\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCHE385Qh0tuWwysyInwyX7/xyVUYulnuVFEV1wuR5utwIhAMvAxP0A+Zo/PKHnBirK6sMCK60Mu3HducpXT61/P58W"}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.8_1602377682847_0.5832442315165511"},"_hasShrinkwrap":false},"0.9.9":{"version":"0.9.9","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.751.0","btoa":"^1.2.1","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"3c0159a5553bfdb0e4db7cc7604fe341e34adca3","_id":"@synvox/core@0.9.9","_nodeVersion":"14.0.0","_npmVersion":"6.14.7","dist":{"integrity":"sha512-FBW5ZH6vz+CrftHUtAKsWkISDHvcnGFBmdoibO289sXAsyTNDzyFgcqS5iN52YV8jIqO2Bm/wLYKo67uaVTqMQ==","shasum":"1438ddcd88ae5088abc4e4c61da3f99fa46cd045","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.9.tgz","fileCount":16,"unpackedSize":674470,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfljK9CRA9TVsSAnZWagAAlOwP/AyB8WuTJ9jJ0EB4oo6N\nnnymUg7v3G8P0B0zl7GbKGYs+dWGKnfOHbKl14t+GKlXO4NZ4q1sgVuyTBO3\njG4Nyp1Vbqy/bYbyYCq6yNiZ5hLW2Jr2m+VD/uz7Rqi+Tw3PtUgPXOOuWeeu\nhv7O45cs5kMb0g6iG0qvc+ngDALNR3usKEQxgRLYlrqc/aQfMELPtKfbN1yM\nKOr2AiJIO/TLfGjbmtigvIXZSlkysYIviT9613u9bjQe64jAePaewpV4uHor\noqejlFyoN+qMtEsGTHFfBINvrrXRa4i+YraEBc0Y5tLzZ5PqskWZfpI6SxTq\n9JZA3pHLibA5dUSUZ3OeugFowR5dY7FOsbEMmD6mp9HDnN6rG+KtZC37sF9R\nUXbAs6h74qGbS0Mu5efvBXgvrNPoDrd8TL5ke4y93U0Cxt9g9fbocUZnsHxR\naM3h/H5y9EZC5+uLYVAdTVIHCbGOBJyDdkq6CsJZpp2pzKuN51OcpPbKA+SE\n97oHJH2j/h10ixB97R6QN+yjIa/d/SpyB4b7IErwFcCX13KrOfOubPored34\nF1j83OP4WHnnkcaWI1LVEu1xmfGpGB8Y5BjliTclya6nUJ/6bg7qX8Dtvb8E\nwaNFnTKNgL2Uelc7G//RwnJELl40lLFiFFU/tjQaI8+dx6X9TCIfoVIz5EIx\n8bil\r\n=2daQ\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCICqhC9WrExH0EwXlIwbrtqh5ErxaE/pzZjMcy44MYlJbAiEA+z8OCiJWVvw5wSjM0MaDVWwvnfuhbtd/hDKOECvkgnY="}]},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.9_1603678909195_0.6732321742423775"},"_hasShrinkwrap":false},"0.9.10":{"version":"0.9.10","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.751.0","btoa":"^1.2.1","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"4f749d0b96d0cb6fc3a5164ec0724a06cd9c7e09","_id":"@synvox/core@0.9.10","_nodeVersion":"15.1.0","_npmVersion":"7.0.8","dist":{"integrity":"sha512-yieshZdSq9LprTCXW3X4e4t/jU8U7jc6S+EUMXuWctUs9XUNRUk6FaGkdep8SkBO7rRHaDflj4En/XqQcuDk3Q==","shasum":"ec0b4b0b2c12495071a597eb929555b65e09f03d","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.10.tgz","fileCount":16,"unpackedSize":674529,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfsMXRCRA9TVsSAnZWagAARYsP/i807ERjub+9MY1P5r0u\nvfG6YrtPruwGWttZObfqayLWmF4bOvmONT3au6NPEQw6h9PFdzw+tAizgKQ0\nzDUoagOTJpCd0HUnbDwmhGScMHi9WEh0KIwddbHCKrbNzACDlPtMd1zH3F37\nvsdZY9F/DcwgZ77yEX+yVmNlegCYJpwR76sxkLPqAukOAB3Dkjd9ZjDo6bTB\nAiyJL6l2eoctRJXTb13onu/L1Rp/OfPNUazq8MlhoVYe3Des8qp7U23qOM8b\nvalMB7Epy94cTYc+8hh6V6S9fKmaVHyjeOOVx+M7X6Q6uS/FRUkkRFtg3k5/\nDQJzzx6OGgGuPgoaKMD7IHE/meRAlpX5pLk8nGD45yWoT/fuJX9uodxOAr8G\nd4EHgzPMZ4SIm8sTQD3Qwu+1pJXbEC0XEeexS8YRmhe8xU9rFwB1Dsi359nH\nvc3/gWbR5voM0WIT8U5PlM6ZULxVW9EIBY3fkXsQcYQGeviavcBQvCuqxDwD\nAaZIfwz8GmlOFw3dnJkOtNGvWq34skREzK8WccVIFMrNshRno7j8jk1qEOmO\nvOERcGPslJHyEc9HeC/97P0EIwFHVEJk6eFUfQIrLmxEX31Y9lixGqngxEdV\nATyZsWx3L0xDJGC2PU1MDwm2UOStb7c0CcEvyk02PeL0rHffL3Lk9iiTYm2+\nKCdM\r\n=i6B9\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDL/pcxaUoUJGymO1Lx02ykYfwP0IBoo0gRXWDhd+Iv/gIhAJSn/VtA1h3xKdB+PLP5tkZ1ydpyG/AKbnT30y7BHXOj"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.10_1605420497299_0.35484154302683235"},"_hasShrinkwrap":false},"0.9.11":{"version":"0.9.11","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.751.0","btoa":"^1.2.1","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"2ea04c2c3d7237a020933373b517c8772699f850","_id":"@synvox/core@0.9.11","_nodeVersion":"15.1.0","_npmVersion":"7.0.8","dist":{"integrity":"sha512-9jmhdrmSob2RqU47i7frHaUsr3ScA2GQMzmQnaVK++/JO+MtGhgz2usgwt05KYhsAu1lZp94HuLHdwOFnfK0WA==","shasum":"89c4967bc5265b4cce63c722066bd58bde90eaf1","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.9.11.tgz","fileCount":16,"unpackedSize":675177,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfsXeeCRA9TVsSAnZWagAAzCsQAIEyTM7RF6mDiQoshmb9\n4lIqfbGOUeD9csaOYxI8IZmP2eTH742dZfLbfO9hNbImh99tMXc6V2byJehu\n0K8FiNtAcw10fzmfrJgtBA7BXBz6H1BUJTQNbJDBHG4OtcQVxkZPcxdEdqVO\niHDPASzRkt/0AWZ4YunkFBf3czG4RFuHEMt8nzLwGa2aN/ypVSZxt/AbbX7i\ndvznkxyP+AcWrLGQPMD9AzkLQArerMIbJRTdxrNHGfmKkRn9H8rYRGexmKFT\nft3CtF0v3tYSznjfZGEnHC8kg4BTtZI0kQdW7yzSouOoelMmKKwkdJoUw3TY\nvUeiXJB5vJpL8KMfGmEx3HWaddKZUM1D9DrfU72dnUsj1jVr7b8CdNlDgfYA\ntw2hIl9RBlzTmckP0Qd8D7Bepa2SZfgZ8YXFJuMOQ/3guTNNNes8G6SUXL7a\nRP/fe4xC5hwgYDb404HO7BpsEy+yMO+zBybbuTV9P0Vk35bkW9IN3H4bsiGl\n+9fjqoObrbdrWXm+HntGIbyJ8fCyz+JrEEXOo0PAZuZ0EDfq7OMrdRRziDtZ\nqWOFkZuedlT1TnGOdHalZGxnXX+HTk429LYYcZ84LHk/GH6HuqDn3OGbCWu6\nECnLoTlGws7+CywPv0rEQx+glbu4GbVJo8ZKJQhGQvTmceRnH0N+IxopRkrr\n/F53\r\n=c5VL\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCjEBwdR1/bN940NITuo/d+GiTa8N+5S9oUEuLBWw0biQIgcBfXu0Hf5VBpcQR/1aN15chLvb5ZLVb9cSxJRI8m6Q0="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.9.11_1605466013801_0.01116015771453771"},"_hasShrinkwrap":false},"0.10.0":{"version":"0.10.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.751.0","btoa":"^1.2.1","express":"~4.16.1","inflection":"^1.12.0","knex":"^0.21.2","pg":"^8.2.1","qs":"^6.9.1","set-value":"^3.0.1","yup":"^0.28.0"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.3.3","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.6","@types/debug":"^4.1.5","@types/eventsource":"^1.1.2","@types/inflection":"^1.5.28","@types/jest":"^24.0.25","@types/morgan":"^1.7.37","@types/ms":"^0.7.31","@types/pg":"^7.14.0","@types/qs":"^6.9.0","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.26.27","axios":"^0.19.0","eventsource":"^1.0.7","husky":"^3.1.0","test-listen":"^1.1.0","tsdx":"^0.13.2","tslib":"^1.10.0","typescript":"^3.7.4"},"gitHead":"eca2195b3631e729a4e497e6234a3488ebda6ad0","_id":"@synvox/core@0.10.0","_nodeVersion":"15.1.0","_npmVersion":"7.0.8","dist":{"integrity":"sha512-QZirGQRGS+fx6+nH6176juJyGycEZAeHNMCncRUARco6x+B8ppLhO2bWNtIqTcIuFllQGv7rsxIGMsOrnpaqpg==","shasum":"c1a41f35cd27e922a2beea62b19d1a773589e344","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.10.0.tgz","fileCount":17,"unpackedSize":682310,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJf4aTXCRA9TVsSAnZWagAA2VcP+gOuSd7ML7JK/EbJZnOa\nxWEZbfvHClD2wvS06+3B0iY3/kikMeYe/rUYRXh5ya7s/PIIzymHMWuWANEr\nrQBx/cKeKnuLQUh9bCt+R+TNPH82MUXjR/v0B5k4PieQ8U4g2y5AySuZZ7b1\nt5GddKS1uKavgvpIxFBGk9tBOnjLicd7xCSGKUh31Maewv4IUlYw+j+H7DZT\nq+UPPFEs49UL6blj2L7WxTbtlo2PglT+f6LR/vjxeNp3toIHEcvJrKmjs9sA\n+BBjAve31O4KcCeTz51vvDFi/ncDFOyZxEke4QHztYwF4WyYZKte7VhloIUq\nkzeMcTOkBbY7xIyAvF40EYxAguMbo4zDgWvVqd3DvbYIEsOd++QzgsX5Axm3\nLmed2anTiiZinXBKBq19AlXVWCL0rcYMibaIMrsND6nwICINyvELwIOfFnqX\nii+LBNKjOifQ1OWi5brrTFjf1LWN1r3CQ7LJhLX/RGiHpyFzUlRLJt5X/6Jf\nk71o5tGFVDjjL3joyJqMKmQoz9c+IFZeVm4trVpmaHS+3R3rVSQhJeOx1+ux\njK2DQX263xUCGbOOms7P4CukLzi+uf8Sk1/IIV4Wuzp16unHkUbSjMBn8AQA\nxJcGckrcSevoRrFot+nluB3gsVkOGp063Azi+kr03EgoqAwqL/Prc9fntUE/\n8r8M\r\n=PpxC\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIHNap471hBBX5x3IbMObjVOcuY7UOy640oA96PSSWwBoAiEAmtMJSbQhipD9qGkARY12AslYSRl5NWn5L3ngCJhM91w="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.10.0_1608623319081_0.46994383276969853"},"_hasShrinkwrap":false},"0.12.0":{"version":"0.12.0","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.820.0","btoa":"^1.2.1","express":"~4.17.1","inflection":"^1.12.0","knex":"^0.21.15","pg":"^8.5.1","qs":"^6.9.4","set-value":"^3.0.2","yup":"^0.32.8"},"peerDependencies":{},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@synvox/core","author":{"name":"Ryan Allred"},"module":"dist/core.esm.js","devDependencies":{"@types/cookie":"^0.4.0","@types/cookie-parser":"^1.4.2","@types/cors":"^2.8.9","@types/debug":"^4.1.5","@types/eventsource":"^1.1.5","@types/inflection":"^1.5.28","@types/jest":"^26.0.19","@types/morgan":"^1.9.2","@types/ms":"^0.7.31","@types/pg":"^7.14.7","@types/qs":"^6.9.5","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/yup":"^0.29.11","axios":"^0.21.1","eventsource":"^1.0.7","husky":"^4.3.6","test-listen":"^1.1.0","tsdx":"^0.14.1","tslib":"^2.0.3","typescript":"^4.1.3"},"jest":{"testEnvironment":"node"},"gitHead":"68b36f88297e6c88b192aea2c7d6a013cda658e6","_id":"@synvox/core@0.12.0","_nodeVersion":"15.5.0","_npmVersion":"7.3.0","dist":{"integrity":"sha512-hcLr76blg/bao0nm0KLFc97D/s7qMipzOqJCoRtvOU0T0sT3XdsfpM2y42G9tz6lVgQs1rru29y2FTZHNtfmJQ==","shasum":"3754598232ff5b1e900bb1b8e476366c3f8f9a6d","tarball":"https://registry.npmjs.org/@synvox/core/-/core-0.12.0.tgz","fileCount":17,"unpackedSize":962065,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJf8rHjCRA9TVsSAnZWagAAGtIP/jjY7q/8GEgLhvVWcopa\n7h//Iq5diMAMMqwXGeD4kGYrObnOvBAgsoaL2TFB/WpWhchrp8rb+MDGFHex\nhWtiFCx8aKZDy214E9N5ozkCNYX5ZwN5KA/GJbp4hpRHDZxGTKA6evcPfLOm\n3B2W3N7ghjXfuMfElIlmEjS5xu7h3s6ybOn0vmcAhl9xxOwCABUjKfKMHbAF\nJow15xcngoktcbcvlPIL0fZ0UPA/V6zuqjGWZezNpKMeOkLgNcdFrl9qiCmd\nTFhlscMh/wBDyaeLMb6w43nUoe/u7/qiiugz5GF6RI7HN+l1Gm/xoxDFwZ60\nnwFUbwhvmW7tL8+YofFwS4/5MlFMXnUxxRn3RbMd9OTFk+iyfetpNkIauQ/Z\nshD/btvJlYATEQ5I0687Lpkps+nOb1FZcBL76wi2Q++QysJDonJC+0ExlAPS\nzsaIKydwx8MJkpKAh75+egKWssxehpPpktzMGgHIq8m6TsOCbwoHdVol6q2Z\n9pGvCpCHlcF8rMf/Z6zTwra5qujYtW4BArBTaOr45idlbwLQFb3KIRHEAja5\n3tPS01yr0FlLgmrHNovWNigzKqrjzCZT+/Xvg4NUx/y3BDEzb+lATCJEooy/\nVaBsci87F71K9YD4qn/qs3JkYaiUpmRX36MnM55B1G9b888Sdo3KcefQVtXO\nB2mq\r\n=fqbG\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQD7Rqf+MltFMvhkom1jE26glCXaKGE9xP/jD/P4J67R3QIhAIz9w11msYZfd874+yzU1b2eafWVmV5s3oRHmWq12I0B"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_0.12.0_1609740770562_0.0037270147085268768"},"_hasShrinkwrap":false},"1.0.0-0":{"version":"1.0.0-0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"tsc","test":"jest"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@size-limit/preset-small-lib":"^4.10.1","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","jest":"^26.6.3","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","express":"^4.17.1","inflection":"^1.12.0","knex":"^0.95.2","qs":"^6.5.2","set-value":"^2.0.1","yup":"^0.32.9"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\n## Authentication\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity they are authenticating as.\n\nYou can use any type of authentication library here to populate the `context` object.\n\n## Authorization\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n## Querying\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"people\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nIf a client requests\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are avalible as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`.\n","readmeFilename":"README.md","gitHead":"cddb246c02f6722463e2152a8abce2b6186ac573","description":"Core is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.","_id":"@synvox/core@1.0.0-0","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-VU/uVIuOEXa5AT8OIv8bGnsStBPos/rjiyzGxQa2x3ZUIKHKF1pfTQkpAoYLQ7bWrbwg4UrHXA72GUTnJpjoNQ==","shasum":"be238f68b24d7f139be84f8fa73c6960208a48e3","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-0.tgz","fileCount":44,"unpackedSize":162685,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgVtiWCRA9TVsSAnZWagAAq48P/1eMfjUKXYcU8epaJlZi\neCiXvrKDhzGpuu97w50qI711a+JVExvN3AV2g4cIExXYtRpKix9OAnFtJJYM\nN5AyNNe4GLsjYpMj32h3Cny41oVngcsvONbG4wMosiGW5fQujzKXcBFFPhCz\ncHzyONddJYkiFs5HiKTNeouoaOGrFCHZpAiPCX3JRE3rV8HJy2cR0UAU7RFo\n6bOT7lfh3EP84D23BZY+2dbQ7a5K9vpHvRSta8xGRFLvpJp6hr+Ld6kSJUhX\nkcZWPhJuW+S2TkE1hZgaXMQtP6O6lBS+alBiKuNF5qR+/pnth6VODheyRcbt\nd65GMYYYsO8Cv2jZnoCrg45IDj83BwcyKwTk2vZA5ij2bH8SGM137V9XM2NE\nX4eH3PxNImdUgIFxwYP0LMcOabdJrICCUVON63WFiFOgRzlPREcmtBvgeJQq\nEtAq6TBlytSnzdZxM2OVj9tZaGQFsdcE+xClXPj1cyh6caRiQGq/ks0El6z0\npMZuWn7ehJmrR0atlHHzvF5Vudb/3qkNHVNnZdQL0SoM6p6yoHAML8CdUBCQ\nBTH0G1Oi8hpJQxJctJRxLMAm9pUMU/eUl0O/hiybme8wTG9cj2p/52Xd7TRc\nZ+Y4YXWrm4Ld/mdkgDUNu3MecHWYky/aC4Ns+vJ1mz7kLkIBFzvzRkTyrzaS\n2rZi\r\n=AVUG\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC48a4zQM/8dHPDw3zz4RRHYQmeVzVxXmBdJyVaUJqzpQIhALMqal11goac5vtxgSGxvT55GiBmlfsEffU4/eA+s4Ki"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-0_1616304278078_0.6512933013246662"},"_hasShrinkwrap":false},"1.0.0-1":{"version":"1.0.0-1","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"tsc","test":"jest"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@size-limit/preset-small-lib":"^4.10.1","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","jest":"^26.6.3","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","express":"^4.17.1","inflection":"^1.12.0","knex":"^0.95.2","qs":"^6.5.2","set-value":"^2.0.1","yup":"^0.32.9"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\n## Authentication\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity they are authenticating as.\n\nYou can use any type of authentication library here to populate the `context` object.\n\n## Authorization\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n## Querying\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"people\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nIf a client requests\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are avalible as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`.\n","readmeFilename":"README.md","gitHead":"348a252fc025511797c9fa29990d5d2fe81f799a","description":"Core is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.","_id":"@synvox/core@1.0.0-1","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-g5KNlyarq9c2rvjaY2tav/mYw7dGKaCXzq3Y8RJwxkIziH4XzsaGv5NphNAZ66C5My6VabeTG4Y2TSLwFin8qg==","shasum":"942ee147d217d3a4101b12a416c475f8ccb9adfd","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-1.tgz","fileCount":44,"unpackedSize":162685,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgV/+HCRA9TVsSAnZWagAAwaoP/3wF/DJNDbB57l+H3z4a\nPtXBTwIFhDx5aAf41XDVaRVYHIMFw9n704GGJ7SsighKOgJrbgu8Anl0dTyY\ndm+KeKOApONgy7y96NetwnfxCLgR+a45gn32Z5uUi6VtecJ45YzdSphE+ii9\nAmbCW1WVzawdNpEB8m77zxun9LP+X3rLdu27dPcWk4H5h+uCM1qzJbWssQoR\n5BcOdk7GY9zq+69k4OzeTgZ9bArQ+DvrF1errrnS/VyZckxEqRHvzsv06PR8\nXM1UlYH23r5XDOfaDKJvmD8LmFxRs8QRM1OQ2+84lfTBbHgyMeqAyB/Eoi8W\nj5tSynRk+4y4bN+AppVKkJxGroVnS0+mrmQB8lVDPhwTIQJs9t+yo2RkzErV\nhNtKijEUktnhUnxE7rScguCLOmMiwo5ukqpbkfejryN0Phb2iOyizyghFfQU\nGEtkO/64R/C0/2o2RuQu7wzyDOfN8O7pT8KwogqsJw6otOH/Oa0MNBHNydkB\nZ+klZu7YIze0cOhO7nujT8oLeSJ4YmQAYqJIIBqdVd3JORXtRGfdZOTFi6d+\nkStkQgx22kh7GDTs4IuHhhavrMXmie9ZB295h4CrqujuTkHYG15tCNxXWbC4\n3e07sD/YSGpgWPF3kc34GfeP2UEBtq7Kiwjc2uJRZiCvWMhg2qwnVigiRakY\nAcsi\r\n=YZqm\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIBbfUP2bJuxi6qU6yyEI+ik43sObu+7nkNaiWpl5OFY6AiBhIx/2xZAW22Z4Fm0n63w4tEgmFZHDfxzq2tXP/8RKyQ=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-1_1616379782839_0.5098982152404157"},"_hasShrinkwrap":false},"1.0.0-2":{"version":"1.0.0-2","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"tsc","test":"jest"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@size-limit/preset-small-lib":"^4.10.1","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","jest":"^26.6.3","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","express":"^4.17.1","inflection":"^1.12.0","knex":"^0.95.2","qs":"^6.5.2","set-value":"^2.0.1","yup":"^0.32.9"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\n## Authentication\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity they are authenticating as.\n\nYou can use any type of authentication library here to populate the `context` object.\n\n## Authorization\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n## Querying\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"people\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nIf a client requests\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are avalible as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`.\n","readmeFilename":"README.md","gitHead":"b533b4e543f0203761d799e4378c55a07c7d503a","description":"Core is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.","_id":"@synvox/core@1.0.0-2","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-RaayBls4Bq+kAj5jROg9RJFFCClZaICMAjuKkgjVT2mDlN4Vok5Dlcg/NYbgvvKH+XJB7pOFIv9utpdp8YSMWQ==","shasum":"552d51831017b7bab72dff3926c663fb294e5792","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-2.tgz","fileCount":44,"unpackedSize":162685,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgWAR3CRA9TVsSAnZWagAA94UP/1t0TiwZsJARGNkwW2Kn\noN27SP6dlGAErMPwYL/A+jRP8N9b16RWNVIuPIgnqzmunfPCDsQj6DTlj7yN\ntbT5DVdBlZEjTQazrlwfgBSzqP/Xn63vUGdioG/9GsybNlsHV/6XeloyMqQi\nsH1zb7OaoLu2Jbm0qgTTuEn0CdtACm8l+jbM+taadRDV9XITeG5blJ6erwOJ\nC5c0oVgVlLL1VdrwMUwuDDUmq4/AiEOd33p8e/ZvR5Jv9+xVWd8kImcAKaiu\nH/q2uLEHdWhk04zrp0ZtxuugXjC51+337u5Kqi+ahuOeu/BTuQds6Wd8Chbe\n3GzS0g0xAFgslAjJMjKXWlDaoXy/R98W8V/7am3vafUQyHt0fZ6/SbYNthRI\n/nsgkjWgfnvuXEeQBn6pQRaSF9uMkQTnvr43ednRA2bSKi8hj5LUIibLdD2q\nV9ZG59IAe+ZcyQBCcv+4I7DhqjtYTQYvYvFH6wfpF6oqsAD9921fL0jytEz8\nKxvgMHsCbxJfTVpFBBFGpeUI9nRppYtchsmPgNrrSjN0XkmDwJAscfJ6xtUl\npoZ5HTqs6J4U0Eq5wTCaSL/xEGgwmUZ63t6K+Nqc+S6H2lj/eXj48enK9b88\nh9kDsoEnhgsTC26YyCXP7TM5lOtdn1ND6EmlJeqIa+jZfX4PXwBRrDWueg6Z\nKkm9\r\n=JM0P\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBJAAJjYT2ZD1Oz9tVDBbW1huyP4uRZbD/OtNxh4yl1tAiEA2OF6LWNkqpBmXkAdiIopXZDKWqLaRyOBYkyzaNhprCE="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-2_1616381046752_0.6396956941427461"},"_hasShrinkwrap":false},"1.0.0-3":{"version":"1.0.0-3","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@size-limit/preset-small-lib":"^4.10.1","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","jest":"^26.6.3","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","express":"^4.17.1","inflection":"^1.12.0","knex":"^0.95.2","qs":"^6.5.2","set-value":"^2.0.1","yup":"^0.32.9"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\n## Authentication\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity they are authenticating as.\n\nYou can use any type of authentication library here to populate the `context` object.\n\n## Authorization\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n## Querying\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"people\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nIf a client requests\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are avalible as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`.\n","readmeFilename":"README.md","gitHead":"f32046a6f93cb8f7e729d7c088068594367a5bb4","description":"Core is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.","_id":"@synvox/core@1.0.0-3","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-fcHYC/0ZnrF0xJTXWFqUiZSgnxijlnhw2EBzGe0rrTnxUc5+LdC5F0zh1C7+aZluSSfX9XeA8vhpAgy7P52L7w==","shasum":"03e668e79dc941eeb551185df74af2b49e80bffe","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-3.tgz","fileCount":44,"unpackedSize":174546,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgWAamCRA9TVsSAnZWagAAwsUQAJdpNK77hsyUeY3+rljd\nMF6yi3y+gYmlZ9BQm5MK3Bup3F05Iv19IMngzHWR0D9+5rS1FTJPub3KviaT\njWGCQjOlJN2W2T9NCdYTjMRlpfDkHDd0TcJWv1w0xG4R/jzGvSjg95oPFXF+\nbUXLP2PgIMjctgJk09gc6ccPxK9Nyemhm8wD6BBrba+o4eyrywnQvE8AFbzv\nocjqW1TcoxUw7J72R44SGP3YSQTHAzXISQav7lHbreLSMDl+JplKem9D/ZuB\nsYYLRravGYSwwRZBrBS/wbZEm3qqWjuUUWwFAzRJprDMACmvCX1nRflVcR4W\nKdPFp/aYBdtBQ9jnPORje2TPrYO/j4CzK/t6mAlRbtd/MF1w9uycXeE+4dnG\nH/XHDXkQOKucboVOSxhXNcV9Xkk+nDcj9ck7RK37iZHvMGU1sOqFjRkNFNhe\nJwzBzlCPsCVPKx18Fw8fANR0b0NXzEMA60zbbq4YRi1/cXdPsBOB4uiGjx+P\nJPiMLifNKuRrhcvytdRe75Y/sXGjzLLXuwlmAb12c3lvZ2RcUCSSHxlrtjXt\nQsFE6bky2cO1LJz04SctidwXauUsG05vtGYcSKqYo5A8n0tqBRtNcQCJYIrR\nw6iKadvAKiF7uskaHczz/IQv2OhtZKToCYB1fzqV6zCwrmvbwZVk+BM8mzx+\n13CV\r\n=JqV7\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAhsE/VUw3zzWR45n2GocSX3uLvQj7fbw0DHErGRcMcFAiEAucU3Gvr6dyZ29owDPpYBzRdZVOxenirPeKeB3bvwFjY="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-3_1616381605731_0.3199299585225688"},"_hasShrinkwrap":false},"1.0.0-4":{"version":"1.0.0-4","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@size-limit/preset-small-lib":"^4.10.1","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","jest":"^26.6.3","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","express":"^4.17.1","inflection":"^1.12.0","knex":"^0.95.2","qs":"^6.5.2","set-value":"^2.0.1","yup":"^0.32.9"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\n## Authentication\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity they are authenticating as.\n\nYou can use any type of authentication library here to populate the `context` object.\n\n## Authorization\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n## Querying\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"people\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nIf a client requests\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are avalible as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`.\n","readmeFilename":"README.md","gitHead":"ea2c9b9b50fe4d9f7736c86254c682e8950fae2c","description":"Core is a middleware for `express` that creates restful endpoints automatically. In development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.","_id":"@synvox/core@1.0.0-4","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-Z3cLeDhWYurswVxQPGapEFM32xU8vKkVDm7PPT3IY2ryjgLNyLsPcyQ420eTXHkh9UoV/vo6AYZHUBjObHsqtw==","shasum":"d5b1657d57c2d80e69fdb73fafebad0b76553a0c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-4.tgz","fileCount":44,"unpackedSize":174576,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgWA6aCRA9TVsSAnZWagAAP+4P/0cUP7QLF3W9yQcGI8Do\n41Bj+P3FzVdWMf1Xyu0F+74oCKB4CVAGkDZtzorE430kwtjtGu0NjCXv0JCW\nW3sgsppR/qqaki5zIp2RKiH3s4f0dKWqpHcqSi5J+fmHfyluZjfGIgOpCjop\nXoHCffsnJV/ZvGvA+l5IFzdUvvrTsJ9FhAchTDYQuR6P4HlwDtaiaWAurWIF\nv2Gm+Nl5Jlc8XDkdcw5pq57P7vifvQu4m2DBbKFro9sF7AR3h2OLoIDcPVxI\nmHwkrj3Q9eFRcytJAgHF5XyP0I+OUiFdzA4wbGHKw5CP+rSJzQwtPauJO1Xm\n2WXjemKL1Bhf3btC+V7q1VI9XcArkLsHXs+PTh2vZHGrhIJZkAnsENhVwioN\npY7dlwtS5C8oSc2kWAEgft1FZ3tObbjurSYO7SPgaRLaWfowtiQF5IpTHvrh\nMBF4BTTaIDw3DTJQNLyjuQEh4SWofPkR64lapcR1jjx6NDJ98TXtI0e4QDS7\n2VG9xZ5pQyJSSKRQkwNne7F4NgNS8Ptegdqi0Zcfl5OTvTDGCBWi5oFk97K5\nmWzk4A5ePJ2vmAA//EAUi+mT/qMy0LbSWjfBWpS/x34TXNAcS9knQTX1+i4T\nCjQBWoiaQpBJTPgVZoipjXY69aN7ZBDmL5Rn9GNsVd+LldGKDizANUcICMl1\nhuwz\r\n=dkQU\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDy6kAATeCtxzr1icpNouse2agQHD0QIZS+f+Zl6goMxgIgNvYw+yAlFMiFARon7XdmyEqNPlbTSm+UkICyz54eAdk="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-4_1616383642397_0.9961615911578794"},"_hasShrinkwrap":false},"1.0.0-5":{"version":"1.0.0-5","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@size-limit/preset-small-lib":"^4.10.1","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","jest":"^26.6.3","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3"},"dependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","express":"^4.17.1","inflection":"^1.12.0","knex":"^0.95.2","qs":"^6.5.2","set-value":"^2.0.1","yup":"^0.32.9"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  () => knex,\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n","readmeFilename":"README.md","gitHead":"e9d2334dd759e65b3d8cae1babedb702e044c8a6","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0-5","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-p1eKm9uSGrnuEfQGpjoT68RtKDCdbtmBt04oHnngMH80Y8YDJ1sp9CbBH5u/XvAStsU/ZVqgh4sPX+j9lzwFuQ==","shasum":"4a7ffa8cbb2040172f8a52dc7ae9718e800150b4","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-5.tgz","fileCount":44,"unpackedSize":192516,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgXosuCRA9TVsSAnZWagAADN0P/3rS53gmtDS1fpXhzdbL\nQxaIY3nzOnz+ayQHu7LpaZqoKxqk8Ja9j9m3K31zF2OlVEUUHojsPxpcbfwH\nnUItiXlPgELvvBUVuPnS6K7AQ049aTtp5q3s3v7LVEPhQ1GKIgZ3Tru/yrSW\niJ5Jevjo6Tv6Apd9K5IC8zyZZB9ZY8Pw3wWlrbcieVlcKQx/Bk0i9DSkn+ox\n71ZVRt7nFhlwcjmiugPzytJYloqJ3jVKL+56tTqfUzCcrfRx6pfveUK4TSdb\nwia/ZeGZiiVplBDWKncaF6baCOzoddvQtSdfhLP4PxKLW+nSMMzjOm03S5o+\nTm4tz+G9OZJfetnTJW0iqfh8AtuQ04Psn3wtG54y2nCcJoanvSTeJYyx/XyI\nAfvEJld0vlpwqQ16DQERZkg2w6qSalORvYSgbY3yhj8Fe65gyXsP/S/rrK++\nLmvI2KDc/6JuMFYJ3tG5fYLhqknqEtsAKF+/iZZZ6fUVO+8YH7qTa7DvY9X8\nXwU7vT4QU8l1CIREzA9qoCka6IspNsWKLKLJ+/F4Ha2CmnpmtEowzAY24epY\nhGWFWtnRwsaWW9PCwJaGFGlYX/C3BowCuOKd38c+QrgDjlOnrLrYEt4Q5zjA\nGhz6XwWOXNwvn0299Kj2l07ld6x0FlPWmFUQykDQFw/IqWQwYuMJxNOqLJN6\nofuP\r\n=4idC\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIFXYLa9PWs3dfFPd9YPu29kSIN5BdhyAfGBsTNkmdP/GAiEAjnlQnn8YVKwbooAyTbMvVhhxMgvuDecaacdlnuZse/M="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-5_1616808749497_0.058181616615116116"},"_hasShrinkwrap":false},"1.0.0-6":{"version":"1.0.0-6","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","gitHead":"93d9b77fc9b6a40ee9df729883e9dbc8ce10f3ce","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0-6","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-pQsFLPczWf5CNLQQiBSnXpTvjfiBrynHhfbxqoF2nhKwSERCehHhA7DcW/qIw2Iz8BZi3xLQCGdi+JBV+9bCLA==","shasum":"9aea1a25c8ad623e378484f85e26dc884cbaf418","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-6.tgz","fileCount":44,"unpackedSize":192851,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgXrIRCRA9TVsSAnZWagAAMYYP/32mIqcJN/WjCmpgYuJK\n23Jme9c+deRZ1S3BTwOe/QPX7b1jEh2f0R8Rfzwki/8wOmrH9esYjJslLfJS\n+vaJMohy8bGfzAz+ptuECpWC29laXJyjc26cZVgroVNjaR3QZLR8o10B3QrJ\nw8kUbc6P5y35pVXW95DQ6Xzfa7AE+dvCCnZC+H6WVqvNB9DRd2AlfFFNhMUx\npWWNszMvmAIfKJvEP2QRuqj245JlkMoQpleWcfQ6NQHPzfDJ/x7PIk+y+YI7\nbEbhU7U06+Lay8dNVxOK4VR8OpklVRhxO/0J3AX7xNwe6pj68GUSQcUWZ+80\nIokTzN0Ie8BvQTN4LbAHnrPE8vRxGwz/astX31Gru2Y79ylOqOvCqDo5YWlB\nsvvs8XvQsGertOqLl4pyrMzH1n00flSEYOkPbbSMFwOV+KGV3dvEYIIuBCok\n8hF6TJ6x3t2N/UEXYhSnxLeWryw6VqlgQDUj346MGJdkmPnJ6W29YA7eyuGX\n8OUqbtOM7i17MxhMKLMqC5kIT8vS4FKMtNG5MAFebCwAJuoDeJdpfAOGI70Z\nWYh4VxbIbJTvLd3pUYCxp+k5ytKtzlU9j2CknVIv1+r47pUUamYTEcp4hU56\nAhsociP9baiAahQatznLZnUdZGt4ofDd3QxbJrvRCOkp6JRI40aaaZuCrNqx\nnS3V\r\n=aXi0\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDRzGVc7vkbRc39GRGfcHkKgctb23onXLwR6KCjFhimEgIgOo4LkJ45OHJBgepUp6yAdkn7/aRNTmg8jm5BeIhc1iQ="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-6_1616818704988_0.9311980891239791"},"_hasShrinkwrap":false},"1.0.0-7":{"version":"1.0.0-7","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","gitHead":"8e3e9741d393845c62c94e4af5c96a15dc890a0b","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0-7","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-4SkA5u0H8tDKMQSispH/LoXSwON4/2sCGqT3JZ2urcbatFCdnLpIU+hBbDX4HfSUo/xSTxm1sr1f06DEMAwDTg==","shasum":"10027b191ab22e9a90924ce3979b6ea4eaf1202c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-7.tgz","fileCount":47,"unpackedSize":194914,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgYUf4CRA9TVsSAnZWagAA2koP/379j7M3Y8xUFHe/jZxo\nUrVG/eBVHiP+5XjCRWcG26wEZLrD1oAVRwFTylQihv3zOvGgRpJewbQKFawC\n0nP+T/QtGVsP+867eaw2q1oWIvlUiqmgIL2p+Q1s9U0bFHgiqAzH2Y/L85sw\nQKJr9W5uBjqBDe7aXudMxtEsWYwDmXwZ+51b1hj/6zHs3RVisVEaIl7tBNY7\nlJ8wGNTubdnqT/mkY60m15p427Ibr+feEXfavRYWAp6KwDizPMgfOG3lZv9U\nUIHpGkBn1u9Rnau16mZBln4TreK64Uqzp4t+Be/a8tJCjEe/pErpGnx0CdLC\nmiraGBWRLJfBd2NB94evEnf0kKv/oDxREfdxXij2fVuzEnQDKdhkbhK6rJ0C\nYA6Ei8JY9A/H3b1LCCDtwTbp6FGJ8lp4vJmIwHjZJ0A6eZqJ5bJi0FmgZA1j\nAVQoQ042iYVQPNxL2M41DmM1paMKxUm9gEv14TSGLi65jzlgcBBAbHvmu3b1\neJ/W2J7DHdLA9UGkZiSpNGB1E9NZrIDU0as+7N+f7o6KjnYxkbFaidpxCZLF\nATeYF6xQ1G0DE1kB9fa/zoKg+OrsJHvmhfigt66qdDOo1u6KOj6UgSGd84gg\nCB6ltqnEg1hM/o7QYcijV9HQV7tST1YlMpeBMWUlRBAB39WnEk8TcLhGzvfA\n8CB1\r\n=HIYS\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIGfJfv5Xiy8ag0yACIqW1YXlb4KDMqXOlxKzs7zR3DuAAiEAr90eAL8X482uCqH5MZhvxlJIu8MJhJ/Nf9gY338iQuQ="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-7_1616988151874_0.09895808745338819"},"_hasShrinkwrap":false},"1.0.0-8":{"version":"1.0.0-8","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","gitHead":"2395352a752be1afd41b829722d7383b0a3d30ea","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0-8","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-PFkL4gKTwdcJSsgdEn734LCWFzqKyLgRUwiNZKa+XguTf7rgZuh+c47HxhDXkRvLado334ll8R+OTD7H6//uOw==","shasum":"08f2f4ae6e5914120d8c2c5236d60b1eb4f9250d","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-8.tgz","fileCount":47,"unpackedSize":196556,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgYneJCRA9TVsSAnZWagAAHywP/jqYKsaUdua2IZt/+s2K\n/uov/O+9Xq99Zgxxu0rKgHt9YS9xWW3i3m1jDnma8rEvOTzAuJR79iI856ud\ntw6strs+wD3km5CUXakAhH+vBB/sYfmdVEYN+hLOzs8EC0+3ZLACgXdrvkRA\nW/PhDhmjkojRqHVKmkK+uvXJ9duUHUs1Nk9k/UfDkvHrDnD41USw56t+fs2F\ngIss0Wi6A/zdpQUR5ijS02sPel3WOXqnqo/V+2OmlOta2J4t8kY0+121+v/g\nI+i0NLg4SELp0EIQG9R/geKinxTsagV5K6dh2mGOJh4OhN7tLKWO6u4ShVOT\nTVJSBm8UQAXza+B8E/g6MfXqjvok/OYqSSIZ14HHWUXPR5HkVND9nzicQy8c\ni/CV31UybumsQpVwg1jIR/qDhJOZE7b8FrqAwCulwuFCXjK6vE2RyQi8DE7Z\nDOqp5B5/+M89/198I6XORdR93pwSaWsWPBHEakIbxtg07VRFxYmE0iE6FPwc\nVTgqXgrIlxJdL48ZgVvNiCdjy7L+5PKRjCOl17nkpA08yDmE+c8fN+/r3H2I\nOJQJWFxxkwoLbeaE3gLggf0Wh894ugESaxYxjIfUs2exvfP3mA85lIfZEZsX\nnH1ATaHPfIR/vunmLxr50wyhbpZfAPgFqZ77ys6KAQH8gapqazezytjwhtxJ\nlLgj\r\n=Ac5N\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIBjgYUE+c+7C//zQJlKrHZIKqx3/iQDVS8Q5MI/wJpxrAiBHxitn/Va4LjfOjBiCa1fSJ0jnXw+3Pced1hys1YIPGw=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-8_1617065864989_0.005898410735480697"},"_hasShrinkwrap":false},"1.0.0-9":{"version":"1.0.0-9","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","gitHead":"aa4a8ef3c2857b01db56abf294ce58d6f0a096bb","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0-9","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-HwV50GuNxoI+guCsmcoRLiGXcgVG8oJIZ7OyI4Nr11p4Za4XEBCgi26WcPRjPfWvOP4wgleYdPpachpx/Hze3Q==","shasum":"6ba69b7440938a0cbabcd5dadb9a3e0f5f2790ae","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-9.tgz","fileCount":50,"unpackedSize":200474,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgYqd1CRA9TVsSAnZWagAAewsP/RGXxlKYLffhTCb320AO\nYa1A7M3/m0cv8m4iNEvzx8DUy0jVS5PlIGJkcqgytWpgL5yFGT2F5GWBC3kb\nnTma1BvRLCa1RfgzRMyXIWwQpLQUbLP87WYMI1pKzqN8kyo+aWGWulbhXkA2\nhu61zlTgvNKwpzZJWSZ6Mzzo2mE2Vfx0YwleuIEcpr3tgntmRS4m6yyNFIyl\nbDgyPeyiS7gcgAF1G9BLiGC0FkjCfCZvIhDNUzl8gwemyMJiFqY5sQ90kPyn\nqTDZqKacGK/1it3iZZSmHIgL1IUhI/q7i7J2s5f+hW6VhsVQOLgsBGuQCCNc\nVTYPihUNSXyntOV2NpO2D4GwTW52D9fsuCKpWguxCHlQzZj4VBv6zAu8CECh\ntz5wZHmwCdfB7gmTDem7H8zgQo5yR2WQdrVorE2rCCSNCReUxxbOOathiH74\nd3KYPsu1OesDmA8Kw+IBxYF51hHoiHJ6iLr1KiH7hgkTtta5S46k2MXREf6l\nhJ35jLCnnoo7ntdF+4ObTWzal+w1XV9eBlvyQS+lN9j95P7feknN9mzWXc+5\n6V/SjjUJmXhX1nlGHkqC8sgCB/+PXVJEW1mPdTSHiZNSQzjVLrPgyDTIox2R\nuNcEl5HkSridbAxtir+RhJyAa2YYDgDLrKfJ0h3l0lh2Hp+A3oRH7a6O/ktd\nIKk/\r\n=dAw/\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQClX0c+BRrpcF3jHK6/5gpDSJcDFgMejnoQIWvJeonM7QIgTIggC+0f1QOjIZgC9piN8sY4+4omdEJxoUkAcauq2hI="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-9_1617078132673_0.9629178395673703"},"_hasShrinkwrap":false},"1.0.0-10":{"version":"1.0.0-10","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","gitHead":"4a11bd90e6d57c34c02e1144056625b73a3d2f36","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0-10","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-myrksL68ElGTtF2V7+HYk8j9PNjX3Zi/ETXKEO60s31fd1+kl+iweLla7vIhkgOtkyhX7HR1GM7bh6T/Uhr5Fw==","shasum":"05e89c0075304b25afc8190ee0f963f90c6a8371","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-10.tgz","fileCount":50,"unpackedSize":203255,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgYrplCRA9TVsSAnZWagAAbQEP/3wrot0GJ3OTKEBFH4uy\nkzNhx7mQ7ag3RtDrkQAYWZMFVGfkSGzuOWKy97nqzAuX/f0WHnFo+tynMB1E\nuROccg3bEYegbUOPCWoivpbAP17Dric7nZ5qU3BdGGcLTg5f/RH3MfqDgoAk\nbSA5TiOiL5QQ6x5dSDDRuW0RTQw0ZhThWXxvfemq66HoiXbS+Ko3OMhVJMct\njR/4mLIBNUbbTrKNfSmJddBFGU1y5MogvOs6/QU/eFKQaW4tuoG7mk72/ytG\n+2lxWIKEAth/RJJwtedfJBlVNrmlrQjym30pxVRjQh4CGowlqgDup3agiZPz\nxVJ4SMaUYRoaTgApHzG1o8rtEtHEDCdSiqdsGgW8wwFYhbIx3rFEfuqHKcLD\n3qyu3Ung9JOdYtambtExU0AbebXbc0/0kzwbFlwJC5IWHyX9SHAJ5BjLSE7U\nzE6Xdox8Bgw2tPqvah3ScCa/t8tNu1n1osvaDHgrOGXmdr3Tl6qhBMpblmf2\noU2KnMKdkEAqYCM9sdQjvACzh52bD42lpzF0RaVpHarx4bRv6RknX5Bg0e85\nGNHeiz7wpJJ5wPl68auUI/vjLECSZIVYXwIF9XjbkDUTCUlR4tsoC1OKgeuk\njrSlCg1ETe0DdkasBNaNw6tKwkN8uHM1ysFrjmVTB0rByFb1/aA8K4KV5t4B\nVrSJ\r\n=G5YA\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDK8KMwuOWkB89ZGsGP8FOrtvXIQchlu+9YBjTdw3VQ7gIhAIdM/rTOMC9sq36/pM6BVtM98eOmjG41oA3eOoeoPaaT"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-10_1617082981421_0.4263869796958586"},"_hasShrinkwrap":false},"1.0.0-11":{"version":"1.0.0-11","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","gitHead":"16b7615d413d37761eb4451b1cc8655b7bef5827","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0-11","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-/6TmeFpfeaHRWMWII4LccELid2Iiwwf60inr3m2iIss7gQwjP5/Iz/9CVttao5VrG7BZWw4gWjfarXWNQ+kZhQ==","shasum":"48a4631ff45c7a320768b2d975b8fc87dd50d471","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-11.tgz","fileCount":50,"unpackedSize":203709,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgY/Y+CRA9TVsSAnZWagAAGWoP/AibvPMzYdOlhiRNNvFU\nsN/jPaBV0gxJyPONU4XgSMAPRVJKjSl+Ivd5LNc5USO+dHnIdY1RjvcSheN3\nXfnSiRJ3N5lWUOCjxTdIdunimz2CJGtPfbR5TFytMz9QTg8Kezr/TaWsKaNy\ncDtxmDtjHxNtjZS2f7jDDdD6VCd6SR0rr94bDY2t9wMsVSdrJstMH5VhuC04\ndvaBLqf1yvH2cJm/yyl/lKxELBLEAOxneHTYAZEyoprYOda/7LEYDM0BSVT5\n3srfz/ShQJNjadS/cnc2jBHQJHXxdSv7tRcvxkWn+bpTOSmxZo7Qs16Oh/dw\nMhkmHC0f9fJkzI97Yxl+1zdBtbsxLdGu8pMVN1vd3qvnOIu82K8ixXu4EG5q\nBsunrtFN9JlBsZEcR8CP7+MlyQx7t2bI7dDA4kAOokkTG00gTcQmWqTvcPJ/\nnPicdATd9JncIrpz1gMXF2uHz4SDP+kgl0QkyNdEra47sXl+y6YqEQLAfMTt\nx8x8uAhf0f53bQlPdtmUEwhNBlI5pMsc1ZOGV9bxe+DhNrJwo4p1Sz+TENJd\neCaf8ACmYCtKwtFyJuoWbxuE5c2DmmScnGWW5/qK6EYWLHboC2BMdmCFJmx9\nEPPtohfjrungngnMG9FoCmOqp8JHqfXgPv3BVhOwVHNmttUSPXv8SgIiAGm+\neJg9\r\n=B/U+\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIB1WPvCD445wKxu2LRrbnysHor/9+fbbaRxyjr6TzwoTAiEA31DG36xHOpc0pbgdcfbRAV9zvZjQG6AKoleWF3E001Q="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-11_1617163837875_0.4723377098563246"},"_hasShrinkwrap":false},"1.0.0-12":{"version":"1.0.0-12","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0-12","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-Plvrh7Yb9m1XdCK1uU16V733qUiTXkqOIzcXOjBFfe9/lWZcC40/s0Cr/DZRG/6cDiOwAJR2Db5h1nxqrkTOXg==","shasum":"40a33659ad158f0399fd8c147c091042d48046c8","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0-12.tgz","fileCount":50,"unpackedSize":206179,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgZo/RCRA9TVsSAnZWagAAO5wP/2j9/oAGsxHKFc5tgB0j\naNTqAqX/YDTFIW27v6AP9YcUtzTf3WkxR3F+0bx0hm/eAJv3h4C6CAYa+HLI\nnRmBxV/0EXJ8BHTG873LjzFfzsAFQwHKhc/ActRIOrj1aWQ2PsacO73YQXte\njEEmYbW75+dHHp4wHAJhhXQ0TqVNO+ICQj5UWWrYptJHBnw3gchU0RVJ0yWm\nOC3V02gg0Gn1+3Yhl4cb9grG3HLsrvFAgqtFvnSgEX2t1HA+ct3bgRl/0vYZ\nt1fQFRgz45z9BNNPSk4WhZfhedwibkXg3TjU4ohEnbHVZu0K7HYu9D3KijHN\n/St14IjH8BFtiXFhOgjSasNf0v13Gfrmfi5BCjn4/HCMOrY5gpjpVZ1meA6r\nZpSUR8WgvqAayrONi0VBQpFz+v8TQi5VI4ktWFejjBkAJblQabpYGHgzi22C\nhdQgBmqLXGUXrPHmEMQX6molOW/hVJ6CQtq0tOO9OE97coCVAtgwEu0mYfM8\nSW6uceB88bA7Aef+gQHQPEL1dSWhv8zrHZbHglYVn93T4tll8+vxEbL8N0BP\n4A+S64XbbjXN/jag/Kl2KyNPNDJTlzrv4f3WAfbER6DVb/3IUCk1XqGQrRLu\nA6GEjXVzIbSFjAzn34sPcy0yqacuAdTTVzuQg0qYsSca8yhYimNTFosJvgoh\nnuPt\r\n=M9B8\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCeWupVYJ8df/XUTmcklu31xdj+4M/iTTwK5Ewc5Bd/BAIgbffI+ub1qD6YV5hQse0zKpS35BHKXyjSSpu197H0G6k="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0-12_1617334224719_0.2820280764183456"},"_hasShrinkwrap":false},"1.0.0":{"version":"1.0.0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@1.0.0","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-2FNMwH4FzeC8SSBqokjkz7mbXpf/Bz3psQdHkcgeZ9QTq+tS2F5EjOyfAmRrkDU50AZ8CT2eV8hAaNsmVrTuoA==","shasum":"755af432cc66b82b96b07ca144a1a93651b1278f","tarball":"https://registry.npmjs.org/@synvox/core/-/core-1.0.0.tgz","fileCount":50,"unpackedSize":206739,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgaVBTCRA9TVsSAnZWagAAKtMQAJfEis8FfClX7rwUog2E\nae/Qm7nWfzVYnj09gcvt0T6eE/yMuxLsc0+fZ9yHuq7Oe367i7FjV/PNkdIW\n/HMkFpliSLIdtwRcO7fjy11ELiTvr4Iw14lfqxcBzntbwgAfvi8LvI3LN4lz\nTU151zgodXG35pBE6X5lt5Egtu5JB/djy9wCF1HDRMkGuDRuD+IFzJFsnvMu\n7ZYXhj6YmgVzuRum70AJxZY05ifzXXeArHCEKKyyIi39KIs4VLPTg+mZkU78\nP/V6bovvz/WxTw0Owo1o+m8eSC+D7K06ZsOSXso7fZFgNCOS7Ndgqdlz73l2\njN7xt6ab6yl4Fb9TlcnDVrXrL9eoVM5s1XR9ujPnRTTcY6TPFq8iojRNkA5X\nRQ29JwwvAn0qCH+tOG7UcTz02KPij/7c9323kIZRonlMhygE+rup0j69nXO5\nfUSLCeL6AdQN4mkrTLPyuG9vkECWEbwNbuCHO75XvXLyBmFTKBhQCIYgfaRZ\nsr0hJyoGx4r3pX3L1GkBOaUTgD4tzbnvllXNyPW6YGcbg9i4Bw897lw8cD2m\nVi4G0ho9g983zoWl7Pk8Oc7JYmTOp54jHgbjC59xLMfKItabSdRDgsaYh8KN\nLCsigbkM5Ieu84tDpGLegSG3PpJyyF6Zsemlq4GiEwY8hVqn38c3YsKiUGCZ\nKjo4\r\n=Wyiw\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQClI853fZT9/R7tTgUgtDXmyjgHdWigXH1ryCofNsErPwIgZ4RICTstZ16xF9XZSaU6W4V7ahFynUFcCLJwUxLYKxU="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_1.0.0_1617514578545_0.3075382142444043"},"_hasShrinkwrap":false},"2.0.0-0":{"version":"2.0.0-0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-0","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-BRBuSxWWTHrp0U7FpCKtA/JKHVt3waKWekJeGpeyhwOwf/8MUGCKtYY3awakHf935O8WUdtG9neh2sl0DaSzTg==","shasum":"355471739d9cc63df18f446c6185c41bdafd232f","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-0.tgz","fileCount":50,"unpackedSize":205053,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgapd5CRA9TVsSAnZWagAA3pAQAJvABhAv1QqEtU+UZa/g\nbYf0C8zi5a9LNWfcptkrclSJ2HPBl7MG0C5KG05Zp0MKFZZ7QGV53pIanSfq\nHnJtt1oJKMBrzsyWPFeIshME9IYEhMDt6Sg4tSHlBN6THQ4aHOZGdTjaE46Q\nDSFfTCpcB663yg6XPR7lH1nhOSvftvj4nyT//qkHVtFbxF5VjxkLcFTlIFUh\nattjU1mhQmYzTIIH9+PImzRue//+J+XmaFXcdFLauTsxc+x8YngcEm1/hpgD\nLAoVgjIvDidZL1F5pl4gYxGe2gkVKqPWa9dCGfBpexlEIkAVIsaJSqks3SUM\nnnK6vUJPW6ixaOyPyQAIRKTdCIm1zRVdROF3rR9wHOxMaE7P1xqSGpYCODAP\nquB0ZYXfuc2Ia8NUV4H6T0Fo0cz9i+jRCvSn/hOj39srIFodrHYpQx4fLBgq\ngh5t7xQnrI4pgoFsI0qBmlmitgJunOfWPsdwI6Ztwoy739dhaymxe8wXXsBE\n9hVZaL0eVss4AN7LYiJnYCEMy1gGh8PQauKaFWrVKEjQQFe5BpqPj9EOSFwd\nKSptPgL4wXesIphF9jQguVRjmdU+cK2o4BDnc2so4z7ePnLeP9RAO71Enf59\nasXnOOWzQ4FdEx38layG1hzKKDB5WtN7uHufxh397lzrTIRpYSJp8p9wAhoW\nuOe2\r\n=KjYT\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCICqYty7xKqExpasjk+yKSBF8lxDtlm0PttjZRwSpQfjbAiEAvpj9ATp00YY0gaNPlkHw5Cem+NCidI0EejgQVyNBkJA="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-0_1617598328931_0.7224116903471398"},"_hasShrinkwrap":false},"2.0.0-1":{"version":"2.0.0-1","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-1","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-6p0jK4YXJEIUHQyZO/+LLHn/ACN/XisuEhFjanfOu7UQMRQERDLT1mHbwYpcrpXjd4I42ez7T6ncCsh5zoXQQw==","shasum":"72c9ed179c2bcad4aa2fc74e3e7f9d66208087eb","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-1.tgz","fileCount":50,"unpackedSize":208727,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJga87cCRA9TVsSAnZWagAAPewP/jtMXyMxmmXOgy79VkTn\npgwpDaXU8Ih5sd4XjXSPnJmCJU3wIeWTol/nhN9GGF+o3T5Fbu5P5n6k647e\nKLbhBQNMCwR9aMWyaqi8QmLjiFL6kq4YchmWqrWigzWJ0ixUwUMs2EYY+eZU\nvWpmBN1YbYdfLojVMJvZQYZ3O3c5K4FVA2rDvsahpBKXChjdL33VqOAyEdan\n5Z5tkNjFX6L18NA/Y+OiYQq0HD6yPmrCrXnsdAEucT3YrqF7fR1MIRZ30N+0\nAVK5LuD3IbWvGoR/RJikhB4nSxN0D8tOYMN9sVQyUPVMrClgbU2hGjc5yAjo\nrTV3Nd4jNuQkmWjFEHpOPiOvjq9nrXMHlW2xCB/ePJDdP7uO5Z/PDoKFIuer\nZoFHtrLTa4nh3ccyUD87Hzt8JeJZams7eI3Yl97vXhWu7diB7fxmlX9FHHWW\nDtioPpbzMfvkElws5mXmx01iHCH64b+EB9A6Pz2vys6JcEUh451HZWGjjPFC\nQ9QsK/tW0vgg01mxYFFwr27iIUDeQkWsujuwj9EjYFNEtUyvjkPQ/SuVehBT\nNZxxoLiAxdSqDhY5ebERiKzt+KtONHKmrm8yDwxxewAsKqB9jdRnQPJD4S0G\nKrDudj64wYaYAWpwt/oxsaDb4vl66r5aDtIOF3VB38mnegGbMzZgsSOA1+aC\nWfnm\r\n=qAa/\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIALMl8CyfohLudZ6dr/EY8syPuxmwI59Pxz/zQhPDN1IAiAL7P6qoI1O8GA+guttI4Za3BHlhz1zGchfT54JnnFvNQ=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-1_1617678044195_0.4558234773364265"},"_hasShrinkwrap":false},"2.0.0-2":{"version":"2.0.0-2","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-2","_nodeVersion":"15.8.0","_npmVersion":"7.5.2","dist":{"integrity":"sha512-spZwW0VYMMMBdL2ixzwYNTzeM3xbTR1o18+elRp5FmOBy1wFJV50ot6d+HqEfmfKJBFv0tlxOlcI2UwmziWLlg==","shasum":"1c5f8b6851c43e0922b8e56f90aee3ae9f45dbcc","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-2.tgz","fileCount":50,"unpackedSize":209905,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJga9ydCRA9TVsSAnZWagAApNgQAKTuaEUyNrLne+5fsK9g\nv5i4KiD5S2nsTec3tWmGVAv/6MHQH1ppPijPWvUXX1bWqnYK6GNbJJ1aXf3v\nMsivzDw8d7Y7K6fKu5KVoW4sBTkLAJUVXRpYW8GCoBCR8yi4OizuTXL2+trm\nbcmtwdFTp57sbWTEzqXNevkNfrIea8LPdtQ4QD8h68gmFVva1MuyzZ1Jyv8Z\nObw8S9Ah4n5N7wA2RNd+sLnVPwUkNKvNrZ5uSum/APlM/c8dX5MZgmygE1dn\nX2bRAiQEfppTjG5c6lS9kP/BTiHKFh7W6fowmmw+4vzurjG2FjRLjrGKLaQ/\nWgL7E30BAvhI63gfGs6U25/7/Oz2QdXXcbOlFbh1WnraU9aFQMbxRpiFmon5\nYOLjaLp4SMC4VwK9cSeirYVKe70bbcxjyHVlLir9yPoNr/elFFdWo6pje/RG\nhXACOgm/eP/zfcXeZQ5pjvZaG9p9n5r5ZrUyzXNeXxNiQxS3ybASzZyrgeeA\nZRiqmS/rRSFVy44AtNO2rD5pZ/TF6lLXDPbZjT/1WniCWsjiNcTaEK3l4BmF\nRG4ZZ+E43XOPbrNOKvCGhqVE4iDYaye+2NKwLDq8ddisWJG3bq+HPgDrY4gd\nlI2KMgvvD5gjl8SX+KMldxtB6Cgu25/glCu54R0mXrdbI1YszDycRNFbESlQ\nDvtS\r\n=y2lr\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDk6JUG/yBjovChV2XCQq4yrK/iidajePj/A5rM7dLnKQIhAMz1nXSzfRzh7vqulJDmkL57DjdK5BFZG+97L/zLT8VA"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-2_1617681564718_0.590605290629199"},"_hasShrinkwrap":false},"2.0.0-3":{"version":"2.0.0-3","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-3","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-kr2L735+cI0Mw9AcnhB3KORL6gpHaZWvpMsv0oLCDATzNMTwk+So5iSGArI9nqOdomF++x7tFsF3P0IfFxzASg==","shasum":"02a7aba345ee91a49cce4e1ceca268aa8e939aa7","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-3.tgz","fileCount":50,"unpackedSize":209921,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgbTq4CRA9TVsSAnZWagAANbgP/1CEnoetOe4C37QBzGkS\nNTcF6KjMjvYevPepcCnimNDD5reucKvEpA5gjIPp1JcjYya975e4266os7mw\nie9Z+ROdrbkjJSu0MVSIkkzDHi02/qyzeaIHaeLDh9UW2EjC53uc/thQR+q5\n0DRGzfOEfu93SJeLdbmnrOzQqN9fFNRclDdq9GVawMFPur0rdAINVOK8s336\nZwMg4u0sYfDpmxWGOBgFyzykD/enb2c9UN+lsiCDPLUrNvkdNA532s1FklHx\n/JOmNg+fyTLqHb/UrvihKLJTeLVJaXyH+X/PMGLn45vMjkj2k+MSwQ7YcOy1\nzw5tX0Hy9FurZgRs4W0mKApVMQOSs+J3gYEtdLkmUD/1UJSm7GqZ1f3VKGBd\nTb7bnf3LTF18BvzHjIma3b627ZEA2nQZs/jHHVBa/pkysA60hOi/3QlcLt5f\nJJ/lc0PEZlIU/VTHNczt8lIZkVx9kokUCqBwoL0A+poBr3BtG9/7ST6nVRFX\nHU2aOGPlAcBEOJLdANC96O+PbMkU9E/hUA+Fe12dsPuxNukJm0E9dOCjHHmI\npXJ+lB17FSePmc0SFk7zkoANDBR0V6bUVE1mgT6SbEL1PWhqmxv33ANU8NUN\n5Z/qpPeQFVRndCyg8RokNhH79WYTlOcty8cQ2bTLhrwGUl4woetmJG9InNSh\nuKW7\r\n=e3S3\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDi1g3sn5n4SwUTiGY9zn0Vrryv+1ffq0f1WycsoCITVgIhANNDJnPD82/w/PYgvsnj3md9NLn+F4Zc0Y+hcvFI+epG"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-3_1617771191382_0.540421087040984"},"_hasShrinkwrap":false},"2.0.0-4":{"version":"2.0.0-4","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-4","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-r0dvMzywITC03dlLLGTjYBazCB5dl4v3adcp1UZiNjPF95k4kYqvhBQYsxSLpCVoYw2iNxPEd4VEKNXG9Xisgw==","shasum":"e6a6ffe07a74c36f1897476313a7b2d4b77214e7","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-4.tgz","fileCount":50,"unpackedSize":210765,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgcjs7CRA9TVsSAnZWagAAsSoP/RsYn4QcwK7aPTOQtrPw\nz8h2MyRMzgzOCBTOKT39UpKh6n/pyku13h/QxL6fQfnm9rlATvyh9Z067sov\nyLMFPLievYy/jBYxQ3GNUS74xT8spsh+UKp/mfppnW8pT0c0rnD13HU3V+Nd\nzuxdjbFyiHsign1D4HrMJYQLu6rJSa4JEPFu+vQoLNnwC6PCCu0sRHUe+xxT\nELPfiGRc6elf33jyGRXzWuGVOKTAeyirhzeIJubTPp4pCWeW1eEHBC+qkN8Z\nVExiDVb8h5+/ETLATXgyp+5N1U9+pssfk5Mt3igM1gSauF+QO7uf7OJSrDRr\nLS4LhDwKKpfRAMq06vU9TwKsAy15aclBpZfK0lkwXWuQe+BinjevS3gJz/wv\njdoTNy30OMmRkr83Jactv/uxKX3Q/GcsHkWQ1hN9qUjejjNL1+lSRQNL0+M3\nE6QZO/2BlxKp+s7FJGLvIyv2TtJr9fpp4qtEQPvd5nglJVik0Ob6cW/LRAQd\nweSXLNF17MyVZQaB3t/SbppR8MRu+tT5OHbNuqa6Q1Hqu8o7bECINkz61THZ\nluHlFntb6cu4mJpc7Rif14EdjXjabaAVtQe8Fa4Bd/gE1bint8ViL3HrWV/H\npybPkY8VNIjahESQ/VmFPRg0NhJMucdUPC7FNWQ7dSJ1Ui4f+xUHT0+vFpeG\nGMtV\r\n=6H71\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDMYOqDoM2ePL1GN6r2K621paBFq207+MgAIocno9+U0AIhAL0H71q2Yy8maORnJJZeXQTG+loOx3ZY/c0Xd9IhP3Hc"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-4_1618099002985_0.2421579148369759"},"_hasShrinkwrap":false},"2.0.0-5":{"version":"2.0.0-5","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-5","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-J58PUxeFPCsLZU0eRrK3ElF0PkPbQ/JRZKQy3vLuAXezxt6Vuy1GzhEqlXmRIrSnbuKxw6joToEWtZasD/roaA==","shasum":"7174a02592b802c377c97f30c657cce5b9b037d3","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-5.tgz","fileCount":53,"unpackedSize":211683,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgckxqCRA9TVsSAnZWagAA3IUP/i7T1I0Y/TQz9jrYtAui\nSJNTdsegq293DBNRusu0tO2aC2vATm6iih5+yrUyQuVIf3kgj3cLSin1XuPF\n5ImTcl01jQkU5t2diKXoh1A23keFgEmnIanudbt60UZOTIXoP/55r+AYRSYi\nmXuFAo3387kbQPP6rmWwhHGc396JnAodUy2ujwVTIDiieOK3aMBIL4lztqof\n8+dF6QA4WGP8/ovU/KFUfr35i92odiMiFsiDZU7hCbRdtWRhEG+KdklahoMX\n6jFtJ9mlkdILkQ1XqUchAvwaSgsfrolNhpK/eHJbmpJkyPeAuCJVTO49Due+\nQQ+8/xZp1/3ltiz670aUnJ4MV9PNZd1xmv74cDs9eQfAERhJKPsoOaJh9keP\ncUScPNNShVT0pUpatVlcdszBmBwnmDpHK2QAPmom294cpu9ly/KkULiZREbZ\nPpLHLLo5oO5NY0i7J7zn41ycMOiKAvZPUjRdPpesZATN88Sthuw2AJnqBLVW\nqafkKDazGNYdutnTDWWuRCeHn4/CZ5U48ns182zE3Ht5Ut9Vq45EAqYz92VL\nFBZtYKmC0k48CtUEsUvPnZPdukctfR6JaHnQoyTA5pU/jGOCbOOmI8FUUCNA\nvBLnCEeUGEFAnJxMQpo3PYJRE6tyZVEtvW2NiA19YN8rXk+ah9xeviIryRGQ\nqgvx\r\n=DSK6\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDjXcTElVe/PeM2aGG2mfWLmC0r1alNGND1scUqeBGG1AiEAgm0/iB/XNzwr8Tnz9IQ/zV/h1GfHHB4auCQ2MMJrW5k="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-5_1618103402235_0.4877428319643362"},"_hasShrinkwrap":false},"2.0.0-6":{"version":"2.0.0-6","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-6","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-ay9ieJ5vIhYtAZXZjU8gOgzFFUU/1BZGqLW+qqBkM43R0j8ZhylMGpRlhkE/wYd3PY1AI6ZbEOdRFUBixn/k7g==","shasum":"1cde5be6f0b1e9afac611c53ef7925b49a45b64f","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-6.tgz","fileCount":53,"unpackedSize":212183,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgczwECRA9TVsSAnZWagAA5ykQAJqQU7AyvC0OCItoyd8h\nj7uqXKPR8L2dYHlx7pdHNLDMubIJVDs2UBzKOTgr6zAiiNQV9Ei9q0ZRFYVn\nlSU7UkpdYMZWfTSikQRZWaQhMVvBXHt5wujXAeR8yVD5wV9BcMIskGgayo7b\ne+dSxVSbp5Gnxvis2PVTaHrvd/CzLALhGB6d2MWKxlI1igeaHjIPooKI8CBH\nNBli3fHAHN1tYj2um449WI4gbHKb9CIIEU4oiAffKy4mxoqfYob8SAKyDnV1\nIYpPUWP7fYGNq7UByxKaM9IdeB+MGH5upukx4PO1XtFIk+LVelE7Egg05Dyz\nW4zuBDsf5nOk+JZ0kemWp8o8oReLbzpLLe4e3p/6OG93xFuBp9stExwQO1na\n6eY1XbcNTruzPHptiLKCU4N+78CSl0OT6h26JJ5koo79QwCE429Ux7DXF6Ey\nBRL2GCY1h2EPwGwL+DjbKvbDgZPTq5EeDMPAf3YL8zLgl05dXOc3DywsIhWI\nJnYwKvVQ+pTCkp76EZr1OtnYRxeYkhEU6cOSlJedM1nxtusnM/Z4oxGZLdw1\n0GDCjzMDs80VIJ1KoYdnJUdux99NeTY6qLnII7TPzVRrRI2K7CJNMPamAv/P\nUbK1DXoonOVw9yvz3uITVfiGVVJD66m1PEb3t1NK7OaYu4auyRm0vudV2c25\nem2l\r\n=WA5g\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQD5xxPi/WX7tMLZzbB9Nm3Zx0Z4DQQh+PC6uXptntsPoQIhAJrWSNQlg6lPF+z23HohVqWtPEZqJwHgedeQXQFkNgIO"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-6_1618164740440_0.5202535447779069"},"_hasShrinkwrap":false},"2.0.0-7":{"version":"2.0.0-7","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-7","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-I8YL1Mjq9xU0a5+ARWnAJDXj2VNf+Yr/MBzsrtJ3SSSdIExxMs9wGVJ6ELgK3uYQdIukXBk0dDBuxUlJujEBwg==","shasum":"150222f9ff60da3d7c13094339318debae0a899e","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-7.tgz","fileCount":53,"unpackedSize":213041,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgc1kdCRA9TVsSAnZWagAA6c8QAJ2KAcv0ihuCDhQxG3tc\nyXnp5U8hwsLxRaR5YmrdT0XmidFjmcf0cDsvrG/GW4v8PawC7U4qXKNNvSNV\nlZQWJ1JNrB35mI6tZpZbOhDWJ8UhkJ0Jd25Rv0+y+lZSM7h4gjygT+A9fosM\ndTQFjqiWPxlQfda7jYaWbottdg/wO644iAjXy3V/cH3DbfbFjf4/dwIMy3bY\nRPbiOdUg16AMSRZOn9DuzuxS5lpb7IFWa7GdRhJq+OLorSbKe1RpPv2Mm+4n\nEqjQVVzDZSR233mnZVZL9OQdY68eFSbBbWvS4g01N6fGis8TvmQxzCyVWbBY\nfWqcyuYmauiC68iCKzOAZqCtyVLoEaN8/51JDIK53VVZrOH+AFb8ZnkFDtRU\nQSsROI+rDtq4vW9oOy76ehanbAxU1jBYwjJgkPRlHqNqTWTiTVCsEK0XeS8Z\nuZbB6jxyAlpxefii1yDoIJW+MErCfG16YxMrBsu9P+9fBYdluiDafW0tiYot\nxx3BwZcdc7IbGGGE8rgb7UZndOfcgfucEwGe8LUj+qfTJDIVOgS0i8QU8s75\ndHOxu5Kscp+aHQJ1W3/dA5HMpboNe0rgB0y4SBRXjAJyU4HI2Xoh8NMSyW1e\nmcsOPTYB6WeJNUppGVRpgq1uYIgFzXlk6mlvKAx28VGehPHhHpIAsAKrlgVv\naFaL\r\n=+oNI\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIBXGGKKNgaBG2BXrJkzeDDuSu0C+XYq7QHmuv6R5TheBAiA7Zll2P+kow2ztQ6CssHL9q7w73bwOGrhChHaHRrHucg=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-7_1618172189433_0.9222335676509426"},"_hasShrinkwrap":false},"2.0.0-8":{"version":"2.0.0-8","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-8","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-LFw4RcWvP0ogR54kNa6DSTq2/Tk82vNLL5RdU77nILNYmk9K3nxjjrZovC+h5Z61bse5nwKhFH+SiYBrARt52g==","shasum":"906116d7ce0533fc67619a4410b677f300f88e11","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-8.tgz","fileCount":53,"unpackedSize":213046,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgc1t+CRA9TVsSAnZWagAAMtsP/iOdCX40p8RMV7S9AwZy\nUjFypvja8rB608sWjLe9iIKuRR3bm1LIsCJZl5f9ICmaG41zkg0JtSLkS2Hn\ntLtkGEod2G0P1m3laQXrr5ftgWa+lPudSNSO+553NfZsO/QfP1DV+eFprP7M\nQn+is83g+rTa27CX1muDn2GeNdgowPtQdawjKp6LuxLXPsIP/UImQFzyaWkv\nxy6NyYsaoCU1ivN79G17DIZw9/21pvPfiqJwr1SnE98YFmy4KXADEAqBl+q8\nb/6yVM7ah/KlsrurO9SrPAjCFXenquOCGYqzx6tgAdPPoKXFgFpU+NzDkByG\n1bqMyR8xECj8lkYRALgplS6xQgiUv41UyhKBl6/pMrdc6K3ZIjp1BmOvi+RR\n6idkwciw2Wccvd+AdwGoGwT2KLXsomp7t7ICfN37P1i+AlViyz2dGf8sk7RM\neiakHrgk4Xct0YVWGbu7fGbTWIEnPkGaqx4BdZLb1khKWuvhV7VnjlcrxQFv\nLAmigKT6gaQDBzh0ffd8NNWSmcBDfpKaNeFoaEuyULKqInB/K6EzXGhPbVik\ngeRhKsKYrnuVYvdxmKegpUlWvuY4Wv61Qef4MhIg4xcBRlvs8SRkxzMPHh1i\nWm13HOWCc7yV6V/LDtjG+EhJn1ii/FE1UxbOiQDUEihlI93UgBx/bGRkHVBM\nq9xw\r\n=+E9H\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCScyFPfAuqt6LcEdFb8RXIINE/HsPLwz/226yLDOeXJgIgB8NJ2bSZVVghE98zqxm43/3oTWMaWf0TrAVPHNXE8I4="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-8_1618172797466_0.5849293160172111"},"_hasShrinkwrap":false},"2.0.0-9":{"version":"2.0.0-9","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-9","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-yuS/nUsqyI72i3rkYX1sS9WILEse5+4+z81XT1RcRwbMJ56Bjt1SNnnLoUOhMqiqU22hifMtreKCdMbTttlPeQ==","shasum":"1d02ea6777ef6cfe23f0386ceddbe99b2a257033","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-9.tgz","fileCount":53,"unpackedSize":213010,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgc19fCRA9TVsSAnZWagAAlnEQAIaS2P7wAvMTIVp0EkWv\n38ypF4IzzCCrwrHbG/mxBhF1eNnFEjwS/YU0DO56o6UtbuE1oMD3YbtEMca9\nj3LcwHdp9RPU1Bkm649KFcM5ZA6r76SbJJbVSGtx7kDIE1nMnF3dYdcmcpUN\n5/GL/eSCTsEGqzY3Iv5RAiIbahCGFxJm9ROIN8K/JdjlqzHc8q27Wz1IQeqj\neUHONxcQsxw+BRBKWC1xElHC4pbQGTvlZ+Smx2pYWO//k+1J6qgYiUS4Tdix\ngWk7wgV/5D0ZN4D9asiGPp0Oj4Y814AiYojyha/SbAUXfczaom1VgwIbIyc0\nxUSnkMRbau9hLIO3UM21D9CmcmQ8U2NhAZhEgUQ4BFarU9nHa7emaBcwRbHk\nd2zRyJer9hk55w9UZ12Xgv+zOnWvRtxIF5H/vFwrxOPlx9oQ112awNOcRYWx\nPcmvfSFEPDVV0c58A56/tbgaW9aHe6PmGReABtVSZH518UCKQJBQnw6mWWia\nCUUzOu2ki7ApQxp5H/e3TNs9R2q5RT6UsjvtNM9QWZkjuYvhEnpsM2JA2vFY\nS9vWTUqhKD7d2Nw5H950vwhJt/1VYkMVAPapdwi8rgdjiyxT5ES8Sk3U9DDk\nVt1CwKU63qryHMG32ZuYC2AGxMYVki5ZgOOtCFU6kpPvAVPS/LRqTaWxhJ/e\nP6Rk\r\n=M3Ob\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQC7QV6aQyS0rrvvRlVU2DY0iRJcEyXEFzK5UGD4PYZsYgIgEINJiv5QRljsN76mIstSdWgYqmiEAGY2xF+/TJszonM="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-9_1618173791262_0.9681933924166017"},"_hasShrinkwrap":false},"2.0.0-10":{"version":"2.0.0-10","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-10","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-aV9/DOtlKGJk8rysPtyBXYu5uxOBH0Nj4ji6meBoJVOyMGYJBHYzfvk63Aj1kgdrRO0MdsuL21v15HInICR0zg==","shasum":"562205ac35476bf8f5bc146063d91b2c3d3f77c7","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-10.tgz","fileCount":53,"unpackedSize":213294,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgc2UOCRA9TVsSAnZWagAAq4wP/A9abwsoubaZAQRhlOKP\nmWG/ZIg1qSIkg/1vrXQsd6SbjbukSFGLxL31YK8kdliFgqiZwG6JZ/R5IMg/\nQxg04+K1KfpRYWwXxYweZJ523AtmbMxuGkHV5vZ/xSejzR+R+pVdTsqfYyOV\nLgKbEMXdQsTv3r/8F9XUiaScdfgXmH3gA86nXanJRczDa+nlADAeFTyYATxx\ns1v9jJwLOw3kSSaUuU5FuhbRoq5sRjxnncigWAi3t2C9KU6HZYgRg3HH+o59\nrSsIfwNncg3r8Jhm3ZW0I5j41MopJkHLG4rkz6K+4uThCpFhtg3hCChQWGwL\nkEnI+MbB7OHxkA2B8jG+IjL3pNllFoHB+xfhXKcWMTGc16/n1kdDzOBZIrM+\nDgquZ2I8IkbSZHINNgrocnCCP9mQcwy1ZZbs+GMqkPQPKBOIf9NzVbejnoES\n+aaZREqp3OWi3am1wAbPH0rgDvbuogsmGtvFwyNApji92N/7YlwpOV9eH0Jp\nCW2GMKfjMbzoFSZsSKU7kdcOE2hpqZn9VzXzEytG4BBwFXDSJrL8rPP4My6r\nxI/mC3MN/0dRPUoMOBCb+eFi0MFFwAiyU+bRl3bkB81884hdvTRd/z5Ta5NE\nWKIaQscQgkE4+3bPV7aIDnaqvnEQmywsEEo8HB1nWlMbeehptkW0KRWQBOgB\n/nX+\r\n=jsRS\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIB7XfUktqV6gXQPn+sSySFQAyeMBWx5EJ8pww/1scKsfAiEA3GOWLJ7BTghg2R3eT6hobWrdnaPgp2FS4L9EJuArUkk="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-10_1618175246369_0.6256037523907858"},"_hasShrinkwrap":false},"2.0.0-11":{"version":"2.0.0-11","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-11","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-0GpLKxHoAJ5iHpJsBg5q8iEVeivtwlxFCC9qoIX7kBBmHAhPaAXz62jOS4n++YaMmIUfVuApXbiitK47l9bPbg==","shasum":"79b8e474f2f16989da49f86501a235d89492e40c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-11.tgz","fileCount":53,"unpackedSize":213915,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgc4dfCRA9TVsSAnZWagAA8e4P+gN2JsEfbYoVOITLU1cP\nWtKVwcE8Nn1Of3jSDTrn6UD8p2pnsOUcoyjjxNaNmY0Ab1z3p+0D44qod3rg\nNDRrle3EjHoVG4oFH7yp7EbdjobDe5U/9/AqUccp/InbsFDnQ4KRHFCzwCZX\n5GFFLpmE1WhOPFx68sADp9AyUCQ72ifnxESYOlm5GWvI7hjbgrFil80RXkuz\nurN3U5WT6oULNLnWX5rnqLARkc+TEdlmGWgHzTInR4ATeY9naee/ZC/rNNsB\nJ1yTXoTz125E1QPamPNGgRIdxEbMxoh+ui2aNLK3BwEH1zCiDCdj/q+hHZ9F\nm3r8X3CYOsjCFUg0Gi6tObI7UVRNRRw00zfxdZTkjpcrci/DJrteBknPM7n9\nvCsvPsj221HOQhm8ny5UoOp1hVFbkxe2zWy6BQSUzuv+WQKgGF1pFdxVSBZV\nZbk0bB+gMcm4deqRg62OwnTsZSP7OuEgdAswrpygNpM9ieiHSXLHNyQYdenw\ncMSiTVonSvkdvzXmINzdurTJBDFlhVA6Py1TON74/ZUW8/FNTRZiYxory1Cu\n8pBWhA+yp6YiRubGiE52JtBcdA9/6qzfeWgV7nABoCZ80q2xwvrPd+xn7oq3\nnrIxkUF7ZbOy+tZa8e000F11lapvbW7ya0Hgc3lXshNimQB+BRRlj0qF3xqg\no0pz\r\n=3c8t\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIC6paq2tiPIKXxexW95Y3Wtv619S+EMOJevqUTDiagwjAiEAqTygucGxEai2+F5IcxKu1RaopZijY9LtIxPKYdMRpSM="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-11_1618184030746_0.9489788996300654"},"_hasShrinkwrap":false},"2.0.0-12":{"version":"2.0.0-12","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-12","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-1RxdryAUYAHqDkPBqzxdz2s95yjWDBoLVhzKlPBhruT7sWRzgkEvFYluc8virOd/hGQXf7vleQ466Avf7Y32Xg==","shasum":"617631a54152dd28de6cf3ac4506b87202be4d89","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-12.tgz","fileCount":53,"unpackedSize":216919,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgdOU0CRA9TVsSAnZWagAATa4P/2J9WqUq5OpKWglnVfgD\n1/oxA7MtcgFxnzFykvm/LKJductPtxNDkn4nVbI/BAXdCZgUfanF5fqIO6xY\nHjetywlXDI/faXV6LH9kBZjcH2S9LALcQFFGuzpWBj4Uv4mZMfIoRGdSfmgI\nAyZth+mv1TzZ9FuOYG9qOQkUC+Vi02c2k5KtU4uo6mBS3jpgsMvWnxpe7vjx\npauQKYPEgGmGNDKT2pC2/lNC3Kj6kIw3CIUQu1oqOlbCvTUbaKXWohVlmctK\nYosoW6K83JeOvQ4GBufDCoMx3lhLUPDAaGfs3YhKgF5rQK11Je/wgB+QR5Y2\nMs9Y+qqCADUk+xDHnn0Sezvpvm1F4nu2QsYvJLpf+63YHBrHCtcWaugqpjAk\nYXbucjDd7smelUWkaUb7oJWY9ea2DGzfBIuxV/IlMaggb0gzGqmtcZ0EW0NW\nq1Tfy5602e3qWf3gRk8BL1rKBSD+OocF7icCpz/ppkPS8JBoaLDK0DEszQBp\nNibFaCGI7G39AGGDfxBZqJWLnZWPhAD3rgVj1Oxnci9p6V6c70YO2gXAbmOZ\n2r+FlsKqdHVU6cpo5dxT80+5TevUlqmpYQoFTJg59/esrK0VsKv8vOPQGvFA\nU1sXKWnDFLDo8kfSQZO80hyMgfnsgxej32d+sov9mMmaOh3tCNxuBC5Qm6sp\n2aJI\r\n=uF/V\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDaCx4h5QgI/Mtru41VYym8EsXmix7h3SlJ5yKn91vRuAiEA8i9ztJKI40C1wSk//9ArAGmFTiIdxcYTR8oiuGLSZuU="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-12_1618273587749_0.8999011192581019"},"_hasShrinkwrap":false},"2.0.0-13":{"version":"2.0.0-13","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-13","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-OrlsZ3bzJ2EU0sxI9HUP/LXcJGwxpbzzUy5vAmPfJA/8O/LS3rEYjFw25qvZcNHVCA9qKzRftIcNMAO6UVjucg==","shasum":"e08c74d117078b4a84a935ef2d663515517643c9","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-13.tgz","fileCount":53,"unpackedSize":217085,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgdOtoCRA9TVsSAnZWagAAK0wP/0pr9Hic0lsWl76mzUbw\ndJLD8BkhfU0st37i13P/O1JvkoD7ywXg2FnOW1LuuNJwGYzD4ik6LzsIxVcr\nPgo34KXerziVDEeqRpNlLxE3Jd3DrigqHv+X/lH6jrLh1tJ7DCk7gsgl3bgT\nPQ17MVQQVbnfx1TeeUL70I5M8jV/00qewhy4z6BLsx+V6vDn1udf1h91k7sn\npg9qXUt57LY/qsQfZAvz5uY61l64CU9t8CO9fajlUVrUoae1mhNzXwEayVzw\nnI/HVszrxD9PDntcfa4aci7O77LI64/JPLtGgpsrxxYc9q3sauEDK0xrf3IB\nzIVfzVdW2gafN3T0WgdQ8MCbjWFFqCWWo6A+P2BYWg5bZmHiME4mJaSw8bnc\nrhXEfOzrOh56xuu3RQoccH0W5G/wm71lOViWhwx5IcRZAjE3ZMX4nSEMYyNW\n36nox/CUZjEyYOnCGlficmWHEXq6Db5PVMTXvpmqcst0xVKz2PqjxQ1VhIyc\n+2XzdUwGBHj3wXSkVmwMTFnABuux9LVxYo+1MqWe4iqIS958POv+fWj7Bn5G\nmIxm+ce/CFEbkmtr7QDvK1t54aZKauRghhL6Upgf2Ry0OX5IhG3aCaEBrPU9\njFzFspWpMBE7RtC+YMYPOkOW8jnx4l/1WdFxaC5rWt/lU7YpYkPVK8ekRAlC\ncbR4\r\n=+2/W\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCugymbhZw86xZ3DMJr5OU1UfaKQyVL/VnBw2aTI4pJnAIhALESZeYSFx/Awx74wJA2WhUSta3zBVGBjhOAWYdRjpwQ"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-13_1618275175719_0.5828835146368301"},"_hasShrinkwrap":false},"2.0.0-15":{"version":"2.0.0-15","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-15","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-dmrKsQnT6xlgaEwfXlFJ/KCQfkdJsOxgsGQBQoIB3Ssr+g6JV+0sRZbjrrbap6dOGwSz0xBx7yume+F1apNLWg==","shasum":"a6ce45da9274054377bf09a393c626c4aecd3501","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-15.tgz","fileCount":53,"unpackedSize":217385,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgdPeVCRA9TVsSAnZWagAAOvsP/0jHnaz/bldtI4ZW91i7\ncpxgunfUlBygY3gJsEhl6uOpUsh6MdaQRcQ54HHPmCyKP8Q+fiRrjfl4+JiI\nqsKCEPQVjx3Ze9nRww2gsgUdbv/5N3mJqayBRdqeROEm5jFaDJ4ekO9myV9q\nHLL2+VHsTtdGp15T1KMvmFoW3RqL1C2x+5p3h6IF1m7nSMDnCwdYbxvVUlyI\n8cluC/i+6yxJVOim36Q5eTWH/AC2MYJsxMn+FMcimivHsGUadDn7Y08KnhLE\nUzwc2g+OumAQI3/vSPe+V07dxP9iy/RRgR05+vO+BUtG1ofLCyuQZM7OufNN\n/BlfzAtoiGwhqwFracp/l7H7Jxk7BR/lpe4zFwsxO7i/+vU+MUErMdaiZOXi\n6MeUap3MsX5YNyEhUiHgatUCtjwJ5hwuRsRl5UaTcmFQIbj5aRNS3bHAw07z\neLFKe2LZ52hRRKIpSJXzwpunR2UaPiEnVqm+Bt71dhumb7FnZtnqgEpFa59P\nDni6OsD4LrwGXeampr6df4L58gF0nEoTPtCynEXj4Qz00BVLjZAHKL8oj99i\nbQVfZ7ty4omNgHIysLSpKbw7xhCEas4adGp85q5NIFFkn7rZdHgggvMd9sag\n8uAetx+lLeIO7grEZdRxwMo0mb6xUHcn3xhHfK5w/qFhOOggqI+nI/BMN7LW\nHJCW\r\n=gJsB\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIArLEUnYUbYWVgB1mgC90ygiAlQ+COO7qyL8f6pSYz2lAiEAspmFvL0QbHYwQj2r5b4A3q7gfqxwvr6mRXCqXG9CuY8="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-15_1618278293242_0.24621670868988255"},"_hasShrinkwrap":false},"2.0.0-16":{"version":"2.0.0-16","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-16","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-G0wKjlWO9Gzp1LTtnLNYWfAmdTcWhsyhJ1swKqI1Yq3MBtV9URGa8mE4pCr2nr1AnwMdoPH5ba2+hQeYfxE0vA==","shasum":"8ec39a967c42ed1f23e52c129dbe103d74dc3d20","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-16.tgz","fileCount":53,"unpackedSize":217740,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgdSaQCRA9TVsSAnZWagAARqAP/ieoh2JAVJ9f/FZeucvt\nHG2qzUp2XLDMWXgE23MgaKVneZhUd+XNWQUIdfabP7wX5jV7zGNInTduNJwO\nLksCPjisB+i8g3XgceKbQWM2vyPZDS+cwPhug7ty94GqwYKsAPeoDRsc6HML\nbi9fuVqANqCndhoDEMusKA7D9GIjyQvWPcaOIo+y2Ml6WwZNWmv14mqAUfuv\no5m/c1dENQcgM7oRxrOPXH2g/XP9EUUd67OvgTkR3wxowamXwyYYd2Yi34f1\n3ulkovYz7zyIN/ULeA28gwCjWJwJALduBJ/K68Q97iUWNyq9Dapo3m6zBVS6\nWJ9AblgYoAlTEszOVBtKeIgp5dJ8TN+M40lYCtn5Hi2caI/LrQhAbLj4Gyn/\n9SFNNDcXAAoc3IzFR9uD9coCMxc382sVudHi77ix5Sw0U4+x3QZbsY4Vdg9m\nTd1Es06YjL0NvTgWqc+6HoWBOtVuV3kC4eu9zPLAe7ZIK/PByLEgUCd96Lgx\n+qvrMGD9wkcrjdCApyEZ+JYiYLF4TS1+4QqSqYptjRYXCtZDEI4dGJwiFTpA\nmp7taXDWFEoufaC7rd/ITyQehH/1NuU+Fyh4Tl/Zzk4Nw4hSYJ7IcuWO7d0C\n1VK614RLJN7Hi45387kGJP4wZ0HEdWaPLFSUaRmKROe8Fwcp6b0gz012kFFK\nzMzr\r\n=ktIt\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBeWwV6DVv6rggUNmCDrBKcO2bUH3SFjQFLmwDdYXs86AiEA2vTX5Ppp8QsuVLpWfFARKM52UO2XKaM1TUBbJpOdULY="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-16_1618290320102_0.4031997061842518"},"_hasShrinkwrap":false},"2.0.0-17":{"version":"2.0.0-17","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-17","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-DeYZSFYR2sT7ayAkOrn4sx8ikkYjT49zri1OpHPt3Lzu8mPjDveDyNsyUZUKTCakKAv9G8RYVKR3SZjGaVhyJA==","shasum":"8447995c1e1274264531c659c83ec205dfc53093","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-17.tgz","fileCount":53,"unpackedSize":218466,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgdibpCRA9TVsSAnZWagAA20oP/2TLyd+lycxJu/+ymrot\n5QwYE1pS//o2xJILih3K16sZNEUwimSHP7pE/0fPTZFTw8FVD6Wj0T05cgA5\nTqcGYNWsK/CSZKm+VqNFHTvG9E1TPscNdiwDFpmnpLXZLY1b24HvIC0iNN4g\nxUcTqsOikHg3KBeAgQ845WlzA2/vnoaf79NSw5Z8Ltxdqy5RTjD8TEWRIGRd\n7cNUdUIAMcS2CWq4CkaQ6A9JCI0BzlXJIbkUSyPtpfJmX/2oW6CnClvde2WH\nmrBI9Wix8kGa726wzT1PKoD1NIXHyc9s+2nneHDY/BdIHtABas8gPKDOXMPL\n5lnC17sExCGrnQ3dl2KdoF5/0mHMDbte0Sehs6djMBGCVV/TAHFb1uXBTiBr\ndx9L/y9Vkd4dL4m/X+AnX32qjneuklmVq2er1/Qlj9Z9ZP+uja4y8aPOvy1o\nAFcq2R7QsCU7nCULBGJhrNJQBSuOZ6+jbG91VUT9e9DWRGSkS0MS/g4l4MBw\nG8KpuIvgMzlM1aLIlmU83DaSF+7ruVjRQPgZfkw/weJ9wHmu6yp0jo1HUT2V\nMcfCFsBDVk5X5cP/NJ5et8Js7nQhGVFluLgLXnbZ7qirqv8pSA7QWyDWNtiT\nXcpgMeSf6/Ec2RuBex44slwcVgXoZCrd23KFksluEjqVGTAxW7JL9KY1hGwM\n1ua8\r\n=QOwK\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDAOz2Virdq1w1VDG91cpatGvB2ugpqnmkLVILnMCX6XQIhAOgNTAlFWkC+UB+fqmZt2c9QadBCbPjhlc9Wk/3fFzVU"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-17_1618355945277_0.1319492883365132"},"_hasShrinkwrap":false},"2.0.0-18":{"version":"2.0.0-18","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { data: { id: 1, _links: { posts: '/links?userId=1' } } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { meta: {nextPage: '/users?cursor=base64cursor' }, data: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { data: { id: 1, body: 'Yo', userId: 1} }`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.like=search` which adds \"column like 'search'\"\n- `/table?column.ilike=SeArCh` which adds \"column ilike SeArCh\"\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name ilike ?\", [\n        value,\n      ]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"meta\": {\n    \"_links\": {\n      \"count\": \"/items/count\",\n      \"ids\": \"/items/ids\",\n      \"nextPage\": \"/items?cursor={base64cursor}\",\n    },\n    \"_type\": \"items\",\n    \"_url\": \"/test/items\",\n    \"hasMore\": true,\n    \"limit\": 50,\n    \"page\": 0,\n  },\n  \"data\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-18","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-vOHz5tzNrIeU1pYeEBe2fbPej+v8+YNKsDsY9vmBdfIgXAp5W/6OIGXqY9Msy52Ucf6VEt9Mrcc5B5nSA+3cbA==","shasum":"b82f8248340e001b144fc64f84bdac99589cf840","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-18.tgz","fileCount":53,"unpackedSize":218466,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgdnLGCRA9TVsSAnZWagAA9lMP/iy7xe8CEgW7VGbWUMed\nRVF8BMlp6yk9lLAXmg8+DPXD5kCQM1o7AKoVm/zsbCEyx+lnsPl6dAlnLbqN\nTityjtuhc86SFCpN/3vGJhaRs7TCy0fnJf957/jO6D47rQkmffK6Ox9JPtFu\nvDE2EYmfjRul2z5s/23npy4CO5CTESkjfsMX1jRhI0U9zfYwJlI9QEwiFkqx\nGZDsLEBsc5R8P1TF2ENTaRdcO29DKdLORTljYaiwX5MEkKXnkageDdNWedif\nUdUdBLygpgsoeZveNAI97s2wi16mMKmY86WFlIjPqA8GHq/J9aV6iC3Eca0L\n7GWxqPatmizAtGuvswBoij92394VDzaRuU+UTAsjkAr0Q4lZ6Fb7+zE671+r\nlHzK55ZvODtnUxFy0uzmwuEpZC/fKjAsiLGiUjifgRuDbeZ35t9rzidseFDF\n/wOPfDE10pkXMsGEfAFyQlataPpdHhoVkvqhV/slytMCahIa2oZUCoTq618V\nvuJkAqBU7zfABOVmhJcGLGTKBIkb6FE4HNjfmwAwxjhid9wuMEqhJcJiv+fU\nOQuk+xtrAN5UDISLMsEbG49XhOEculXQOr1RYXlG4ZL9MD2fRYupLYx4yDmc\nAVGpaM2SM7g2nC3OJzwdCkFKAmM0+sg87FcV2SBLjRkg6K+ytyaEkuRIOd6C\nAJm8\r\n=Marn\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIFZQzGUTNBmO6yEBNAU2OzoyyOk1tjD11OkmNXAUujaQAiBg/uUdBgDu0UMDQQh7k9aTmCf2N45hx8KTrSXNbuoXIQ=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-18_1618375365699_0.14039743629099255"},"_hasShrinkwrap":false},"2.0.0-19":{"version":"2.0.0-19","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"^8.5.1","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"~8.5.1","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-19","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-1Ig+EBKfooTS5E2TyxXbl2RlhqWmfCOOttHsknWaW4wi2+DWLVM+ggfrWb5EwoswifIHwIVWIygzLgv1iNc39w==","shasum":"26ff0ed507361dc16f4cf641494eda69bc4dd46f","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-19.tgz","fileCount":53,"unpackedSize":224120,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgeNA8CRA9TVsSAnZWagAAHZMP/RCnL7A3hSHv1UpGC/lj\nu5xr5/M/WuTCiZ9lQc9TWqqGtmi6zAqK/Rv0SyzLy1xZjcgUXB4ydEeLBeEk\nT8F9U3cpvKg6AxhubBOgECT9akBEvLL9l5R6uIPvxD2mYKMpffjs6NbW7O42\ne80/8zKhNa0SPVRza14JKFspXhjhajevCpl6qoa1t2hPqSZMwR0F/bfmPpAx\nHLR/R+icLwMhESoCrmNNuVRiGDq6XZ0sZ17JBUvLBfut5TbnubkRfoj7dJoY\ntBYmIxfvafn+VJd87JfuNDIHAODOXl48mjQ2PiF3f/juqV8IgjEicZwGLCKM\nx78Of8skVNyD9TTmXOEYFsS7+9OfwwJVFGeM7ydhZoXOgSa5+mq31JZFBmXq\nFlutRUxls2jc2UAkE2bnQgDz3diKVghEpSPd25wXuCBHpxVm03jZtkF323xz\neFtUHo3jOcmMYdQ/3kSnzWPpAWM/Uk6y7iOMuOfVlXehgHUKDZiQSLf+WwwJ\nXUQfu5QTOYZtOCY49TeuMhLJr6OqYePs8Hhawhyem6pfvtRkeBeEt4H0o8V1\nlQM/qQWLRkdHonQ7ZhSLivyavkhwvQt7OxNSfkDc06ybAWYf34zpqeHuXTDc\n5yibZSEdl+k5MI7KrCN+KXQjmiBIPnNALvW6bTy4cf83m0F7rM+XjpPAP4Pa\nyC19\r\n=afch\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIFf/5lS6gsR+/xpDwDkj/r0t91ta3tKo/sufZBBuWIELAiEAsyTe7QTgri+JmRQfMd0TCR6wJl0jmC1UuPY0LvugEFk="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-19_1618530363616_0.016475962078532413"},"_hasShrinkwrap":false},"2.0.0-20":{"version":"2.0.0-20","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0-20","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-GmNYL+nKnu6AkBNFZQ3cXdemv02D1G9fspd3VA9eNymFIgUGtFKPmNE7cpe/16ICs1ZNuH59CUBTHN58r7Ievw==","shasum":"5d9f31008d779236868b218b981d62d7ae158b8b","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0-20.tgz","fileCount":53,"unpackedSize":224119,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgeQGlCRA9TVsSAnZWagAAeEUP/3vHhXxOnvDGIABRHnwU\nBnSL+fs1zKG4g2PStR8YzIFyitNIJ0RJ+jrnnsCq2v1dxSZiv4uUlGFuC15t\nRGnrtxWhTiqX6zJ2zGyt8orYY+tboO/vPQoqigrXWaR87ggQ3VkEpaHZeSlq\nsRDg5GIkqBEgx0UCXpkV+4+HKLjhuTd2G/2ktIMnY9VJIqJWNrYayo31vgfL\njauDWNnn8lStbXK2gW/t3mbpz0QYijq986b7Wxc/uJxW7i4dkZ1a2emomyEQ\nGJLvhWyxr9oolmNtxtkIriDa9C2G32LHW3YoGK2ethp01FpnqjISjWV0sFBw\nPdswyChRtzzP2/ytUjI4Zi7CpxXIDRRm1QYjwDs6dBouGvmpNpedEbZlV/Dg\ndWd086d6uGjVCE71mmsSImbM2nDDH2LtbsVFonrjEcWtgrYoyOP07CrctcFv\nUEgnGUZ3njL+aHkygi9td8vWshy4H0Fq4sVftqGcicOEEOjR477HUn4EHjo6\nLbEsaJXhXO1nh2VePUfVMSLTY2Ni50F31S4r/I2j2f52/8xzL4lm/9po6miV\nPTykpJIYUNcJanAtn/IJTG/aFawqApeIAYPfEGxhdr4odNTh43PIpvLAOtEL\nfFDEX9LFeke6X//jjeaObaobxK8Zlq60QgMHGCpwzArSyjZT41lPPZZ6z7tM\nvAMI\r\n=qaI1\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAcSLyEmAncrF/TFFuPxQqcuh/4htO/+d85ey0UnLCSdAiEAn40dO3zPh1moY5kZJ6pnUwW3aDe0C/ggNLRyiVG3uy0="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0-20_1618543012794_0.8412152646932978"},"_hasShrinkwrap":false},"2.0.0":{"version":"2.0.0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/atob":"^2.1.2","@types/btoa":"^1.2.3","@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"atob":"^2.1.2","aws-sdk":"^2.868.0","btoa":"^1.2.1","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","_id":"@synvox/core@2.0.0","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-eqpn2lDo00YKFxWd7t8mxSpeb1vDpkBv4iYBNSUKYeX8HIkFcsxo/SoPEGBlIu9JImljg/bt65mRPuo6rHNsWQ==","shasum":"1e5baa9a958f9f71976a5732ce25fa20e464e12b","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.0.0.tgz","fileCount":53,"unpackedSize":224229,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgeh2UCRA9TVsSAnZWagAAGccP/2jQRNMcanS5hj1vVo27\nl8kdZ9/xLuvUHbiyENgE+y8v5pnR49R2utUfHuXFfR42ZfGRFmgmytCh8KMI\nu6ZV1/7hWA021pnTCZevhi1PhKkrlAlvkBVE9Hucy2WH1at5q0twU34/GDIQ\nXhU/5pReCtmJnRGPFwUx/nCWCxWkX3p9NEjYNhvxWeSXyPOHrhqxOmxhOsvH\njqIuwhIsdLttSraQMSE6zkJhUX5d0dAeJxWL38qrOhqLLXVZ6aM82UHGo93N\nLKAuRe7+GUsThXU45jne9/N5nLbfpkSNJILvSTBfF9whhFny/+Yz2Lzfi32O\nIo8tH5rXjJBOdvQcFFtiVlMT+RQBtHQhwtB54EYKrnSO+AEFKCpBxYIejtS4\n5hLFjH0u4fqrEhhEpVlOagN+EhwWq4xq1wZP80LNbAyv2zT78eDB6DpqIDWw\nNNWOTRxyb1Hwfn01zj2SaEbmUyHw2ZmII9NDG/NipsyQITh9SPbShFVYuyzt\nzjTFSNoSPiVA5DkDHSRrEVA6eV5ivV6fKmZhum2PQ61OUU1zXKaLXrvBXp0C\nEJrkGThiti0igzsMDof2Q/42qU3bQmbLPHzNq3709hjTFIZNUsu77roqAwtk\nyZ7QyH5bnh3sDh+yz812s2xbkkwGLin3ze1BwzBp+V/CVeVImgfYFo82EUbC\nzXed\r\n=wRvw\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDd32MAGYJ3T86lt+WXQNyFxF1Fs9M90iKfhZMA2kKR0QIhAM91vc/ZxFcPHGEVsbnEMeBXS3OBHGllfoDdUHIRoXOQ"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.0.0_1618615700123_0.0199677505264928"},"_hasShrinkwrap":false},"2.1.0":{"version":"2.1.0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.1.0","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-oXWbAI4f2bfAWHBtYQXxf7q3SXjnjG8m3hBaXe2S6V2SuTm1vXlam3diqAFIhOzcmFUjTTDOJ3qLyoWuWsO+cQ==","shasum":"168237f26764deda998c70a0b0a8ef9cd30569d2","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.1.0.tgz","fileCount":53,"unpackedSize":233758,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgf62jCRA9TVsSAnZWagAA4v8QAIK4rUGzi+2urfqt4r/3\n5IBEDwhq4HGs4Iq3QV9KT/PAHBin12F8IpRYFyDTNhp9Sb3kTNbzAa+sicya\nS9KY7joxOAuW4s+pH7tG2RYJLPs1DhZJSMI4nAUHjBSU+aI46qQwUynXvXir\ngNITO0mbs+K5TtvNdo/SIZ5+XrPUypoO2qteN3rbqVCXNr+SbO6NGGAik1PW\nvq4ShZqNbNAk5TWfSbEhawY1khucypquZJcJ3mhOl/PgXhl7bS6wwqBbCIs7\nI8Q1s58gNXBXDUe9+G35llM+tPAtxDDAya6TqRbCkMGa+rhatLFM2r5Tv0K4\nbx5Na7ByHTAJJba1qUAJ2yIaoaElryf6ZZU8OgZz8RFzBb3V0l1whgoFipzL\npBtOYlF6OCxVa/jSD4VGZx9zM49sSPAiCgHSAS50768m5CA/9IKDNIhqwwiw\nYB/PWme06VNusi/GGj9M1uiJFfDz7hP5dAk5NGbqHnVTv9nyeOLv+HFE74aZ\nA7G8NKIlMsfm9YPvVDux8W44WzHBiviYtzk3/VMj7AKnFc9cAoecf080cyv0\n5IeHRE2doN3izaucOwIFIs96iRVEJp2vvoEMGyLvZH4hO94cGGFz5Xbjcvxs\nHZ/HRWXyd2WHZr9m1n3jEQWs2g1Jfq0EXPy4WxNbk9Du/JE5ePcQkubQf8kd\n2+Mg\r\n=jV26\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAEzUNpstlA4Bc/qA364SvQlvdfAjjn2TIrDAXGum8bBAiEA+3eDmpMA2VZJIl2wcCuZg0l/fKFL5dArsAX+noosKQs="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.1.0_1618980258464_0.6978457878339268"},"_hasShrinkwrap":false},"2.1.1-0":{"version":"2.1.1-0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.1.1-0","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-KOtnh7bFWpMFwigmrlw+hWFq/OKaxNxANqSDJBpQS1bghVxJVU7T0IRcRZQqOl6m4yUDEugL2RPz22l5fquI1w==","shasum":"6398e4d8d3aa151e2fde9cf3c0045a713890595f","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.1.1-0.tgz","fileCount":53,"unpackedSize":235872,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJghkzZCRA9TVsSAnZWagAAkQQP/2o2A1v3cq0a3ju578vx\n2eAeBNuUw9Prn0pDHiZwxhblny7H9e2n+e5AhJK0s9B+iC01it4RdxC+83Tx\nWT5kD/DpH5iTsjRN2ElARhWlYXMJUJ2Gfiw6jZlIJHyPlrxILn00R1FTtVGo\nR+wBAPnVTapqqIR/4JkNasspP31lYgt6iZo6/IufVJpXb6L4/tw9mdi7tord\nZPH/RB6IF/kCmcgF/0N4ZE7G3srYO56O1w3X4pXkAdiZO1vVMlgKC9VQ3W8j\nl/iB8yurjLysoLke/3AbEe4gLni2PG3tX39l7AoiiiRvwlESArt3KHmIH+qr\nSjREXF0Pm73DXlkPalhsFgJFiZPU81vqibv0ioFeBsevGF6B3DBC4s1uPwm4\n2uiND1OBouZfI8lp76PyecaMlzrQ1BoMJ9/l1K7iYyK7m9sYXdj7SLc6O8U6\nzlCU8ECdgVZSk1N9JMv7RJaKNjp06UsZ4sutSBcjuQxvSF0oQQcJlkkh4LFn\n/Pl9/yFeIYvQsf5sfNd5HJfCYvLwW5sPlcl7N6RZWz5SFp2TGTA9dsT45x5Q\neQBpVo82QR3WhFP6n9EUXZrnQf9RXkQlIL4LMbFilSXonzC4HFjDLFSEXO0j\nezf9vBYx6bGgqu3TRz2y1zRGzKvaiWUujQv2SPhP3Go0gAxLEZjdlzhDs0nl\nARhP\r\n=9a95\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC6m1M0V6e+LevRQRw5b0xy5OaoVeEz9C3OMT3FYiNuCQIhAMEnRd/BM/d3hshZipeNWGoTpHg0hxORaQIzFJYcPy+X"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.1.1-0_1619414232925_0.6413291739202143"},"_hasShrinkwrap":false},"2.1.1-1":{"version":"2.1.1-1","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.1.1-1","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-6iTpXkLMaFWUE/2wBuLCNrnKwayUO4KtmGMyrMZcxC+CAMwOD2gq9rxReqOL27klvZpu3ORFQVLioNp/PcJC/A==","shasum":"a928fcdc20aa5a2a2c79963dc54f46d0b39c28e4","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.1.1-1.tgz","fileCount":53,"unpackedSize":236023,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgh3bHCRA9TVsSAnZWagAAk4IP/iLypPDxYueK4KjgsMRe\nbygORM44s1MF60QlNSYg8m5Czbk4rcika9yYen4nWYhxsY9EagqhxUDn6CLv\nP9hmT0vcJJgx1W+RZeJEYBZ9cr6C90GxhbUaOnb3NzvOVA0xm3ndAqAV4uZD\njKQKe5ymBzLahdRgeZvkHslP9Uh1PSPHEOuCDl8jUKPsCjhmL2I7fG8OYIDl\nLbJ3usDY9pWZBfbeAxDurJxFBwPEgaljEcA4g5qErrxiEd97yTbcfqQ7dHfi\nQEzmTzs14RwsErsjiIcujYyx76csPzFC1K9pGYD3G7sYvT/UnpeWHkl8ld0s\nCweFHrsKEP77ZFuchDw2Fa068QRELpKDR+nDWmqKPmTOBCptId9Cw/l3Fail\nW/z60X2v9cYCPBFpUINN/HW/rLeLTcM1Iblqin+SuaNx5Wbl1gkzZwmDPMBQ\nljShbk4J6VpdUHjMjlh8OuaZJyS7m2pS9MQgygwD5TL+8D0amQ5cokOX+Nmm\nk9ZtTePXaS/bxSXssqxBbsCsFOfPV6Hcpjl9CFp58reAjVwuyv9ARJWTlHf0\nn8Jk3GMub1CbY/QDtGd+TbFSOGDgz7CdyfNcK0kjWYo03A00716VtZJP1hGF\nS0NM1piC642GV/OfuSzKhmtRueXP3gpKSKqoNrS+Jldyxivt8MIriyToZtDr\npfZ4\r\n=dyuC\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIA6LLMhKTV/Lj9DvBKYsmMklO4EisGb/JVW7Sox6k0rkAiALoeVydZCj2ufY79kG6ONNQvj0A9moi6oRphPCP/AexA=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.1.1-1_1619490502567_0.6097226512545149"},"_hasShrinkwrap":false},"2.1.1-2":{"version":"2.1.1-2","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.1.1-2","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-iVZJ3+DM3ok5agSXvBcqp5aXB9FZdUnNqOLbzuzfsVpnv4tF8mVzp0Lg8NQiMW85jNFOaHdoQeQF6OXut+yGUw==","shasum":"8d92bcb7bd1a8cea839c45eff69365373d4b2d63","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.1.1-2.tgz","fileCount":53,"unpackedSize":236060,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgh4KeCRA9TVsSAnZWagAAgoQQAI3sHPJa23DzPZ6anCJB\nZOVkY1A6Tpvx07cIRs9hNBmtA7V63SjhqFPQU5Lhj97+dqKd0Ualb7AtAWrW\nbja0PGHG4fuvBz1gkJWVBcIJw+/JHzkcQEdwJKo56qU1DEIqwfHL+x6y7frC\nA4WpGXswsW/Tnc2AIXNwkizZMYtSTdHWdjS9bgXTBy1jdinS/CTH7B+ssECC\n4GY3MnGgtA1pS5CbxeA7aQyOH9J5v/DsRR862VlxFHw66VfmPpDDWBj3Ij4d\nV41gWoB6xu7nQNEzPxI9g6HmNieK/G0B1Yh9vV0lgF/rab6QKpW5poRk5BKC\n1MCT+wAzOKC4SbUoDAZkuh8MdLnUGmv1URvtZePLF9xUmtaqYIe3tWZGp5M/\n0gw9vgOfW7RT3Lliw8fs4Rn2JAovVLCKZj4oWNfN5W0sF4Q8Tof5DJvBh24y\nqU9pHeJR4vGgl4vBKjBscHrvDALfNnYy7HztdTTS+MT2sKx9Ed+MlRsdVb8L\nub8q+a7joNkEmXj1/PDcrHdwymdqmezXnxgh3ioxiohUuTMnF/d7hk/C2n0a\njNttnfIGXePlGwQyZqiCBMgrmlu/BqNAKmaYNfB5araVjJZC+nSH6jko65qi\n3vXPGNb8JBYq0D112cB5zVDmWcTQesGsVauzWNZ3fZxsBXOntH+tl57L3n0G\nQLuQ\r\n=hPDP\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCwt9Vk0PKwnQoOUUheifGxQkvmJGBNkwSF56zFz5Dv7gIgKVp3Xw3mA7LeHfFWpfw+jo7SfDUqFDChJOMKD0AGSbU="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.1.1-2_1619493534299_0.2861273098574437"},"_hasShrinkwrap":false},"2.1.1-3":{"version":"2.1.1-3","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.1.1-3","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-0f9wA11JP5b4+hfj1/MaGS4ODa3mDzO9+gzdq2ZWc2WEptn82PPC7RtoPlcRTbpVFCRRYJmtGAzRUcM6p+A0hw==","shasum":"97ad8b2e4221ebf7451261bdad5b72449f234c00","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.1.1-3.tgz","fileCount":53,"unpackedSize":235954,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgh4rJCRA9TVsSAnZWagAAM/wP/0Xni2kf8HERLbXUx9Dg\nb4gklYaRBK71uA1DyzMtkhWBsdjg5UVb+lyAxTCH5joMBlUcxcllwMFEgLqw\nhWOiIgX9y6IvwEKBkCw9Hf6A/9RMv0lLERumeBUmWCcPM5sQ/Xy3Gp8br4Ai\nZSQryZU8MROVheErcG3+ukGPJ3TnHs89HmwZqSRljVsOrXIfysShqKANW179\nMQVuCawfGXbPtb2co/oH/QiSHS/qmxhVAoIPfBcAtAbs1/llKV8J+Rs29OKv\nmcwQU+LaoBye5qbwB3RzSYXwdLKst7u+EaCm9C/rOwEWkRAxVFFv9GGf+SPA\naci83b2voM20412e1tFNnNBQ6IrQrUKDexV9w7DffHtZQxsnkdmPeanwTK8C\nEL59Wld5CgWAJfPbAYn9oUGaX3sc/s9E38D2dZhxMBLPuNSm/ck2DreMlE+9\ngV5gFOh4GmlP3EkaOejiNMFzziXlgQE9xPlPKvVWIu+h8fukEpD5h9wU7u4E\niVa68Ok+unvZTPMX7zNawcUtasWFPiy8XI+S5Bg2tJ/e2K5/v8AzN+7mQYjp\nmTmLJMsxRiR/e+e3+AuoHgwc7kCRrs9W5IFnUqnH802vGWqKje1gZdeWjdwD\nVr/4iOgLZyEoUYgMM/1RI+PxzKprvLiRsE4LeWNGkb0RdFZp2GXHt8vgmZzr\nleIc\r\n=Dztp\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCrFOVFebSXJ3wxdEK6udvzKA3JEQwAOgindM1et2PmBgIhAJzx/3lcumQzL+LyecLkAcCe/XQ5pFx+2Zr2b4wbEy2Y"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.1.1-3_1619495625370_0.8080556207889891"},"_hasShrinkwrap":false},"2.1.1-4":{"version":"2.1.1-4","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.1.1-4","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-VvvxMrgWGKwI1biC5YB6aQ7I4IToYho+tSSs0VPE2OxUysOnv3hEPLOVXvX5W0RVKR7cxpOMWOyju/2ge8mHoA==","shasum":"338924f6bd65412bc7da095685fb35d8da191f94","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.1.1-4.tgz","fileCount":53,"unpackedSize":236041,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgh45zCRA9TVsSAnZWagAAqH0P+QChXC43z772vdUTUNpB\nlfYEwHoe7VQbZrXpzULA1kbszYsAlw+uSq39imkkClNryYb6w1LrHaTWtCCY\nvVjJfdMiI/VvjSdWNwRWIY51z75hPBTvxcszjMIgIjg5Zx0Nat6L6pC5Vgrf\nDy0+UcsKR3bfmme4q4K6OMtlX4JrqlYwMEQINLDYIitd25WpgoI/bI2Uz8h7\nDVc5oExVl3acFT6uZZRsLhJ6GbGkg7Q8LE72gI2lEuQcKZRm3SPFbE5fJNl1\n9jUtLhqUHIXgAOuxE8b8rTJkgn41cDi8g2UAJZyXDbx24BT/xwIFLuXLO2Gf\nINbovRfrI4rjQz1W/8R+DVLMNB3nhWPHnkOa7H4A3WtuntPhCqPuIzpyH/MF\nnXXYBySosvm8B8fZaCVJ/1hO4hoFHfalXKSihsl0sMVjvi7tfpxVQlK9JIVk\nbcJ01qq6oat+EQRsJChsp9y0T0uFEpyTzKUlW74mVbH3FAJ05/ZHHUbIQjVC\nRbUifTPP1Z2dhNr5mPNxkB34ohqqxjJormdC07OgDZ6FuTQW4uxg9rdGkrZW\nfnDJCevMJwQOqvamulKxqOrSrYJVLju9fyma9xe40RqJGL9gYfuxIHcD2MKF\niSvBCtro02CMgBVRtf1cFQw06LzxkVnOnVv+/JZw9nWCPqIv2hd5rAQtHkL7\nCGgu\r\n=2QxF\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCD9wSDMPpkIv/WRxWxByG0KfncdgZGPYmG4E1qii22iAIhAJanHUnXvIcaacsl/eUCNOpo07n+ewTBOhwK8LWkAaiM"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.1.1-4_1619496562706_0.037446973360091285"},"_hasShrinkwrap":false},"2.1.1-5":{"version":"2.1.1-5","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.1.1-5","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-zc1aH+ekd+PapwntKUMLvcFRRuTuyt0+ec/IMt03TTUa/MXlDnUC3Y7MEJMmjbaEfTATLwxpsWZ2j3YPJk5FLA==","shasum":"1f0ba3010fac03a2d30677b356d33221196d8116","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.1.1-5.tgz","fileCount":53,"unpackedSize":236025,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgh5A0CRA9TVsSAnZWagAAHIEP/3eh3z/jb3mQ439tl7W2\nbXx9eNVaifP7OdRpx12KVVJ69vVWWRE73UsT3Aja4JxGWBrOBLYmYMz6xX9D\nyi7RHl8Y7EOd5glJXxlSnZEGodyrM+P/gnjLSxd5nK2fE9+M11p7/qLoxppW\npR49I21AgxefrLBb6bHqdMVnT6H+++thnrt+7NMyfoJhARhki5zsSueOC2HJ\neyu8WMxEnocdpeBGcHJRvqRJbWEEOE+KbMSgOZpj8NvNwSmbmRjk2YIZdFy+\n+0vlL4XRZIS4KAKv1BMEQdgYl6Mb2dvaSt4Zy01jIbiznrs6upma1un9m/ME\nSIaQRoF/2MD7ZCJcCVREscM6UtISPy25ufgTE73TH/8RX9tuohE3LNXQy4e2\n8PufZzHf41br5ryWhlzCpZdNo2z6FCEW+HJhi4A6wYVp5kg5ZlqTKl7df1WO\niKRaI7ESDgZhXgwMhAy2D3UX53dPYvF+bNR08AGP/ry3z4kBbWcCRXFhHVDZ\nAwRFtE/268hginvnu9C6hl3naI45FucZMUYAkhIwCpJ8QDeI2QIFJ0olam0+\nhArgBMxl9zitYFL80TbHfrLhYwLq5gVaS8HgSA/JhQsCrBxI9LaDA6u6Lrvd\n3xZePjLtBMngtnA0TPziE1ZQ9P6TYo4ugIRriJhSDAX+MDTgSq6CJE2+83EI\nXJ4o\r\n=NX6Y\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIGN67n3KohFq1ORPYZPT/AIeqsHoEaaV4q5DEePi5XoZAiA0P+G2kNUX6/tatFDr4Ia0vulcI8uLWOp6iGlpcoXHmw=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.1.1-5_1619497011848_0.49647309408034657"},"_hasShrinkwrap":false},"2.1.1-6":{"version":"2.1.1-6","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"devDependencies":{"@types/compression":"^1.7.0","@types/eventsource":"^1.1.5","@types/express":"^4.17.11","@types/inflection":"^1.5.28","@types/jest":"^26.0.21","@types/pg":"^7.14.11","@types/set-value":"^2.0.0","@types/test-listen":"^1.1.0","@types/uuid":"^8.3.0","axios":"^0.21.1","compression":"^1.7.4","eventsource":"^1.1.0","express":"^4.17.1","jest":"^26.6.3","knex":"^0.95.2","pg":"8.6.0","prettier":"^2.2.1","test-listen":"^1.1.0","ts-jest":"^26.5.4","tslib":"^2.1.0","typescript":"^4.2.3","uuid":"^3.3.2","yup":"^0.32.9"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"]},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.1.1-6","_nodeVersion":"15.13.0","_npmVersion":"7.7.6","dist":{"integrity":"sha512-1CDhSsRjuWbh/v/nWE6ym1T3oU8WW8qiO8CmXGftAc8I8hnlpeFobI8Dz+DdteQ7pmz8LFP97juIihI16mOxCQ==","shasum":"a5c797c9417aced46440409de1447327f6c0f32c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.1.1-6.tgz","fileCount":53,"unpackedSize":248420,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgi37fCRA9TVsSAnZWagAA5c8QAIFHBZ+5957RqRpiRDKA\ngLAC6QvSuWJ8RnPaGhKLpuC6seoKfnp43SUhquMYHcimWwnkabkYxU+8sd/4\nTF74kVLaPfGbnD5ILDJ2eES2jGM1vnFocK7f1jTDtxbWowOLb5JpnguD4oT7\nxlyZriGj42SvcYPUHa290UDHxHR0LIIs/h4vtc8uAxLVa8s/+ejpemx0boR9\nanmeqoATxoJzq5/9/OZ/E8oLFUH33eU5S1yMBx3gIFMNtYpej9AsR9YMJ7nB\n+2Y3N65mlB0Hx8LN3MIndc5uJi5LK9KEBPKbfG62NibC/YQCcIXdlbI0d2Gv\nMdlCHR4GXA8+z4J0NxPisMxQjTCnibQRhHdnIuYU3bB/xhwoJf1Hjb28HaIG\n4eZvFIOV1hhGmYSDk+RxSL1frIejpoH9R4OsjLY73qB3gMeZvPRIkL8qXWQA\nLuK+WgUxJWybMQgFx0dsL61NcebL/Ts/e7U2+xCQk2U1MtSJ15BikNB3HEmO\nbScduQ/SrFw0qSwE0DxED0h19r3f8pFjSqDFK0TD9jPKkKPMGzmDpnu5hx9X\ntfFNok1oddxoWRw5CrUd1TDIhyH606pEfQxCdRHZhGVG2brYNvowij4DYEs5\n7nq+ZrzOzXxfayyfkYAmTftWFOz0x7TnjEDS6WLxYGLicvZSAWYVWjpLkV2h\noQDT\r\n=K/t/\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDRTX6qOeJxXzbe7psxaGFGJzxiqixhx4GXLm/GyUQMowIhAMEfmloNxrWPMDdtOnA4iApWQGn+MM5h/tePLo+IU93T"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.1.1-6_1619754718611_0.2677269033585379"},"_hasShrinkwrap":false},"2.3.1-alpha.0":{"version":"2.3.1-alpha.0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"fb91879bac1fd2a6040d461ee07e1a9bc8d7521d","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1-alpha.0","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-8Xbc7zSga99jVCZ9p0pDQFPSn2IthxDu6MDDUj2O50+b7PzaYDtmbtrG75leQMXiD8nqdc1LJoVdm7K0mr20qg==","shasum":"bdf68cb9ea0ffce9869f3da788ee56e71e3d8d85","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1-alpha.0.tgz","fileCount":53,"unpackedSize":247855,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgi4+JCRA9TVsSAnZWagAAtwUP/iXzn6GgsNT5FLCgDBMI\nXYTD72hWjbZyf6cL1L7q3TyTcO1co1xNZ4u8g3tPsU51M4k+cln2larasPcU\nSfYvu++F9tG+gDBI0zOG1lSdoDZ/VdbYc8oCq7PnLs3I5myFliJudf4yV0C7\nJSuCnUsJVHiHQIOSsYf3KTpmKipmdyxZtvxz02thcZIQHkmGCF6L2zVQmFQf\nC8wuOhHaSblstBd7L5W6as6cIJN9nkBUaISgAJwsO1U66PxGFJs3+RWA+kZp\nQy3PMjNmMBwc1AQWG0dw7V9Yx/qAnQR9EtGsuWRyYcTEUasTVpK1biuuwRQa\nJI7eC1ZZZYHZWY9IJGtCetDMAdzVJ6+3TH3D997DvJbGcQ/P7+vK4UfZZp3p\nkhx9j+IMEJ1OkCxUW2AeGQOOvOYHE++7i0dmelPX4nW10bGSsfIfGmlyJlmv\nd/4VKN3md4fqHPppqCXqLq3U7AT0kA3OjIRFhaa8CrFV9sdqxRY4vBKsBYAx\ny5RaYoCZgJMj9WEVNn3TbCny+dIccCykyhRfft6WbFeSSeas7VO9uB2Bwdkf\nf2ilWZWwLgyf0V98PXCvMGW63soQTYiOZLRJdsb2yn2CzTHgQDbLL1UvBu1n\nsMHIkFzMVglPCQZFfrTlEOrXQCU8oSLm1Ef1ROWtKOXQyQXamt7OgRbD/X61\nfaCL\r\n=1QpY\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDr8x0vLYCj/TJzZhySjbYNEIpeG5KNuZsAAwiu7XtcpAiAZRpO/uoc4K9WPVWixZvmaK+qyYYWkT2o8RraIlzLyfw=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1-alpha.0_1619758985354_0.6906279420579742"},"_hasShrinkwrap":false},"2.3.1-alpha.1":{"version":"2.3.1-alpha.1","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"4d85d41ce666e5b8aa557f67193580be4d2f9d6f","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1-alpha.1","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-eKiD8Tb3zdAjzYGStnkgNDQL0x3U1+2OLXyIXhncAKGR5uq8oAAmt/AF/ua3CODovB8oxSPHnmTfQtX8jW/edA==","shasum":"c3012c09bebc061929b417e6ca77442bfdfb9272","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1-alpha.1.tgz","fileCount":53,"unpackedSize":248615,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgjMTtCRA9TVsSAnZWagAAdaUP/A6/Gqrp+keNuoqjSrKQ\n8ypzPOJFOLrP6gyighamQk3lq9NVCpNuZpbKJjC0nVhXwBx9cmtIrL2YKN48\nAsYlO3Hfw8SjMh+Si8bAMKWTIcv537mCcM5NbbA83vz1G+GJ8UEqAQv5UChG\nWQ7Ysj4i5XIC4hpQFBk2lsBGweJiv1qR/QsH1b+XvfYD3DnZbGiEsqQeG5Qb\nFyEqDXLw/rHHFYr0MjFgRy+0dbvuHH99zm1GNgsl/ygWy0YRL5ZYjTHtFbgi\nF+V6utOjCWPr5p89IoisHSLbnhtN5YTWMGIjXKfTdAbyROcddT0SHyldOVW+\nJxnZzCTHQ+RojD2VaRS9qmqLavx+Y0XjEsPtWVWrHgIv8sYBigxTXG9u5u+W\nlhLIsxLF8h1UwoVtX1s0T9lQKCclmdaHGKvHSp8R1KbSF0+abnk4xLW5S0q8\nVUl+yPTGz6uSAdZENCIO50WftmOMkodZv2knwQurFgt7eGTWQKg3JibyliPM\n6ECoD7Ip02WvBIBKj7D3LPKxbXIJ+MuDUlJAsW2JyS04WOkAbjLhQbiqGIH+\nSOiPT+t2IszOI8z+EShlwCz4ZLtWtf6HJJ8SnP68pCUnLhxj25d4OBUKxKNJ\nnUcGV94Y3PwV4WsCzNTNO4Am6+eBpV+sXOgBqDE8gEOe9+ffFTzpXz7dWvgi\nCsmA\r\n=Kizx\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIGK+XWNApUwvMmKBKQyppuzmVbwbYzGsGjtVDGaVNsJNAiB6nZz8ES+nSbPw/phXRUHpJpTMEn7oSy7n141/ubspBg=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1-alpha.1_1619838188649_0.7747220942580715"},"_hasShrinkwrap":false},"2.3.1-alpha.2":{"version":"2.3.1-alpha.2","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"adc74bca51eecc341d2b3060ab252db2fb533465","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1-alpha.2","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-Z4sM4AOEhVOCOuikaymQjiDRKyK9zEQ9TatgTonVrYSy468GDvlbYFLGys3xZbX5MM0oxWLpqEMYGtegNIPLNQ==","shasum":"2713f9894e0ae8c1cbac91a3d201820c2cea8bd1","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1-alpha.2.tgz","fileCount":53,"unpackedSize":248781,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgjMfMCRA9TVsSAnZWagAAy+YQAKGS3dzB0mN62YMRA6Ek\nZSPsNwm9PFB//+0Uu7yt+dIM3DVcVuXhaIyeDUc9rK3Sv+AvVINryosnye16\nth6wXtCMF3IvMudZ2bZVCCyrTkibJ/mzLUipt+2Ym1/nxM+6Y7QlIJILXko5\nrPee+3tJZWP72kNShWMYWzw6UaSNOxBRIsGHZZ5Ob6N+kQa9Mj7Rdj6GKXxf\nbOd0h2vLWEuNBvZgYwz0JGeQUeZ0vghV8nsLI7uA73LXs3EpGtUehJ9SMP/3\ndILl8DQVqTDYT0brzDAwfvegOjys4J5qyXUhadGV25Rd7WSRmP1jgFwWv3jZ\n0wgi69CMiK748aN8p5nT1wlbMxV4SYA98H/cQqZ4bephVMGex7J8KikP3gGi\nD9/o+W9ZZ0Dv8cukvCIqYlbyhyp4Vqx3YRe9jd5T6v0qb7bz0mKGRbfJ+/rb\ns7kTt99MxCAt5ECNCSvNE7Xk0X6uDk9aha1Kq0NXGVzQc308E/fc02jfTm7b\nsmrLZSeOShj8jInJ84L7H6xaJmvzWdQIGVaBLrzIgfFNk0KgTeAMwNRDuz7y\ndAvjyNZ5ymLWznuQ6bLBjfyJhqGLqfNizOzRcyWk4n8m0O4Swyiec+4a+zcK\nSzYubMUoBuLfDwc650mFqGnMzJL9F0rgMHq67GbRtMz6qLkl13MkUU3wSI7W\nV7h7\r\n=Zsdr\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDILtFcMxM+ckMJO30Q8r660T1m/FtVDNQM3E6+BWkURAiAhb3dRPlcZ+FAMjBPUZp7JK+mUZxYGLe2aEjfVqVvBXA=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1-alpha.2_1619838924402_0.8633618101575926"},"_hasShrinkwrap":false},"2.3.1-alpha.3":{"version":"2.3.1-alpha.3","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"84d05edd5f93c55caa2af59307397a0d30daa012","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1-alpha.3","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-0RRy0dSNxD57LQdALw4UULI2auvh2fCJ4WxVr9MlArceqstOqBRtGzjoQt4kLXa6WQgC+NAukqW9EfvLAf3SgA==","shasum":"dce2c5945a777613e34ee193b3cdf0a4077d5296","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1-alpha.3.tgz","fileCount":53,"unpackedSize":251884,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgjY35CRA9TVsSAnZWagAAehAP/1H71h6TSZs55TR7RXuI\nJqcMOKlaiBEefgrZdnARALcq833+PcAyelgE4mi0wskU3ZE+EqDugLtAz8xS\n0LgIhVNI7m7icEFzzWXbdSClGdcBl/7O1DHYGMNawPQjwW/XNCZb/9vWUl+n\nqxd6LyoyRDyk9yWP5TLZ+63r1GbQrQd6B1M6E8EZZOJH05clTI9GWYpuv8CT\nYWj/aMMI0+sLVIqdwkO696t5wUQcxQNFU5fepWDogSrSWJ4/W+yJ1RMOK4ak\nAIrTI3kzibXclfimiIVIzSGv6Vl0OVP2INiU3JFm0kQREka+Aa/dMaRZGDsJ\nRDbmFLzCGYUbQJHsOluqI1j3fV+nVZO6GNnTCsm5treQ70HnXTxmitWdZPnM\neYxexoJkZQAGJMRZqII8z77jbA4jGT//Hx7muO6rLioWKJhEpaD8vOjlPnl+\ngEYw8iaaWMrFJeXKqP6zcaLRQtuqlxk5B3VotCG8w5gEZIs0LogXbLpvB6A4\n7qOvz9dMrBsXhD26Zx8c8xyBdZMgyE+mB+3L7W1mDYdYryyvHd0ewD+Sxo87\nA9BavAmLvCluT9Ym+iKZzKnx7Q1K7j1wHi9lCfiyw3xd+bwOAuZOKt4Pswkv\nWgcighwJ+20LUtwX2A0ySRczF6g4Swq5tpYFhwIh/UTpnDPQj+862iAY/NC2\nE5oX\r\n=x0P/\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC6nogXEt3TwCZ5kXcZD6GxXdwKoUbLVsaSJvHsI6kNmwIhAMW7xL/Mrb/DRB67VrB5m5Hhl2EH7TsMPveh+DdPFV5k"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1-alpha.3_1619889656777_0.9326319879312568"},"_hasShrinkwrap":false},"2.3.1-alpha.4":{"version":"2.3.1-alpha.4","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"12bc2ec8ea003ca1c9ef1eea35a9121457f1a0d6","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1-alpha.4","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-EqlwrWGZaoHYJYNJPFuPEsEbH4r+1SB0fOqJBY/5Sr+UHsGSr6rWTGYpHFzQesXczdzovDw8L66n6KRP4FmONg==","shasum":"78620bbd906308c3f4f9fd08ec3c062c536e2dac","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1-alpha.4.tgz","fileCount":53,"unpackedSize":254853,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgjbUOCRA9TVsSAnZWagAA/AIP/120VeYofqPlxObDljzJ\niT6kyi9aSxxgnYRlr13L+Wr9lyaGMr3sX5i/esgA6OmCus0wmZXodtQoTmT5\nlREDUtleR111oRecUv59NHanlbuoJE5Tw2NOXHJjdsoUhOMkrIcbkuxmpk9D\nnDBuXwoCuhVhhV+ssRWfsUKm2noBaiVWvh7n+u4whIQ3MhPl0ohtajtxPT5X\nh2hqxxptL2SHvE7stp95nBmH6EJWERCN89xiJP4gPVOjfDx0lzL+gjmGm0Iu\nGyJMTYUl3z0nx2T42YCOWKQVWRE4yI46NwRAnjxY1cbgcXu/3/UyMc0Ze+D5\n1omPLYeazbNC6IaxqqRHtPKaGr7JztO/iY/mOz2Y3KXkFlDPo3gr1IMr6dtA\nZtQKCcFA1fl/uiycVi0ZbUzUigl1z6ReRm3cpTg0n/oK2vpT9PMYijCRkyaY\nfid5y+OgbGaiLjplnn4UKvPFNIFtBxxA6QjBU+Hcuu+Cxq+44QEJuoUV3HEZ\nWJ5vwsfxxVqtkKm1WcjmsLF7QLxTm0Ro+s4Z2mZTYGfgKa50+45QY1RYjR0q\nXzVfH9wbw76Ca7UjCVyY6JOqz4vaTs3YT3TPS1DxGpAHwvkhJrQYkyHcg6k2\nMNdjkLld8TpKmBP1tNgelEIRo3eCHMrGT2dk1vg8loGzhNjiJnWS44YmpkLp\ndWXh\r\n=3PN7\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCICTSu3T1+d0pGP0JxbetPHLepekxldEIOUoDVjcoKIcDAiEAmMGRMK3MJEaLJdNZpzGfCqYC8vi+u/75oZ1lqlR0ElQ="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1-alpha.4_1619899661815_0.43861947812769264"},"_hasShrinkwrap":false},"2.3.1-alpha.5":{"version":"2.3.1-alpha.5","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"eaa2a5e4f353cfed8584faa7a04938b6e61e5e79","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1-alpha.5","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-vJYPdP5e17ugkRziVk+0Qq3eY/ZH1qtIpsMM8/SkjSpepWBqJmDDELRmgAAfPNRtjqDpr3DdODCAHbtlKCT0Rg==","shasum":"71dd1be51625df1fd3c378c2c47ab2cf26e93cf6","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1-alpha.5.tgz","fileCount":53,"unpackedSize":254853,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgjiwgCRA9TVsSAnZWagAAJ1EP/0IZViRR6t4INi15NDDf\nzyw+HfsbVlp4xquQWTyU7wFs6Ke75IrPLKqQX/zd1rYvtPvkI1HipX9dwW2E\nDJa4bbm4FlWL+wvvTygHTyqWmxcY2wC5Q3A+qlHhFzjs2YLtPVk/iCUZxMjS\nxoMkhK9bmmhG2d8+c8arHzeUf7uBm8Y6TXj4PB6MPLxp8I83MPSEzuvQr7yS\n+hln0MAoPBE6V6IpdrDwyYW9e2jAkaNlni49S0jF2u/vodmmYmwiDfLkRta+\nEqkECcPtWVVN5OFrbVDl/kZqKRU5BQjEp93xyQ+E+GymMU3V2f6rbHcOZd6x\nJz6oCRJ2vE2Kdqq62EAJWrdWraNQf+K4UG277gSVk0JDgydfgT+uHfnrrb1k\nSPbM3G1L59rf/AXdQ1pYE9BO+94wS/BXQYey8FNFsNd18eRFiAnj85WvQcLw\nUm52W4It8qnP/EDimIRarrZGRgE0e1t3X+U5f2hRhHQUgJZh8raPf5Axqto1\nKuIw1eWSgLPTeaqZA5AP2DXz922E9jFjHc8+mqYAks0FcuerYhuQ/IWd/h27\nj/Ptazr11nM3DfGn0XhriyZrR0BXe7XPnBnjisf+XcNFo9euEsyq33l6SjBi\nHeAOlGJSIjt8atRSmoRIZgfE0mUShg+pKm35HG8GLPoc/PryAVhlryORXfhs\njivq\r\n=SHys\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAdJNy7gWFo0/7islE3wbAyRPQWTGzSoNShfWxRdjFVAAiAraQV8ofGz44p3OgF1chVIBrRnkVff9huCeBIsmumgDg=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1-alpha.5_1619930143846_0.0014199314057614654"},"_hasShrinkwrap":false},"2.3.1-alpha.6":{"version":"2.3.1-alpha.6","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"2ca148baf77d910eabb30c45b6e795023aa5b6bc","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1-alpha.6","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-qvEdIyGHKvHwsE4LyBvNWCacT3nWyiKHkALUxhgNmhi6hkcqcywRZ21a2jVNiiYQE9exWMQwsgW707sMXV1HBg==","shasum":"e4f927eaa2034e7732571a64683f4b38b293afec","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1-alpha.6.tgz","fileCount":53,"unpackedSize":254832,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgjuuqCRA9TVsSAnZWagAA2p0QAIKo7XPiVBKE1lQOXXnD\njUG4E2O4vv0Bq0YNoVQai3D3IY0yRTLBBlBe9eEe7//qN8H5iuXCbEdLB3ta\nlMwFAX3O9mQSpAuHYorWMSvXlQqRTA2sOKagJA4JBI7hNsY6XDmTEf3gkM1q\ncOxIPIr6nMnh1aTZijpizbfx4NgGobnenoGqnfZwdsVJZfroIlIZwNw0lQuA\nqAyGGynCEVKrlVwYEudmYjtw4CDLLOL4OzTLymG1rorKP2FIThUsSYEj2R5+\nWsEuH3Gst1rM6xIg/7CWl1GiaFgD0L4lVMk5mveF3ApX1VMr1wnbIPyUzJrO\nxHIdijISeHqXUloRtEEn10X8I/UDBtzY5wqibbKjWckJRV6tOw7/NS+YMjNt\ngPBCMloiNGIu3zrBfODyYkhrwiwEommEy2KMHyW53M7IDj4AZguqnJpJVYLr\nx2ZCoGDqTqRvQJC230Y3kWYk9PEm0dmXDsdcyPnUJe8wXrmdZc3KVjbHCjMv\nfwjfrky4wvnbcA7JcFx6sQ4Pu/CvK+8zcu5bEmCX2L6UTf41zwhLzqoFLc4M\nI6tjJjGTtN7fC3lPEty2su/fdHteu/Xeu1BDQp2efUENlej7F4fAr1N5cKtU\nXXIhaAb7Ig6+Sdvu/QaKDlyZvWyYRDe11UE+dZbWSgxPLjhenBReFg3ubcTN\n3/0k\r\n=CKAY\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIGohDnORfs/gR9+eCbomSYQYBPm/jnIyH6p0IwaFJg1fAiEA3n9IZuH1PGI5tppjcSPdIiOzNkpte5O9XLd6ijDHO3o="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1-alpha.6_1619979177858_0.24622740312761682"},"_hasShrinkwrap":false},"2.3.1-alpha.7":{"version":"2.3.1-alpha.7","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"1f8483707685313b0502434c918634d049341f17","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1-alpha.7","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-HPbMc2AfgBzZ6vaEZLRYXh6kXqP3EyLYD5IxHV9b3FWoUxLD41SXytcKdO7M/PKfRITr3GzUBOM73wFwMwIabQ==","shasum":"2cd1ec8f9361da3e829e1c66f5ed61738235f5e1","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1-alpha.7.tgz","fileCount":53,"unpackedSize":254832,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgk1pqCRA9TVsSAnZWagAApS4P/2NK3Hd0ZF6b6g8cLLTp\n0ufCNWRQ9ZoN0xJc4v3O1/fA0tAE/PD5QNERWEoHVdHNTNYCGimwIe4iqzV8\n3reSQa3FxxWFHCkSow87EZG9etLl91bDIkiSDMWv6ardc88jBkwmV2/kXDuy\nBcvz+elS5vjFTiEIRNZDN+GSNMnLpm1zJ0mgl04LTd1AvvQqKOCtzp9hZn0T\nn9Fe5Uq1TFC2lIapUccKvGt6ZKDDE8NFEGQEmpJYPgIC6Pikfwh+ddp5YsBS\nzNGBj1ZT8pKoFwCFF/SDRMa5Bp2j4CoVcNXpxyzsSaAtmGlXjb46e2wMCeH4\n+ev50PPN5DpFRNAfbUVQDnH8wrTRsCka8gmVGqRfnsPXVTCnayQW+xEySqLT\ndDG6Ij9MkAKOFnrJJz3gZIR9hLZtJB+AniIUgIYMmMrcJtEzeqhhfI0RTTNy\nLrT543cEt0FigOHUsFq6r7eFXeCTqdVqUsO7vcvTE1INwsIYV7lUtbfYz+Fr\ns7i/HJxRlWmX3wdstKlZ4h5K/oBy6tlPk2Qpp0Wi2aZ2OPq1FN/iYzSFJRG/\nYDjpdFkDi5MrIco72npRGGucJbSaw+vjXQlbuvPZOFejTgGliRRMplwomeRw\nSWxmxXmnj7OZqUvD/HxZQG1/MYvCjnE3/4HZ26bgIWQ6t7n0h16gVlM03V08\n9Tbz\r\n=YrZV\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQD1XL7lLhApXZDfpeyfEPpHiJn7kOLr0CNfA5HDOZM8GgIhAPbfdftr+SkVij0l32S+K2uwjby+a/oTZ4cr/vwKCM86"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1-alpha.7_1620269673804_0.42151268344523074"},"_hasShrinkwrap":false},"2.3.1":{"version":"2.3.1","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"e5e3ec8f1a9d6b0145f73f364188f2f54c1e02de","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.1","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-kqXWF8h4MpvRXaDpp8bNaVSRw/LTFlq2+r3v4PoSVo85Yvh+6VqlXWqO9dvtYTniaWmN5qwFpZRDSm1MpVi6ow==","shasum":"6a652d2ea5ebc46e9c1304dd39756e5c8545524c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.1.tgz","fileCount":53,"unpackedSize":254824,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgoeWjCRA9TVsSAnZWagAAT9sP/2TlxbW+7A1vTwSNeFkp\nN9mXrvbdVEj0kV/f+g4OTxA9R4CQDHPTlrqprRp0xVl75s1Pc38qJKY8AbUN\nQ4TGKnx7HOjl0uS5+4VB4drMDRt/ekwdvVqOHntlN9QoBYrd8Ap8pO7Cu0YP\n7/SSlES37O1nFwTQqgKbX1b52UrLdLFQH8XnlABLpIVWah8J1Dprh73nDGMB\nYG8yDOyF6QfKRL535HP8mixobncJSG6E2x4PNLyNqCvbq05Fnsr86dmEzfzI\n27ddqEbrDaK/JbTFj2p7C8C/kV17zUNmIxYMdazGMRmB0QcZ91Ws4LkST0Gy\nZFtu3ou0/ka25VVFFE8UtstvGqQ6TUfLe5XPlP91qJQvJIEljVq7rJ9dlMIZ\nwBG4hRRCmaMPL1bXK8DVUuMvkHxFyJGd9Myww9un+DQ/zUiy2jWOhuhJ0lOb\nF72h6qtnRWu/Ougy5cqjVSOHO+0P4fBvI9GlJFGnhcsU8KyefFfDdwN6lHI9\nSXQhnDJ7C6BJd2Ou3KsDPOTmn67EmyHTcjf7eDfgihsAoTJ1f0uGDFYLb991\n3ErCmLi9clrIKB+nIR+6COIIUU1ZDEnGjXwUngfyre/IIblYmZjLNTZK7kNe\nll07FQGM4Pl8Eel0mpX27IJEKXUL4GNwNXQGCEb1inkgK5ieFg1A4Z4Pu5q4\niraq\r\n=/3n+\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDhZD9XaUnwCfgwA1Biqkp1ilOpUmKe4fYkjxvDdt/ZMQIgFEkGAV9PMzU+oEXicJCZF4oaF7a7gtr+VIo/M5qxl1E="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.1_1621222818588_0.7377174814881255"},"_hasShrinkwrap":false},"2.3.2":{"version":"2.3.2","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"23519fad1ed6b28ece1cc197024fb206954fecdb","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.3.2","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-hN38uvO8EAxQUXlA+oSKazJO7tunINLZdNqPjkHP28zSGyLDz+tlxYS6FWb2W8Pj8d4TJFK9KxiQBqSoPduoNQ==","shasum":"6790149f43912aa22907887323dd85d3d5a4e1fe","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.3.2.tgz","fileCount":53,"unpackedSize":256324,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgrcnGCRA9TVsSAnZWagAApK4QAJXz/mhWk2+Jv3BM+/Xg\nxQvsNA44PQIXTBt8ORLU9dSnNz7NXNtHHrr6huUfHre+wobMiVjo9aj0u5C1\napBkndU6fL9C76aA2HSS39rqgNTGvj1S44yaz3zIL0shS6ialOxGCKV4wHN7\nvO4gMwaxOTTD3gaJ/mcQ4ucKzew3k2nQwYdrfudW31UMVD42J6y7tBoZCY+3\nq/27KCgZj0P0cJnIPI9bx/S6J1sjPsheHKshU1y226PQ/DLsZMK6zCZQvi3c\nqWJyKGVnxdTmIvQFRDXZZG7eqlxiomAWxIW6+ws8PZ3XD/Cct1WH8GT3KLRk\nEua0EhB9Ya0VgyK+KEQsd5B6p59bLiuSLhUU4vtzO8OywVVeO2tOvzgSutcm\nxVGB08VhkrQ3mM1+kCDHvk6n9Zga035L5M3XGOlf/zAmRh2XwpWF6QSjq0XT\n2Yam4FDqo3rQYc67cAQwvn39SFTqynmXqWF6EfOj6coq4JqqStUppf7G3wYb\nB8tO0pB6jYohXuBoXyQqhMUYFVVv+4LdBPvOFWBwFW6J6cbq099b/fYr5jAP\nBcZ1i56JgtjqiDApXjrZa6xs9Vqb+kE3U1QVFuvWU6+STG/ZmxebP/IgrTsp\nMU8XyoYOA1fFiGK4wcurbddPggXdVnWiZ2gwqtdN7VATLlooSyDGT3sxqKQY\nfHrg\r\n=TUb3\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIFU6z31R+9/VF89L8ng5H/bN+mckgC/lYKkHy2uw2OHVAiEAwmFRgmOYu2oOpPBD01vqPxWKTuzAwZbo7NV7IELKcv8="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.3.2_1622002118145_0.032632395810958226"},"_hasShrinkwrap":false},"2.4.0":{"version":"2.4.0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"c0277ada4c1d84028e04efed1523a52475081b58","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.4.0","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-iaR4scQ290LLnS9UQpFNSn0TPNdpv8mJBCLro13LTqDqqexkS5ZevjW/O3zrtp40MqSLTmtgRSmGwpAMvdCxlA==","shasum":"20edcb7d27b9d1fbc88cdd118a780106ec7bfeaa","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.4.0.tgz","fileCount":53,"unpackedSize":256772,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgv8L2CRA9TVsSAnZWagAAc+MP/0l8tX+7nveGnH/YeQH7\n91/hu2TohimIwW9QgdmX9f3S5lc7f2T6vYk4y6wvMID2uUxsP9Up6EA6uA+g\nsCmwOQwpPra1lUklzhXsmCgFZfz1zTeRcXcCDBJaKWAIpRH1XG07wAd0wun0\nPjZOmFY2OFmuvi1cGwrSZffDLGAL9FUT5QUeQLAZ0/Yd+3PIw4zT0oVWDj0X\nd8Qy/+v1V+hY0OwdMIA/LDyjgFJLtCZTD7Pb0x0bUhQhj3g1JdpguBP4b+5D\n6QzubURfM2UTZoebJ5E7snpaAXZTt7xn8yRxEXh/A7auHYK26JHISvF3k4hF\n+6HRWIw86o0lnxwRSEgfmGkleB0DX0eMl+oxs+6A+gvZ6duK26R6E4nqrHsp\npbprajhyREx3Bgpt5InB/TNXZfyg8MMKuHr/iLpUboBnPukRY1n1353ecUkV\npDKdzO404wN79Ady7x1L45TIkAPyFSyYmJuT6oI6jHdUGD5KlICl3xEOZZGW\n+yPMvq/QZBct2Xrm26BDzc61I2D8n4Yfjfb9bdMjxLg387wIxI1sGGz+bsk9\npQhRpglNrg3wxDIaDk1onhfRjZ0tosdmWC7MXqpaByLhtGyZHrv9BI96hWRR\nXhZdXZEBhHogjUp5W3DnBpD1bYqEb5jiPCi4ZsLEPstcN6k9t93Dljds0I2V\no/Dr\r\n=Auxx\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDPZDNIYyeEJGbyLMvazYpVrByOzVwAs9GFFo0+I2nJGwIgbAeYjIz79fnt40ced03VXtBbXgr43FCrSyhgPzkmndQ="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.4.0_1623180021913_0.5615941759939123"},"_hasShrinkwrap":false},"2.5.0-alpha.0":{"version":"2.5.0-alpha.0","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"817d368ed937ea30832383e8c5e8424eb88ad6f5","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.0","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-xzrZlRVAxKOPrFJ7Uispu5tqinrZpHLqH7cG9Ex56u36sefaKL1Ruf53IgJ80c0dv9sdpccSxJ3fXL5hpMUNTw==","shasum":"71ff08e0c688b4d44f201a7d0da0b0302e67469d","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.0.tgz","fileCount":53,"unpackedSize":256780,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgx/lMCRA9TVsSAnZWagAAfFMQAKLE/1BJByawZLdWM7uA\nzWm1u4W+JG/iATEXomehq5CP0O3kke2JMg7oCR7tD6Mzo3oayJ6mTb7GVq73\nxCKEpc/+1iUp5tLpT1Ulkz9h0f/42XrpdYSTBczcy8WFuitgwcDNXobJv7D3\nJB2OATueWYkwapL6AwHdF6Shg3kq8rX9HIOyMjVrbLzPdm0jvwQyOSiSPhlG\nE7JZE7iHQg7UAd+Kr+/awNPnnhsqMfXqszu48+2ACuQ4Q7i8vm9v1rS2YxlW\njJrdb7kmQl709kVfT9z2JFp0Wv48Q7N3otDQk5lPC4mxGs5vb5meMUqUNlsq\n+bXad9xsxL/5OxoyiQCkNCilXK9IDv5v8OZmrcDiqpRRgHX3wFzXSRDVBfEf\n7YPenxIZKEFafeXI0NiFzTAvGSKHIXaD3bvWEgGlJVOrtyV9GKycthw+tJoH\nZUfaLGh8MsnevMGR7iC9rf4zE1T6mnpW6XqOfuBpMmiDlCak3HOKrenxkdAL\n0G0uKRAZuOg13D4ChW5sqsT41k/K2Wxv4YVkD8g8KdtZ5YMbDP/ia59vg7xk\niLctBe4Dz94JqIayYBP/aJRvGaOv1ts+z9qaF4SP2752lgB83V6tMzsX9A2Q\n+12AsmAhDZJ1B0G7zE0ixyausBnCZeGOrp1YS1FEpRA1plfOhbuEpQsZlSEy\nzuUN\r\n=rNDN\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDO0SBZ6Ui2aVFS/6qeJaauQosiTkGHkUuAi/6f1UbXtwIgC60vdwgzPjBuQGbH3BrNIYEIXdyQuzutzGWozk6FMak="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.0_1623718220194_0.9127896715323269"},"_hasShrinkwrap":false},"2.5.0-alpha.1":{"version":"2.5.0-alpha.1","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"4b52e7c90c69a9a5aa5c6bc56796a3941c001b9a","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.1","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-zDANyUYtSmIYI0ntfHpUDKycGYGuHnwHlUwb31UioyS30l4vjoyuj6PlaqrR7av73OItWShdetSqH/69pmmPwg==","shasum":"13334ca7209748641225f25288ccec714c907c06","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.1.tgz","fileCount":53,"unpackedSize":258326,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgySDmCRA9TVsSAnZWagAAQPgQAIG3wsd8k8+6Z/5hUdut\nbg/thkn09lhvtgrELmAN/XOzEzcsGfUFHrVq6rJQlm6LM2esl3nUfX4Wd4gn\nXmRy9/RfGeBfEZ8GazicEpCsEmU1h85cN8dTXmulWtM1O4D2FLsf792XRT7H\n32/ik1MyQnaatFZ2on5tJOj+6DKtYXx365RgBkNTKOj7LvT9R20ExN8zLgmJ\nDtUIYayglDpOmRkNbE7vKQNsLqVyxKwX9i3xhD5W/Vh4R00C+fO+BVeSpjb2\nsi+5b9AVP16qvPskTbED3tQLr0c67TKxYqetxsopO/3H3b8zXuQwzJr9+CWn\nJyuSROotf5V/7ixEDhpb1caa7XjK9jWe4U15fhLpYplH5Mm4h57QNPKxvLSL\nAmQggD0CqLAeDQQdFTfAC6GiGfRsKMMqq70swiYewQ4kpVFuemioKNemc0sn\nU1pXRHwUpk9uxoRfIOvVtU+irMgAlu6uuKo4Dz601vVZs8xLHY1W8jQe/CMK\nuwVBQjcukrvzMwaO0zxE0eg37B/SprmPABIvVEAj+bsTRYWHTWQaqsifmPiK\nx/S6y0wTBeWjOZjSlUnH9E/dkfWWj4gdl1D7g2iK9Kgx/+bY9aBJOVBZXnoe\nEfD+B4wcbojLnstR9WSUfZxZS3zp+Yrm0g4iC/3IB3v7jG1KJ8E4FqUHXaFb\njWaH\r\n=dG/v\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAjJdgN9SLDDuEKyb+BCgmwN4qUF3p1PS/RZQnXZNIUaAiEAmDZl1adiBmMCLNvlhxZQyYjqgQP6axtJT4v5osSM1vg="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.1_1623793894686_0.16288362313821603"},"_hasShrinkwrap":false},"2.5.0-alpha.2":{"version":"2.5.0-alpha.2","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"4cedf1b85d519d680e8bf2f5505b45a8115a10fe","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.2","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-JaTmxZ7JtoXigCvFxj9uAzT0jTI08DwScRqykMFsQIRX+wQedYOxG19JCCBbHfbgGIrlSEwaB2dEeoUY6ZeCNA==","shasum":"4f81ecb5a3135394b8c04dd2cb140b671ed57834","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.2.tgz","fileCount":53,"unpackedSize":258761,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgyWoCCRA9TVsSAnZWagAAAeUQAJuzVmmSvbG/bdn2cU/b\np+RSnI8P5lFTLP4OZz7BwOV+eld941KHV3hd6TE+wSnMsNnIC1414vXUKCWn\n659OUDtc/YyYEJJWE+H5Cy7Fl4A8mOrwAL1mbS+RDJ2ASRmnwwq8mZ98Yarw\nXy9el6Uhs1tknpe+QLxtSvC6hJOa1hIXqcRcxpx/gpl7BJnCO22QhGK+4UxS\nA19dPTgyajp7kn14XuYy5vXWNEqS5WDsQGC2LpYLcV/Fh6BkQnr8ERxVGI4h\ny5FDgN7VeiZeIKLXUnIg5jMairCf/MlyAmxNWI6QYXwpH6LjTI7gmUYVxQu2\ndqfOTTQlbHjw5mjnFUvqcfZZKyKOHdchKr62r8y/oeG9yzmdccYwgx87QWTw\nt8aeDdnECTHFvLOG7lqnZgEeOkFajoh05d8Ge1u7Smc3s8sDPS8uvR5ghA9T\nFB+DOSG8Gui7Z4DSXmqFLDw9DVttaPTi/W8MQXJ7ndFKAPABbwWZ5gv9kQ11\nxGrsVzsoke0yeYJVcpUp93baKlCdy4dmtYuVy7+J6yFc2KRbGl66Bz4lmgTp\nW+fdIDcZDQq8YVLo80vfHKk2XRSrYsYPepy5f95i9VV7cRHOA3+sFC6Lrx+d\n/uyjZXFiBY30RfDPtRbDiys7ipWJvTXeORpJ12TILeHYaeNUCs2LN9KvNo2Y\npVLN\r\n=tXgd\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCEB46IQfBMj2HPvJ20yCyFeMN2tj65ZpVgI9zXJMKbvAIgWhgQ7x3ce+7oFPIxHCNg+zehVOJ1euXhXw7zPD7+C14="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.2_1623812609831_0.4133288451397834"},"_hasShrinkwrap":false},"2.5.0-alpha.3":{"version":"2.5.0-alpha.3","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"7530c783d15c890d3167aed835b524a158402116","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.3","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-dNAiUEulWE++wJftRsG+IpK5a789x2V5Mz08Cmu5x8IO0A455n6p7vVeAaQZoW22sgYD9OclswPETFliP9vSGQ==","shasum":"72b51920aa78869717783b4c5aded04a9342956d","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.3.tgz","fileCount":53,"unpackedSize":258554,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgysj8CRA9TVsSAnZWagAAeEUP/iBu+WvdvPKF8ZyB9T/g\nKajJfkH9oNefg1SzSixDgD0l/bK463192EszF/csiW8WmgC82h5hz/+VFkHg\nefbZuq2gH4Y7V/PAEEoYMgDZE+kJ5Tc/FfgJR8SyDRK5QO+u7NDcDtBS/wzz\nRtrt/SkZuRn/+eOSN5IOLO67+tkkw67nYsAhVSeRRNbwgBYR0G47+vcYe644\n2mgQjlC/GfcmAFp3VvvvHU4o0a8hOZMS2bBCHokJtpo+SstHZl6mFp5PR6lv\nrFLUE/LCJd74lkQD3bOnEeYdaBDdvMWu1GlfZiRdyqY1HOLHPBpDuXIaJU9C\ngFLCuFH6JlSETEmQJrKE/ZHDRuXGYZUqvFyjUsK49dQKduPqwz3lYqI0ciDk\njcGKUslZ02qC9k1i0DGkNiY5fs7uVZHofC0ufN36qSqZNITD0hzNCXtG13a7\nJnTpz5GviYoLVrLZQRQvTodwIGcdAGGSvyJbfDxzccwtLu0QKCwgkd13EM+b\n4J0DuflrG5f2Nyp+GAr3C8D9pTOYoCxY2Q4haKIuWdQgKcxgy/S2aLHA6aU2\n3En3pjFp+RmuhwklcvwaTQGFWyzDUybzMOAMOIgObpM7tjDPP/vv4Gg1QR30\niZPLvK+uvQvzMLsX37d800gOMDeRlWk/kI1Apn/2ty5Z+KfddMFVINvXuiI9\n/6KO\r\n=DCs7\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIFuDaw//OpJHvDuGd+MoQPc7sbvw70D5lEp9/eZjrrkRAiEAsXx61e8rhuJX3Nb6FDQU2HcTcy7+zv2W6vlCxVtSQoY="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.3_1623902459970_0.3608423110328669"},"_hasShrinkwrap":false},"2.5.0-alpha.4":{"version":"2.5.0-alpha.4","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"85200d2c9dfaa7e4c718057838b5fba66b595ed9","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.4","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-PJfsPCha67mTTlUmOzjT6VCe8VhjNir7hJ3EruvzBHter1iwrmVU+gLyVcfXSP8d3uCTCLjWV0vDdmrUcJ3A4g==","shasum":"dbe3474b6b683219e426da1490dce41fca70109e","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.4.tgz","fileCount":53,"unpackedSize":259074,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgys7FCRA9TVsSAnZWagAAjpwP/3tdxnQI0lTVyOXiGxeD\nbCHZEysxBTum5JT4zAOwT9RYnLr6rv35wwfY5iShTFebWZRT45XEBsS0Noam\nncBuHhCJB0k3aa35z7f85z6M3gB55ksqygTkdWWN+PpvlZ0mHE9V1G+eT51a\n15A3BneS8AlPVZqERRcgd0DsAxD8W+N5ATON3kXzoHhbEI1Bh6jBuFJFvYF9\nJ3RxD0koIZr21bbmqYmm/6j2x4aNn/33XtKeG/0B+qh59d4abnk7ytl9I7cx\nQl2JOqBFGzYZjsGcsEaGTJnFjlRvo+UpU4JCKsoqvVjFjuuriQ6kRZjZGwUq\ntcJg6KEogiztIO+xxwlq1ovFZQW5neW5XdUuDDRDStkAAYCYMYnJ5U5pueYq\nS/OEqn0QMMpS2+qn8DjJ61Va/5hYzA1Dbo5g9hMIj/MyItq+R5uYdaIDI6zb\nMrqBg+le5r0ZBWQyyK3DCXnJAn6br7TS1NvbpmAVVDC0xEi61bEPlpwGXbQk\nHOjELiCBWJkh7BCRmax3UHXqQVTHSXDHv5FkW6SunHis4W77qhMWGhxNf2KU\n2swwLBBVDTa5y65/UST1hmKopGZ4VxhC/SaYfC5Wp7+qERSL5Aocanh0tvc1\nrYLHbTUY1a5GxFnVCcqtEQu8GwHD13yuBSipym2QWjvRKq4DSTS71ri8mmyk\nvtVe\r\n=jdpw\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIGeVj4pDrDXEqAsNKnlL7UNly3hw3KAFVflDLg2gop5QAiEA5uNb8HdAr/Z/oduNYSFn1ywzj61uUIwVE13ISBxcyEQ="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.4_1623903940979_0.7006946248054369"},"_hasShrinkwrap":false},"2.5.0-alpha.5":{"version":"2.5.0-alpha.5","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"3e01ac361e15e8e7e8f1ba31e4d73e16d2ded85e","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.5","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-9zPZbR3lsvE2A8XaAkHDKnneHlryUlDh9Np/15WG9FagwMW97tkNWB1S+NMOECjrqXKeptxr7iRI8qUjF+S0Mw==","shasum":"f9aaf3b39b7f515fb10c11a65f2995f7e5bc1de2","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.5.tgz","fileCount":53,"unpackedSize":258617,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgzq6cCRA9TVsSAnZWagAAjhsP/0N5S1tIR28PNhFcPQBL\nHwDW8ZN+wnluh7yYpzXOjg0DrPloE6DaoIdLPXmLKl5+a1Gt/e75wctQt2zi\nY5KHS95EUu05GPmbWoimlf4i7zMJ+bRNCJCDB2zgEv29LeqDjNrKvXpM6lNf\nVeCzIKk+UchDYNklQrSn5JbeQuTvXdHx84+UtA1KRj2asyyf5EihuUiytDH3\n7lftNbyNq4e3ZNhDWXLJklxHSav1hQwV/dwyRyBihrGFkQTSalPIj7dWkdMQ\nyNHzNgaOM+NzFtrWiNsAL2hBAsSPQ2k4xX7WhJRdQPBsH77832u9w/9gfdml\nxpdqgAOO5fuYBRwn9+bTJ85lpyso+OrKUbdB9NxpqLdz8CWgtbr9la0cA6/a\ns9s5vee/lp62iuR9JFp+YQWes/hi02NJgfl3OyXkNzI8LjVXfMiqvU82lNTh\nslcZR9w4+ht5XsdWyI3ZIjpiLcbgzpQUFlPdblywdLr+SY3QfJkqTFt0+qAD\nlsXFPb/MpByIvXtVzQrJq4rPZgv/tWq52GRouylkWR1WXPsn5qrbJNomz55x\nfW7/ojvAdoRDK5yM6RgGyiXu3SVVW5H/14/J3GDRKI7l8TRtFckIremqCb1d\nN6RI6OSZBGPW9UErCY9z86Li3n1wU0M1bqX36A8yvkMHuCI8gecL7o9pgONN\nwsF8\r\n=1ZjA\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIBv7T+1RHkRgDGf7ILhiYrewtxM6f+RG6qTWXDTB2c7qAiAFuXF31wz7KvYNiERjPcZgSdr0scg8nFsYVIQ5j+gChw=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.5_1624157851867_0.9042974165444593"},"_hasShrinkwrap":false},"2.5.0-alpha.7":{"version":"2.5.0-alpha.7","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"99005cf4bf19a4a0c509e5d62c379be804ee7bc4","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.7","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-aSvbURGbvQOChh8m6BJ21iX+tUtMzAW+8RXcCpSeY0+B92GjyrfUkwXzIPvCQuKREzonfd7cW+9iGuayZnsJPw==","shasum":"e2b980b1fa8c13462fe551890ed356abc90234ff","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.7.tgz","fileCount":53,"unpackedSize":259342,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgz+iHCRA9TVsSAnZWagAALZ0QAI1oxPxJGe358u7j2iF4\nhtDl8Vh1Yh+mlRPU6DX83tnYfVryP9mYkucOUHtDVW9S18UoL5Z+UzOzwqpI\nruxJcKAnaYEP9rUcAnahzV7BaBUfxGho3q1tNFRFLq5IOnVDHbPN+zG9g6VL\nyw4bCFqLhoZsr6tw68wl6XME/NZM2FxMMFqc4+t88XNF/R8KHp60/3b7vevA\nxPsvoSRisXsN1IQf7RHbYm7ktRvwNeXg0WLVrtF8W2jgGRhH99fo7EvmzCB8\nuwWDrCW2EuuV3xap5gSBKum4Tc6QjfZiQzyNvPiRdxhWYfjshCXUgO78feKf\nXhtkOEew7fB5Cv5rVaAaJnA0vw43GdnLBMvWETV4HUk8VLrwTkf0A+/CyHdO\n4gpQ0cwEdxfqFjqsUH/M0BwJezaddvfEsna/i4pABK7GlPs8tDA2h4VlQQoJ\nEux3CFUX6zLxa5FNuUJ5dHsImR5khhxP1uB4/1vw0YwPKkMgxZ4ReWOd5EmX\nVvUlrv3/Vd716fTKbWzEE4CMJDFknBD4JpN9cYxJQ0JoBsX5fUkrqeoHHugC\n+U8uc1WEcg5yoAuCi7Qq8IkCidU/i2H/7+Tg2O/9POBlhNf29Sc/Z8ERNdz3\nvPQKPMpdcVmte6UOcyA1DDjOa37Wbpiid3BD8lHSKqAP1TgXyDzl6CiGEKQR\nQIdN\r\n=iVO8\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIF2I5tikQqYkEbcproj3KU4DQSXylZFaKcYRgjiSr3oyAiEA7PqFdNtw2TFuft7U+uatO9MSbqGXkt2w/XWbyg2rZxw="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.7_1624238215252_0.7382453194480139"},"_hasShrinkwrap":false},"2.5.0-alpha.10":{"version":"2.5.0-alpha.10","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"2e3105c1b44ce9e50aec8191d7fc970a3924f49c","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.10","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-uLABu5tev4ZW33lD5T0+vDdctAAOhCvSV4+QAuneXSeCgwb5ccIAB7pLT5HNIkkfNUzOP35dOEtpexquIGO3FA==","shasum":"eea75aaeae6aec20d7530931ece7bfda30b4771d","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.10.tgz","fileCount":53,"unpackedSize":259404,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg24C/CRA9TVsSAnZWagAAwPoP/12XE77lKXzbd+oXPYpd\n1KjWdAdpb+0A1TlkCwZqfYjlhrUIo2M5waZ1v5XX/z3tgp/l67HjA845ieu1\nPQO/HYCiiErG9pVm4dE/c0OU/U35Csk8w0NVdP2csw6qA9ZlPR1zLk+nYhL+\nJTrb8E1lbe+GET5GSZ9/IshvS7Bb/+a3we4Q7NIbkKoC0A0X4V7qeE/VcQuc\nC1PvdFSY4irFWnXNIOHCnXZXPoM1WX8BhskCGT/hkchlyncKundnf7HhZTJZ\nQ5HMJvWtf15mWUAdm7KUWnpwZz/D8La8Au4GSTF3TvX1E/AQ7eNhQg2Y0NLH\nbePNqx+ujARXcLqrygp3fUr0gKE608Rbak5ckxm/P3kd7u98VVFKH97pLN6g\nAaQJSAiLgUztjiBecmllK5OWgcriwDxw5VV7LWRmahGil8QgLPI3SZ9V5kEA\no25bidY1yhHn4BcGqNRunRVfd1rSi1WQFUqjlOJ9JP7+ZA1tc6QhTrJs+1QT\nqNNPey5AcqT/sWNrcWP+JF4Wrrd6NR/Afc9mc89u1YGLrsYJ41HTF5z+KONj\nrKUrLzeHyKYZJqmMWX1/tPsfyo0NS2ItawNHsY43jbmYZDpW2Pa1OXwz2jcM\nXxyAuwO+xo7ABnPHFTiZVe3NU9xcVAa8FqDJrwojbIlpRyrGAclMovXajV61\nR4Vt\r\n=mWCh\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC4E1+Ou1A+/0Yg1aSryhk0TCeGE19Tvd3qJtVG9ObxKQIhAMqOFQwxA5q8i25250pXgK0XL3xzt+tdFka2WodZlTbt"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.10_1624998079399_0.6048870146729404"},"_hasShrinkwrap":false},"2.5.0-alpha.11":{"version":"2.5.0-alpha.11","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"5af6e1f88c89b43951324e8a5763e33c5bb8f610","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.11","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-uL7OsyzmflkTKCn3bd01hfUWcKTx6Ul+pEUWyHoL5JPLqhT7mHZE9wangzQcXvgp6foRUd9zyy8IOs+z91pI3w==","shasum":"f8cb4082144218db5820416bfdfb255d56ebb631","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.11.tgz","fileCount":53,"unpackedSize":259368,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg24/ACRA9TVsSAnZWagAAYjIQAIFvFFxHpq8bq8Qnrb65\nH1/+5sGh1bqMMTMgntBk4P4/2DIPID7O0dz29TzAwD7vK+OBU96QKDIfcZNR\ncu4MW1lPhaYstD0sU0p2kgyauJodVKp4frz1IjMSd5Gb4cvr6g6+xUlROfUD\nTXa4aB421iyUBjL7h+3dzXwRC2Y1PdYdIRX1UBT2mfw1ApolJENUsOernYMM\nNvUBYW6MSqlgXCJMFHDUDmNU6qy1WrHwOu80Gi+6jj/GOJx/R9y5QOOYbrb/\nNzLlBLWxVgEkvvH15OW3l0KUuYB6/nFhQFn0XmUIB6uy/rPVlF08Q8tMQcS1\nKBorm1dCNcduZrESPLpEbm1UCdYfAK8aF5bd3XrW5e6whAzzkcwd3zl7qi7f\nVJ2DvEdM4FlAEpeHA6hQh1Zl2bLvzL/ewMocemaYgTGbotp8RlAg2YN65xCG\n2Xpjq+A3hf38Pwei+g+S8RQl96tpFvngAahenSZAqWh+leKjm32Go6aZ0CuW\np0sjuuozZ/10lYPZ2gX0G6Uu0+cW+2qCi8iEZgRX/90lmi1LZv+gfjVwAr0+\n4PQflxfUgQL1/X6EmkTBSIxut5Iyi2Y6WGegh6y3N+jG1cvfxcGaJI6WsPXA\nSdJlEW8QY7trZT5V/IL0jzkxJ46AIgUVJjMtgEkJTnpDe2GGgy09kSlRVwl+\nPjlG\r\n=sQQR\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIEFsQ+xovSzK7snrWbduuXsc1mz1JamY8G7ta+qu195hAiEA47eSB2YPefZyAATVH0cOo5OKKCxmPzzRTGO1A+J6EpA="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.11_1625001920068_0.6966279460946616"},"_hasShrinkwrap":false},"2.5.0-alpha.12":{"version":"2.5.0-alpha.12","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"0575af3bef5cd1915a9b9bce1b848bca1caadae0","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.12","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-FBUTtM2oD0s74824780ng1Hm83ZQ26Cm2wC8NPP7PtykJzUfA3pDr+tvZgqadv9vOVeUdyKTQtg1Pw+uxvpeBQ==","shasum":"dc6ed9679683d1e1e298756d395a10a2bb10fc6a","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.12.tgz","fileCount":53,"unpackedSize":259220,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg7dqrCRA9TVsSAnZWagAAv9UQAI9nnFmVJ/yIW1GtWnjA\nYN3KxbeDvpK+kqjhT5WxVeaB5PNsxSp2p8fehB3uG4l3MHWWy1Kld0KqDvya\nbT/JoO+h2jIbJq9fxqUJt4GZHcDCRmyka9nLJki8jxonoC473thSEzuwDr1V\nrQvkPa376nuuzOhdB979/GvZjemE/zntf9qYPqBOHJrWSnxPCzrG/uKLKlxP\n/THLZHhWrUgVa1I4FxXYE9RZL+lpohfPCaGySDnefO1+OXuR1jcbzH4ft5WR\n55crn4/DUck7QyYzhSN3UzfFDZku43f6BUC+PuJt+LXB7oMM+T2Is8eXdAOQ\n8gKwdpBRMjEktLHX/N+ytjoL5K0FQ5vzuPLh+hE3MSkpY9OHZlIcEHVxx4fZ\nr6n8RsEQgj2XlbTCuexyrogM/gCLyxrHd6HgzYvhK+zP/YJIXW1jlntLJdZ2\nzkxyAH0vHFblZoYnXcQXJE8MjjNRWm3FZaTuBVQ3DI9oT2GWpxW2ODLE7iOn\nb/bxRKuUhwKISiOUCARt4Q8SRWqiiKFkKJwGpajsgtoxT3lvEAt9Eneyy+No\nSDIbVVkPC8GTzj0H47m1PXJEWRJwF1S3CBfVK1CZcIKh77eXNDiygiASBG8o\nTeciPsQxiiBEjPTsmdxb5mXjGsll2RjjttybuHnWU2aSCuPY4cOYpZZpiZ+d\n/0Kz\r\n=GySv\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCICmw8Fe+nNqiyBLr2KLhNKTF75CYp9UEcqFkNVLsFrPgAiBmCObbBSQmG36QmeivFMyqD7h0pL2LmeSE0tcKFvhM2A=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.12_1626200747229_0.7657836619790859"},"_hasShrinkwrap":false},"2.5.0-alpha.13":{"version":"2.5.0-alpha.13","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"66fee770807d0477824c5509682d5be1fa9f1c79","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.13","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-0F5jbP4+W25M67ERbbXjgR1zXPi7FZBXuJWWzccSPm6pQw7x2QnoAkbBkufNEpyMEr1NRMH5qw39XkQwKGn0mw==","shasum":"b1ad916c396cd9af42906eaefed01ebb991e628f","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.13.tgz","fileCount":53,"unpackedSize":259434,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg9f7LCRA9TVsSAnZWagAA8L8P/2RG+FMpClBip1CJLE29\nTZjxEVg51veQknuBQaKPtu3+9fBCVLfZXEa2UdaRYBTtvX3pi3SBLnpSm1x9\nv4qYuyaSAGmR+RbJt7zdcRvXeQBNscK1r3PeQa00FMKs/gF6I1cm3fS2yhf0\nateo8R9X8fCUsYXLYOMggxHUjIOKJT4bg0CHBrHsX7wyUwQ7iPn4OP2VOr4Q\n8mv9SJc19ieBrhfnO1egnS6igpvIXMcyh83kY0audv5wg5uRUV34lNMVFkro\nVJ28poZc+rF/bM0ZUay93hlAJ4U+9LA0H/ASLZhC/4YCu9paOMHaylOXQaIP\nqvE+5So5jbPGGFV8k9E8TzEVMqPyc+Yl9gMRqNhPQihIoCrfqXFecB6RRi9v\nWO6zocHQ7hw9kcFeYTMohaL5O6L3JR2R7UIpdMQWn8XL2XydA28pHDqO9Iph\nbXh8pgCNM5bBVkSRj/N1wY+7Aow4zLdLvcHcozQs/5KKc/vTb96hdJzbXI00\nk3s82eeH0LkqwqsJ9Z+yeVrdqxaq684OXuh7rkmfxIJAauBzEdIjwSjUOMZQ\nDTc5J0QQVhQvhU/TVmIYRly7hWQRWurGNxhnjM00cZgVjemge9a+7DPu5E4H\nIHY/ZUWVy+8BF1n91ZZWHkiNtyeXCvF+fivgl6/jSbv/dQb+QX0g9zGoqH4C\nCJR+\r\n=77js\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDbLy/Snt65bYIxUpIf/kMV2GPzm1bBAqJVni0pxnQL/AiAbQOCCciPlb9YYXdGPSiE7lYxaP0B1TOuYsa77fg3eYw=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.13_1626734283545_0.630448077407544"},"_hasShrinkwrap":false},"2.5.0-alpha.14":{"version":"2.5.0-alpha.14","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"a3cd4e4f244cb187dbf680e16ecbb1b80c8bc92c","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.14","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-1i6UnGzDk1KId7vC4Ok5WMIoaGL3D4keRfI01P2S5gz6I2O2v6VxfC+2/mfatwkWXWSyo1OsKNIPXYezvl2ktA==","shasum":"ecdcd26ee09647dd5a1ea2f96a2575cd07411a41","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.14.tgz","fileCount":53,"unpackedSize":264513,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg/ev5CRA9TVsSAnZWagAA3xYP/iRPp8deO+qZ/TlqKnKA\nAPCzY+kMxvh922uGLYMCz8H6BhChLCL8DWOo5SuKSwh6GU8uxqpTC/JxQPAD\nafufOanhzKgG4Jai+jij6so0ojM7A3RmwoLAzBoYoD2/R/A0DI+1YZ9bCLb0\nV2N+EpnznAu+f5nRfmO9C8Ijicom0tJyJGYP+R2r05YkGSlBAcTmKSGVKhl7\n12MYS8BWbFpnV07ck0PBvIJVjuTz4UdAvYmdxR0KKXLeV+XXsL/go+JdAP+U\nWnRcodBHXEjkZUUmKSHhoz+USKK+TDSGQaBf5EM1fbqO6MhNglFzERwgSD8m\nNKULJUEsq3F/80jr2Izrr8FosOq2GHjSMkn3E+Y4kIHxHWMmKQP5LsslA+r3\nyKhlPw+mKmzt/d08sJn0U/QAGffhvhYAL97tlypc9XCajVCZ4fTstasmzzJH\nBbvARMJWTiHSZ16TymyOvB60jIYk/DcZQrT9dr5fDGPR/1AUYrDqMIH+2t60\nxW4SyCM8qzB+HM1R94bFOgmFmgw1Q1MdvHjyVdaFLM12lANNHHpSI4C2nLZj\nziPgZFyDv8uGNdsJLe7i6rEqLJ6zibiriV8M776+GGP8u9blxUA1su7nJ+IV\nAfILp7I7cyQ+3AJT9Z4dm0oMkJm06uGxar3w4X2ouhgJ5yAeuDjDxGeb3rOI\nWkkV\r\n=cTH7\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCmrhD/6fr6WwNJZUzCjtZ4KkCh8aSvBpIPX/XYu86G4AIgfQ0BMGJosAweIMSpplOqWyrmObk6ueDQglnSoFbfN68="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.14_1627253753749_0.298557704168827"},"_hasShrinkwrap":false},"2.5.0-alpha.15":{"version":"2.5.0-alpha.15","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"af7ad31fedf18f07bbbd30b0f6cfa3c8c5b41db8","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.15","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-ne6laO14NtXr5QQFKWlDfyOb/uiTUBiIrrOIC3kW6qFit5BRfk5tExkJRE6ei77z11VMNkvF9qVhTmSBhNKAkA==","shasum":"e6f8284e3296eced8b770d300ab3a23b6f7cfd74","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.15.tgz","fileCount":53,"unpackedSize":264613,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg/fCUCRA9TVsSAnZWagAABaMQAJUODh6pY1jtQBwyDunw\n430Bd4GWALleACCQaBZcu6egajROluwVlgDFdFCzAu6N8yK+ZcCzB50uxAnN\nIBXfw+nV/FUSvTqzAx8zlTMJwO+etBmlcD9KuJ6rfb2doRb0SfOtiMc9EREm\nJ9bVmNOHijoBJJJHj0KFwixmePyeaR9W4on909hxmDf0H1sgYxxoD8c099N1\nB7oENq6cgruYU3iXjQXhwDOZIAZN5kXZK+cM30NrmvsGP2/h7Qgyg0jP5IlU\nimxdLtmvGNizrrw7mq+l6MKkJIMFkhVIsm1ebpdvZonOspGtvQB2nORReFm+\nhX/qC2oAn8e4NajTP2FokMl/MRqLpWfyQWl6DH56uidRlKovpM8nmkuaSMj6\nirPi6AP8G1sLCDnvasFHKIySaVE5elQp9LvUrLXS/lPBCkGiA/T5p/Wd3/N6\na2ThNU2ywyWOfYvh2CEfwp/ta/iIyyXOuj/LJYt49ClBGBnXQGBGHBgqF87e\nDVev2KgeS0pYG5xesoVxWKB0xitB/R3C2rqXOCXY//Elb565V2lb+vXU+jTZ\nUgEc4dAlEtPyepB2z72SYjxwhbFqfLEf/1oKYTCok2MG5lc1PQ5YUIHi8YrQ\nF/sMZw4axRP3NkRMJ5FlCvDFjEVgM10yI+mTOPrT95nyDwZqK3YuZ0XTigTO\n/PII\r\n=cMxr\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCWyB74YM/AUyyevi2ohISnhmFv5bPv6CIEjqUYVoF0MwIhAKrtHCSEDOIU5NwjSoWclYC5jcAXMxtBh/JYkGIGwWLM"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.15_1627254931866_0.12888532760255766"},"_hasShrinkwrap":false},"2.5.0-alpha.16":{"version":"2.5.0-alpha.16","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"2e36d51a6ffed126af8f87d777bf20514e81bf3f","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.16","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-DiRdmOo5Dd/VfHkIq63+QuVqTCSGBOH9q3TdGJdxEX5nIr01XXsJBP1FCCwhwQJ4GnUKwkqBgzYCe+Edfk1qQQ==","shasum":"d7cd84a5b1181a449a88941be802b8c96e15b565","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.16.tgz","fileCount":53,"unpackedSize":264623,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg/fLaCRA9TVsSAnZWagAAW24P/2njuBbBQFeedp2fsfVL\nu6oO+oIajTUvBV08bftqeyCgbk9/7z29ASfM60rxYseVsHLTyYa0jEI38RrL\nll3EhOXJ3+lw84F6S3+WUPqRedOJL8hpWfoBfW2T4MKiVvKUx7uMijP32AeT\nBG/2MSCUO5gBW4bvg8e0rCDEuDG/8j+xz1BLhJ+n6Q2uZoKPt3McA7TNW5Dj\nPW7kaRv5De3xraBp2trFZDj7HOQs+zCYrExDJcr5zjH2Ff4B2kBo1Ssw6k8j\nw0k4nY6wXXO3OeJqhUAhwkx3PF8YhL2TIY5tsJVSXNUQWtpjxNfOFcYq6mVW\nUQEC6RFS/ow04exy6/s7/M8atXJl19N1n12ESPcXXnRtSv82cbeq2M9kEmkK\nssXdDJ2t0jaDTjKoq6Af4JcjlIi9MjIJZrf5GGIc75uGIf0opSos0iEUPgRv\nuL/nf/hfKcb2vIcB26oFWa94dY1RCbiLRQpDyjFcl6Dae/HID6FSheNiXQXu\nIxzwvCQQlI/lqe4qnCG9p/aOTuVEK2Ecfj9+enxXG/6X5TQTZf8/U9Ii05m8\n9re5/JW4j5CrUwW9Xy2HptAElUIVpNE2HsXD34kg8U11xcl71YQwLgbj9Ms/\nvtNhfoWGLX2rmgMJdgCzkDXggyGmdBGAnZoXJ/YGrDKY0D8QYn+A5mrcIyXH\nJ+jN\r\n=I9o0\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIBLe5DK/Fj84cMcQiYxKBk//DWy/F+2K29Hs+VazqAgTAiA1i5x2B0Sq0j2+E3ysoZmwI7PkayupSe/mQzljAChPLg=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.16_1627255514625_0.4125879536132746"},"_hasShrinkwrap":false},"2.5.0-alpha.17":{"version":"2.5.0-alpha.17","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"f74ea01455bb274882e133860c42b5eede311d3b","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.17","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-duOD3rthYyQo7QCNxXNTDc6JA4InUyEAsyGmkeANSUsGeHV/GLSDT8Osk7fHxRJTb+6NpmmMK4RL1hpXH8nkIQ==","shasum":"c4b4a649e978811c0a7ae51cb72e546d673370ea","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.17.tgz","fileCount":53,"unpackedSize":266387,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg/f5ACRA9TVsSAnZWagAAQhcP/2IOt+B0mFyBSTDoN3MM\nsRJermf0xHYTwbWXvpoTnThjHmlWjRubFEtdzqtry9wgXpEyDnUNyk3h1Od5\nNxEQdr+rG82yANSxDXnKTprYw+vEvbdZ+p9rIIJoaf05y0UHGy2gVjv3ehIl\nwx6zM2opCV5L8Gfag5NEB48hqYGZl0Rq/Mnu+tQ6qgDTLPJ4h50kErBLinpB\n5FcNhecBc6Vhv5PrT1GlFmviLHRf8tbF8rsun26ExksNv9YthZEqlSSUU1VY\nvT1zT0p1bFQqAQJiayxZra8qnAa4svZrNDdjbE4gt+luKRxz720WCeFzdW5X\n56u/iTPT9QHMp9AkDoAhitK1w8q03dMD95ggGRG0fQmrcbrssqYs/qX9EhrT\nAOqHzHNawo4mVAcH2WscMvqBpxcqau74xBuFVwZoApW+DiReiFEXtdLOe/ak\nga2eM42sV28EuQxZPJYYGwdB1l1wBBtMMM0Tu3goq9kVG02H6ZSC63npcwB+\nXVHzgTJZERy4Ey+8CNW7iAjWPgD6rg+WlZ7c3UQMpPQoaJxcqdGyplZ8KtrL\ntXqw9SNLf10NCpp9XArUA9GbSg9NLD8XxLSNUbgzvxjAo7uxIlOBg87hfSUk\nxQoEAhwz6gJf/Xj1nnCFefby55gLwcCgSMY2DiB4oPdBCTXsxr6kpJXSEEtB\n4qgx\r\n=W3T3\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCjFunACSzmjXTPkd6gJfg4kr7Ebv/3wNDiIqW+MxXbsgIge8Zugj/Nv9GAiAnMz1WPNIlFP+FosHrfbxENI+iuKd0="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.17_1627258432435_0.9425844457703474"},"_hasShrinkwrap":false},"2.5.0-alpha.18":{"version":"2.5.0-alpha.18","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"2280cf4e428c578142fb587f3a43dd2e5cd25907","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.18","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-SiLLton+boMIQkbUiUPWEY7YcnPT+bt5f6oRFvfTwjHcRfyFpfigI9HLvxiVamvggnCwAKeHsnOrPGLpym9plA==","shasum":"ae41aaf761820da33ab996f16743aa7a377195c4","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.18.tgz","fileCount":53,"unpackedSize":266487,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg/jBUCRA9TVsSAnZWagAAOJ8QAIGQmWsBxU/tPAPhYUtU\nq62m0wpp0NnumJv/j43gB2EvpzISZkvd0Mst+osoxCM3DKHDdYQ9X09XFfbk\niwEcLgXB46jVfvNDg1C6ZoRDebImv7ZGDPhekhM/0flO0IxlLgKkOJ3QJ3nQ\nE0zVQnOwS7uY3ldBkBQmtfWe3inLw6ctZr6gcaTVMVeMBkBEzsd8m2ijLs/R\nkQgShKlmYaXecfXm53zPcIoNbDaIC/HdZYqfKIBAQzvj0p9jqi97/FTiqBmY\nImLuLSJbH1DR64uL7T6Av5CFi7YhRp53QjKRKMgtdY7U+3L0gYqp5zzhIHHY\n21gt5DffmOeb/xscteXdNVwmRob2HqsR3FkRXVrTc177LaAMUkbyMyVeLBsQ\nKNsVM2b44a4078Ns2wankJu4wsdKkfVswfWBlEYnyKlw5vwyaP2zrYyP3eyO\nU5u3XPww/HSgH12IzuDqrldGnsHTiN1PiFAIbO6lr9LCAkAtbsTh2U8BKdxZ\n0kGjsGXCSe/7Nw8yxrFs37BtVdg6+OXBd7sn1TVydp+5cAFyqeksg7yKsI/y\n/oFodBZvsvOR8LnWeTbXAZy5vYAetUfbdh1SGytZTegxDfc8WWgZw3FT6TlZ\nZdEEoSpAriSlj/LqQ1f0+7chQwCis60q11KvtxUmXw7yBwSC7VewafySiEdr\nTz+0\r\n=bD6D\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCICw77nGirPhcJlYKrXZLyJKBIbIQ2uGaxIBZqmcWmPfnAiEA8ZBYKqw50+u7kKTi+0MuAEmyFHtKXgVt3IhvqnSDli8="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.18_1627271252788_0.9103254928775386"},"_hasShrinkwrap":false},"2.5.0-alpha.19":{"version":"2.5.0-alpha.19","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"61621e0368718f15800edab00fcec6abb4a7b099","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.19","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-3MiEEev1dfVbkzo6h/0wT36kG1IvpFlDAvCBx1rjH1BnwwygJ3p6b+MsuKhI8LtTxZ+fH21RctC/erHU2ONmPA==","shasum":"ff3cf2df1235251ea9410dba292bea837d2a4898","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.19.tgz","fileCount":53,"unpackedSize":266649,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJg/jIMCRA9TVsSAnZWagAAZpIP/2OSu81LgoPCU+dv9y2z\nlezqKx3KH0QTzYzs2KmOWWGkjzX2L4L32dJ0C8UiVlPYlTLCs/x9b63/sKUG\n+ys65Qh0cRfSES7p0At3VOhU25t35kPFlexahPmI5x0RlS21IKC46vCh2kNU\n5ZGzqs0vtJqs2n227DigkNQnKrITQOfJCbQM3RPFT/vHzQA9g3b9t288qrzp\nfwb4sqWE6hKAb+kQz3oBR79uWnQ6LW6PL5yt6ooWeAvakwvVGWRyGIN+5Rb1\noEmLyMqX/d3POPH8bBCuPUEtjYKkslQaf2zI+rgz+seuUlXYYxS+Cwu7NxEm\nowyV9dYgZ612rLKH9z7Rom2VIdKNzhL7I/+ppvZvNvZDBYgKV1UyTqr22lKw\nxyEMGDaM+muJo3JO9J71Lk2eZnxXcvPnbEMQ5l2cmi23ZIHNOgj+5cVXJPJS\nPwsNWsGo9ooYdhFT4t3WUu8lYr7ex5Y1j13ZgAVnZ4EBEx/HclE9mQp9E+LH\nUIrrvWcwGyZ+1W7nlhIX9n0T7rHCg+Ux3CirG7pqzbWe/z6XDox+kJesgMBS\nzqykMlOBRYUPCrZ4kqDqg1bwqY7y9piQw/PFH77HuiJck3M0+GUQXeKHwHSt\n0JtYZWLWoXoMOvIrPw37hVldKB3SzZS4BC/PKGlT2wOd4WIgwZDbarkIEqqr\nNPxQ\r\n=CLxZ\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQD0FGOzUS2cIM4E1RLsVj5W+Vmn89RgqHLBjcq9niKZfwIhAJNFOSYt6RrdPhnqM8JnefedeMhXUcAxYgl5sKP/r9Jg"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.19_1627271692306_0.461649629637807"},"_hasShrinkwrap":false},"2.5.0-alpha.20":{"version":"2.5.0-alpha.20","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"20e2fe22745cf8004ada58cfaa728b011e472407","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.20","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-oUMBrZHGif3hoZmRF7SP3TkefdiozDWfftaLKSJmj3gd/xgDifuBbctaFsMmRDT1mT5FL/t2Q5ggSQQPOt2D0w==","shasum":"2537259915b0fb2e401566d93cde23da58e69e25","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.20.tgz","fileCount":53,"unpackedSize":268581,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhNu4+CRA9TVsSAnZWagAAcnIQAJBD02ztDPL/9U8fTLYs\neATtxs28QALYMCLCn2gtq9fcM2IRpZasDqnJTUlAHiCKv1oOzPwYaOQhIoKd\ndbx1oIhnhxym+Cd7pNjGtHlXpthVGkJGoyyrToqHMlmuvxNauMhV1rHx1ujY\n8G6vsfI67J7H7d1sx9p4SSy+ea7qIilWc7YJ3WARVU2zI31xe5S/SEx5SUpU\n0IBKpGdRLUFUNir0+/x/bvxpmOcKI8Vn4llpF+2OAiNQlIWUWVMyDqc8z2vt\n+znWQsJ6ZFs07RmbJG2KNkFCsnLlJnJFyYQyvOclTmLilMghMiHfeUci2zSX\n/WeC960vWvRIDDfAVdVN62i+vwvKS1Bgro6Kvq/qMnfmdXUeTI105PTSS+2u\ntMN4l2+ioE+aOiXF2H4CLMGdaEEIKED2ja6GkEJMk/JZgHHFmacwboeGlhBn\nx2UKKiaTRyT5AQaG+JD111Yq3Vtrb7KGV3wL7BAfofqGHLghLBxz3QBpEFLe\nQ9xHmK3FMmeWAQD5kUxnI5WfDRd2HdoTBMzg7bqbrhrPcnALVFoISN1U/MCK\nCKSwizKqR6CkQioZCykdCtythcVbc6tOeW/9vFsM5nY2mLwILG7Z3Lxny7/X\nvgSJ8+3mPGNEbss1i1myKA6neZRcpf3dSfDN7o1AeIK/KS6GFUL/re+WCmHp\n8as6\r\n=oBqu\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCgxZLV9jznxePIRAl051ULenORvxl6hTuIh3l/f+rlEAIgGwWab1VjJ0QaoL9D/I4p9vElTN+lnWIYci2tLXZiJJM="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.20_1630989886327_0.3986699101078295"},"_hasShrinkwrap":false},"2.5.0-alpha.21":{"version":"2.5.0-alpha.21","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"e8c2b8159457c19ea4e47518d02c6affe09acf09","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.21","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-XonFTCWKnhu7qwzp7ctwdPm0IAvBLIP5syCXINZTW5QmQQqPstKIDa8CHs/8kjHu745v+Av9TSB8sCo2MGObPw==","shasum":"51353a3e37d08edcf2a2e3ef0b4ae264e5f32aa1","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.21.tgz","fileCount":53,"unpackedSize":268808,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhN4PjCRA9TVsSAnZWagAAP0cP/05wNrFdMfgAPZkDxapT\nRME13Ux1QInsxcCR/T94BU66EgKiubkVgfetH2Tk28XHLbyUvUMUNaYsyKR+\nIjnRAnwZvlCB+HM4TX/PxfXaEc/Rb6/VHdX2PyGr6CDdL9K/soVW8wc7LCE6\nzZXLzPiyz1H4nV+zKBLlqchOz5ruj4S8NfVSS5c5rb6EwJ/m9YpXUP2jVbP6\nYukAfUUSoOrZjMjJ9Fd3FP6KSRaXZCuRaJMQQPPK/PLNlyQhzAeca07/FSI5\nPTmI/qrWwZeNHFascNHKT3xlCYXUN9ikApVZOOaHbKpFFZXM7xuXiyuxEAyo\nsbxx7mwhFRkAqkF4r8nCcwJyyearoQCvfSkbmj8K3smR/zeyuRtUINdj7s1x\nZuVQrDBtvndOEbgFcBsgvn2o/IscpkOffqholckZnuFQjbtIcdGACUe9tNiV\ngYz1zKqP3qqmGQg+UCr2dmcgDXZI+PNPDnpIPjGMAnjdoKWS44F1uEWPt+Pe\n7HbrgyMS1uYl0CAmC15eLodpPEGuU1Jo0ENeZCMpcWS76qTF7H48041aU9UE\nztWwSF6zchqAp+WjcQanxOSpN/CXAq98dZKmsTBgH1WuR4iXk4+6wXfj3h3E\nKukSOQHtxZcNLc6XX2rApgrGAXodEUoLvFXvDJmcUo4+TgbbQG2IiRkIMsOy\nmjDd\r\n=O/Rs\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIGlMt5ebMvJiZxfHHgPZ/MmD7HAZrw4lQqV0wlPFvdJmAiAQvxxPNDbB8T2tniyOrbLJusEIGatZVfBi5M5bgcQgZg=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.21_1631028195519_0.027003149426552797"},"_hasShrinkwrap":false},"2.5.0-alpha.22":{"version":"2.5.0-alpha.22","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"4053df61f4ab6f1495ef32959da21ab7cba4ee8d","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.22","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-ypqwqiwHw9FMYis7ZZfB56cwYNjojDllzWITNzbOLEH5L+ia5pvuuzSBDnLuoz/dkYOgFM2ctE4ZLpVlUSxKeQ==","shasum":"250a26d3e00f46fbff1a1e5e85cf3568025d7d21","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.22.tgz","fileCount":53,"unpackedSize":269061,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhN6DOCRA9TVsSAnZWagAAX28QAIh/xQw1qmpKqQVgifFy\nbaoamTq4ZfaXHtJODo1W/gR2O39Fh7l8YLTkGVlN8MvLubA0FW/LMW48xAyT\nakqebUQuoTruoGR2TQIQi2h35nlUdi/QYy/I2HIbFhQ6vAG9j3WP3oRuJ7Bj\noQ6pO+gIWAHV0YFSX2HAKWAQQmJvdGrlm87n6MNVMwvjuXgXUbrSPy/N96sM\nYf4Avx9xCfvjFuHcS6+meS0R+VZS5JUQKK+DonwGCskx8/vzw/uzUjbLNcBK\n0hCOkDlUJiiuByYZPvQn0cucduGozkDrFPDoLiwSwoas+hOQ7DMR44yvY1r1\nwI8e50RCFDPinYUJUJbQmc2rRv/ihRKkzHQ+C9V6PknnObNzAAHkaAk2PrEN\nqTumrICFSd/EUA4r1KyE3gZJ8qeeD3vG/m0hqgkia3ggrSSX+aI0I7LwfE4x\nPp+M0Z3pKRfiK0eUzYmqO+oyO3mueI+oheEZaAEivIZDGTUd3HCx/AUNzxhu\n/8oHkizIwQGZCvRbvS70tTrWKddpk6al9EYTPlFDq5R9jtQrgKfxxYnfs1aO\n2y6rRawtX/1LksZ29+EjTCULxis58j/+HaFflNMwyH+xsrI5qrrpInmNOqcn\nm2geypYr7GP4oDFrKJ26dI8EP9dCs2i6QN1YvQgWxa008PGjxt/zeAwhem12\nJsSQ\r\n=mu0a\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCg5YBl5D4StU6eF2id1GZaI+EgASlQYmFvTvR/w4zJSQIhAKajYqlPxo31wwYhE4bocgtKeEZeKYIlE+sK7XLiGElt"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.22_1631035598058_0.2641651182350955"},"_hasShrinkwrap":false},"2.5.0-alpha.23":{"version":"2.5.0-alpha.23","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"e699862ff43ac383b0509b80ad8262246e04454a","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.23","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-Pav3MK/aEsF0eyHBlrVmV0SBcgtEPIjy/YBIzKi2AEL2C5lShMUDT+/lRQfWKJPq4q4uKA9A8HfTnYpthCwABw==","shasum":"0f6d4435d06b4f449f37ff6ddc33747aed01c193","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.23.tgz","fileCount":53,"unpackedSize":269070,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhN6K9CRA9TVsSAnZWagAAw5kQAJE/zfqspRUjXrqSVy6O\nKR/YPF2vpynf/xzhzNYUTumNaBLOG9F2Y9rmYxF8BUfEPoz7khM1jix1gLmX\nSFJyPkG5Gdqu1/wTJcVL15sagkJ2JHViF5ix+OH1iPabiMWdae8nTeJ2UIVU\n+bGfCe4fFmKeNeSXOyTHhwDKBVbkayadVegfkQe2YYOIv+K1l6fGtRVjAavx\nym7G45GCw/1CD0UV7HdaGHz75SoZ2EPB8VddR5xi7RJ06mjyAw/F5NtyVmZ+\nEVYy6OXRskzL1We5kByXAVOV3ZdMHIvK1GXd3o9ddLMTriIRJOGAwQZIvESG\nY0yY+6NKiYuVgPgkz/hmD/5Pb6RSSFko2WERfsPUwunEJXyjfqahhCIFCFev\noHW4HGlpp4Td6fM+/6kEwDtzChiUk2vrV03CHD5H02OlS2gtv92xeiobbz7l\njgR21QeTRtentJL3T5EXgz+mW957gGcVhbHMX1YLthCDFlSLDFEZ5d6O5wrN\ncdUDqcnRYJ8ELzwvvGMXsioFm0p/te52tJPrTsGJfVChtrmzZb8g3vwuQUUx\n11DoLrlZ5pU4JRwUiVTuu3WbVGXsOFyhPFgTq3ZIY2ThxE1aANQKgBxw9zur\nMrQKUqltQG6JOLl0Wxm8PyyJ7Jo7GaXrx4hwNSycdliuC6dCZgZNGhG0lXZU\nP/kB\r\n=u44H\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIFkq4EVnnNEzsA1wW96+5D8qwn0zwnErBRA/LXNtKJUMAiAhnUacAZlpflAkRK9SNfoFtaeL/Wf5oYyq+1l/xFc2nw=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.23_1631036093408_0.5043973693184509"},"_hasShrinkwrap":false},"2.5.0-alpha.24":{"version":"2.5.0-alpha.24","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"87b272bae87f7a21fd52814dd4efbf7dec46ce8c","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.24","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-anIu4ijc2FiFktSruqFfE/qPY9uwsO/6PGZTKlqJCYNHT+AFEpEWVN+5A8OQtvtiTQvpqZW2ZRKvYODnrtZH2A==","shasum":"95ed9d246b1a3bb5f6eb46c2cafb4e05a1e37908","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.24.tgz","fileCount":53,"unpackedSize":269695,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhN6g4CRA9TVsSAnZWagAA2CoQAKT7z2y+E7xeBxqUzjgN\nzg8K8+ZXKivi5fQ8XqI+fNa1uBaEj8ubkEuM2jYXWe3JZMAG6x1KV7KpCzTO\nwUXQvvJHxveN4CspycRnw60YVkTtSCMPTBJeEks2uh/pZiUCTzgKFjfWSqfq\n1MtzabTyjEpGvFm00HGAmMX7CFC1ZXx5NTZ/gJPk39jJgwVXZj5kvdngsT2D\nMY/eFshIaaLGQfyIi/k2qB3mj4xEdr7HZ/FgJDPtEkqZOXiOcQdhrVdkjhcY\nnlnQK/YnDxxWeZxQ237rq/gYZrhD+DepRodvdVLPUrFa9c4iptW27Jv6NATg\nT548Q4ValLOPFxC62FVA3fOECia9dFoLoauYn+zGHnuYVbJcWbaQJKDLdTG0\nricQyUhfVU8xGXKVJPEW3/S73hkWVTe2MOrkyyGucOt7aQiv5fu3ZkmNGqYf\ncbdwVmlL1ZAcfftPjyULbzyZE8RdI19KwYop8jR78d3Fu6qYc2yLMCgAVEgB\nZT5cxefCTdYcUjQW3bfN96WwlKFW0rfAEyDgeTwKM61NbJd9CHZyfxPiNMbv\nXU/lCPwWVvBSk6FiU+NDmkwNpxgnmm3pZmYYLX9dZLfGaSipeF/9F7TdEgvn\ncwRZ7d+wZ96mwKAv6UXdEs7sfaepx75AoFEEqzud295zXAoUJgsfb+NP9I/p\n1DnG\r\n=6M6n\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCblhBdmIqZdi220t+wKDO0eU25VATlq4z9RK8ntPVFLwIgL9cPRhSdlTsvg2B87LT7DLQWW4aWNlJ1c6ugrTmYcO8="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.24_1631037496407_0.12880097878426744"},"_hasShrinkwrap":false},"2.5.0-alpha.55":{"version":"2.5.0-alpha.55","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"28288629c86df0b6ee7234fa3dac2d6165df7343","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.55","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-kDvTokpnDJR11Uao/VO+sbkBN+10i0AEa2F3jMaoCWlO91XM0tEQL7ae5hEw5Kwq+2CCQjcLn8iAiqEnhMCxqw==","shasum":"4048b46e4de11b0966ec8b94b50caceb773f8c1f","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.55.tgz","fileCount":53,"unpackedSize":270464,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhkqRLCRA9TVsSAnZWagAAo2gP/Aghj2SJ8ACpEVh6BOpJ\nxuXSpgEYyHQstWnAS2SyJgv2LiClkCzXnXAkKs80VCJui0RdzcIszkMTI7Se\nkJs+TAjBTXfjLW1EmUD0UgotclveaoyQ17rl+h6ZSMNT5GNu6rkk5ZEoVtZM\njiPqeKfjA9E8rF0J8n9fC6Cl/Q98hlqzQOTFhLHNkFosKi8hpi5++maY8fWM\nUyHLJRB8C2MOb0HtnipGRv3HbcjP+fNJpZF9wf4IUXLvjhscEZ38c9czkuO/\nIzVl2mrx9zoOR47m3dVq0lddll2vLnaSUNypaqqp9e6uP9pRtHgiFoxv/HOP\n7VEkx5lVjYXSDIs7DigVXF5qoe8qKO7C58d9j0KhFXwuZVCOepjcW6imaVnK\nQySuWo6MF7adIGsPAVxRdamMX76mZsT9dXH4B8LbbyVsP2njXet9GpqfsXUS\nSxquJz5BT9WEYwtodQRnZj+SlQGe/7tdIHXErjnLQVtBiKP8ZRoVeqoPIYRc\nWBv0xDVZbR2+quAYcCkpwuI+kq3Q7GHUuBFctC8ktkeIPQ1J3QqAkBeG3mdg\nI73ruU8Qe9pSqeDN886zs0Yda+ob8Kd/2vtE0gRBmNDoFgcDG1LV6D4EFb0x\n9tUhwzerS2wyQXHTKMggXRrAKtSbOD9ZyFsWTElUWEk9UQNmlSD/JKfMO8e1\nETtv\r\n=5+Xe\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIDuQb4mPpalutqhiDWio+nqta3jgZubbjM8x5KITtDPTAiEAy14JtiP/0WIVKJ+QQub3q+ZU55zkm9bo2fRDZ+wf6fg="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.55_1637000266905_0.7982924077101823"},"_hasShrinkwrap":false},"2.5.0-alpha.56":{"version":"2.5.0-alpha.56","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"d2c8dcbe4f75cc012413076a2317483ca88568bb","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.56","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-ZNsd0ZaUWG0upnz432w7eFBR7nQy3Z5zc6J3k6yzbTgkSLty+6EVCAHQ2V/7u4jwBOa+qfHCEBkMTNE/Jwsq/w==","shasum":"bfa44b5ac9fd3470c41cd767928277730836e1c7","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.56.tgz","fileCount":56,"unpackedSize":273632,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhlTsRCRA9TVsSAnZWagAAR3UP/j2811C5R7MYAC0QZVzD\nilIeyzhd1TkA4c6CndI1tJg2HFHwKrDsXCtoSaJvOTfwo9a3P4MBvMYJvWpT\nsddzMQya4H39YegFzMdJlaN8g6iN/WZfvrYwhYpLktLyASYrs6nbrSrzKwmH\noeDbUmZ3Jg9bcYmr9FC3+4v+EHfqUaCQxoatH8M70j6tOZaW2lymE59G+oUu\nP14LZnUbCDjSRdmDfU6Wn4PYZbfwW7YIpy5IC8a67iErUHqs117eXcZZ6+Wb\nuDF6hPt7xNNwE8NXk20tCofBbyyXGS3z/sipP1uu6+JJzESY3F8abSahSbzo\np6DV7TncxmV5Fr0hTCB8YWd06vuiZQtUf5bv8Z5uIx8Iu1mbb8ep64rbzFo+\nYpCVWPbWcCqEcn13436yYfHD8T+pZ6goDcZzvRMG2mdy9CINiBPBQEHpJfYH\n+GjXbQEKD8nlCurZtifAuK50Qg4VhWobJmB+p3w8KPyW9XT9w3jD+oh3FJ9o\nXXr8UX9pTItec/O1wEl2yZ5wTVVRYXhIvNZQ+kEpixTgv/Pn+rzVx8pFhi98\nxHIba4qSWRdBbv0vvYPAV3UcRyoTjTSGcB6ogCGyShPoZI4ZIAdu2Ycr/9f8\nWOGJCjjhdz1ZfacioFM5mpaYTIC6wtax/ZyqtCJ2MNF70/MEhZhkCU2eztHy\nQSZl\r\n=PpbE\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIHXjFL3IUy5ESGbVE+i8HgrKN4LrvqUMEUKuf0Vz6PYgAiEAmrl1f6GiAIyvxnwCy1olR1poSPGznVw9AImsFm/zZyc="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.56_1637169937514_0.09289438177944986"},"_hasShrinkwrap":false},"2.5.0-alpha.57":{"version":"2.5.0-alpha.57","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"66dab25ca3464e6d62c9ba5c02bd1365fb02e3ba","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.57","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-fhMUrbkxEsc1uDv7mJMNEYsGu418rSVuqG2Xe+aay3YsGQBAWKkT9xGMpqhBquupAtlPCWs4p0PmFqSaJuYfOQ==","shasum":"c481363f042d2ae95ac3927a52fcd60394f7bab3","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.57.tgz","fileCount":56,"unpackedSize":273632,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhlTzJCRA9TVsSAnZWagAAiLoQAJCP3yqO8o4v9iKyMksz\nqW5FYVvGMcIEXjVq4m1vSIc8TxJp9FPXVGwbE1R2KKTzJ0P0CKrOoL3cxO4L\nwISdgCnF0ADBMZ9bccDeW93zMJ4iJiHL0arLTMqsTdUABNBTJTNsvIbLTWTQ\n+xDOJVRXzIw+F1/EsF2B1PbX5xA3PbuQ3VmbZ1JrVRbPSZyWK7TPyYUTHSUD\nc2H1li4Emgx9a9QdeGLb1cg8wmbUmxuAbMH62zaBOJliEp4wUSBrmStdL/jL\nopjNQvUy1EPRBJpmEsPFF3kwBWMmjQzhYGyFsLMNTwXXyx5SJ8rZ7eyT9Jev\nuRiyXvJaYx3h66ARqamevKxw1pbvmRBwls6o/b64L7Vmx/tVbkHldIVL9YEH\n1WRRSHDPligNizCp3o+5eybRRFKu6xQvhnJziuQWSQQ5rzHeXdE+T2BgyLt3\nLWA4mXxADLVVCcsviR9iMjGVfvsfgv8ISu1LmEdXFkC3W+FO4CrKiMgTr7ip\nyU615m58g2k0X9NzRkwpSD1wv6+GiMoTGOH0cVCaUvSXBk/xTnSE3WxZnq6b\nL5xBW3XoaXJRQKAoYbN4L+/qvYB0DIrZb9Eh6vIK0MH+xUPTLKqtjy1knidK\nfRqzsRB41T2V+TRnl2tovE5TV+kQbxReKLlYrT57z3SjkAKrcHQOEEkAW8qQ\n4CWi\r\n=cPJU\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDrB9ZZ0j3UmwymRGb5A0o40asY+gopDDHdmC9vgYsAsAiBc+vVr1OhudP+5M0hUaSrzmvQq61brsZUSRBai42iszw=="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.57_1637170377370_0.9905234088557098"},"_hasShrinkwrap":false},"2.5.0-alpha.58":{"version":"2.5.0-alpha.58","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"e7141de5af3b769c82004dad700c6e9d4b86216f","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.58","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-fIIe4cL58FFD60T8jJtBHtTiBHRXQyRFB4sH5FfAKj2GbjuNMAwhYA7/S8otF57eBhl6SwH5pd+mqTiSeZZlAw==","shasum":"f3d5928b53294499d88a221b9ca47804925fa396","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.58.tgz","fileCount":56,"unpackedSize":273752,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhlT5+CRA9TVsSAnZWagAAEAAP/iCPOhlm72JfQGaYuT4P\nC+bRP/0QX/kxj6L2pA/cn66oXWisnV4Qq+bo4dfm3C3rJlRmV6Z+CONM9VOU\n6/8Nc403i+Q9yszJeSPxL8FqTnTgKbVBaE0mPKBEAedWDhLbyrLFhEvZ70KH\ndpd72RFUROyM3guuqKlKXP3/2EJFH3bhW9JSWc1Ljj8JwqHzOe6Y/xf0oQ53\nTTMct1P4Nxwey9vksgmHdTtFNjWTS7P+mumpAqOuJ2v/TiJMWues8TLEAFQj\nOWMO4EIQFfLyIuFXbgo/jNWSK0oASz0UTA1r60a0Z2wQmPybPHYLDgjiG3Dx\nrHtb5jdyGCyd7mtDAQw7Png3SkW3BMQsXrpeSGqfh9jPW/Ppi6kO37Ggxlat\nCJ0PZJTDQPZtfWJomJ7OCDls0B+DK/fdcYzcnRH7KRf54ILt0fPNNeY3rf/S\nkdDart+TV2/zsARK7r/NBUYlpgFSP/RAcN0rPxqlWK821ASC+kIn+0QmpT+6\nLtII5x9B+n1xPcqJotXVY7iZr5IcL4LWVo2GOroZ6/yJJ+V9a1qdwR/yKl7C\n8ljZzfo3Azl3wwrnOiTaC6efPh0gQpgvH4dDzXQ4+ArUy77smxp3Ixa5PXZc\n+U/bEds0zI8t5zZlBEr++fxlDzo05W1EcQIHn4HXO/B6eMQ0h/HBPXLnAcOb\nIRFi\r\n=FCRf\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDv59pw/5xIykH0mxDmn5lM+irQhLs7dpDszfUIkzeZcwIhAI1EDLyIq7WflEvZRY+iXnsSba8G6uQ7My0g3J9JJ+2D"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.58_1637170814484_0.4439172358532306"},"_hasShrinkwrap":false},"2.5.0-alpha.59":{"version":"2.5.0-alpha.59","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"e08bb74b3628bbe18d7bdacd9dbde20cc8453622","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.59","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-hUE4zilmB8LKHrJa6XzIPgm4k1MsODuDwom0D0vgb9Heb+Q8xHJbQbKgjrXbrdp5oFPZ/LXviMNP1O3g1+3M2g==","shasum":"5c0251d07bc0508d8ea932eafc9664fcecb77d10","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.59.tgz","fileCount":56,"unpackedSize":274686,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhqO+rCRA9TVsSAnZWagAAnPYP/RDWFjMNbpxCI3b8gayv\nN1UK/yUeXoy7DSwHs1w3dDgmzi6rIaVDyHP7o+7Aswj9qgzs8Z1x8+2EdfP2\n5JPZVbUwugfPVMLflMcfTdG/BsqehS7gLuN9bvV+vA82fVlUKrisUk23NjgF\neuQ4Pad0+PbtZRzildTMzRnRa801R7+9XOdnN1V0N08zYfh9nd9CqddKQakp\ncHS3jSzJE7IHaO85Ah7Dv2g5n1QUU8nJihbHz4jiwA7tacFgfRpajKY/T/2j\nbBcbIWP8+sFjG/PcuuLzqGxuuE9CwKa3vBNrhIku7pI3DycAjoiGfFTuxvVb\nNtN3clDNETZ12C3sPxt4kW6cx1z6orLvrAWNZZsX8QHgy3XH+N2pnGA0PkSJ\nceYveFywlh/HwZB2nl3lmRPtdvQdKCujkBOnx87VXQS5L+4yd77VZC8EghCp\nn0j5TmC1R33VLVsw2F3bYLgVhyQUdHcpV1oBzRLcQ43oZDW8oJVd4F9YvH5X\nzjyBrhSrJEKUlKEgoxZn/q0NAGcFJpIzGeb7KhuodbpnT/k6oj20xF+ZibfA\nLt33DtytdMIG6N/8OSJiLAKa4gBwtCUc/fYQDth80fnCTXTHvmPH7HPRl9Fl\nQXsQgLlw9abAv1wSA9LImLj9e2JXps5GMBk2rkSslZHS2AbBwyY/z0m+64LV\nwunq\r\n=l8z0\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCbi7Tplz9Ao48Si+KwYfuqtac1HO5LPChhRHOwJt4POgIhALBLX3Gv1LV9+fFIOSsy5IdMuOchEvHEKyp9sXK9lf50"}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.59_1638461355161_0.08214810317114574"},"_hasShrinkwrap":false},"2.5.0-alpha.60":{"version":"2.5.0-alpha.60","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"7e64b32fcbe0820bf7d7cd094c40e334c447e09a","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.60","_nodeVersion":"15.13.0","_npmVersion":"lerna/4.0.0/node@v15.13.0+arm64 (darwin)","dist":{"integrity":"sha512-jFL7sKu/aFFOMNt3WrjCf9FtcD9ZUd+4Ii2f+RSxqUAQ6V5Kp6/Wr1vxqouySBoX4lgUIeBoRYfukL84tWUGVw==","shasum":"818f50d5ec4c2d6664bdd78e8977fecc6e48d2e1","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.60.tgz","fileCount":56,"unpackedSize":274754,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhqPRWCRA9TVsSAnZWagAAcJsP/jL7nBad5HY6tQYLO2SA\n1p3nikZ3UvQWlrQOl65ChDZT+3q3ewMk2gqm7y4v5NY2Mb7NMX0Jl8E8UQ0z\nJbsNULncS+SQAtlRgBLcSOrAecLTBDO4gxnYEga5UeNdVm8MbzK1UfTtAMja\n4ZWndLjwK89nP935OXroMEb+PRfGMezmpjphTJj7hhHjyyjZO3jZuNNrl4hU\n/+ANBzOmlws7wDlBvQtl66I0tg9AfPJVJHHKVJ/mPPRG0k2bQlNHMK9HNrR0\n0nryag0uG1aie2wDqkomaQfBjo6Mqa/WMrqnMcL3INtP2Th58wnahWVpBy4V\nsrYwaJstEC3LmkIVCVhvnqPI16OYfn6mxeJ8hvJR8jJi0dkDzeJrbErAqZsF\nucWr00ec/JZA2v1ybKmfIl7RTnr6+5jtMsjVUxuAvJeINdzVXU2RocAv6PBN\nKzPyIhij07bZ0aROXNYn9eVDMSjrbrqFJmMUq4Nae1Yz32G4oOH8kT/yM/tn\n0VwbNGhQuiYu5MPZnPu7hHRRVUXFTAMA+eMhEFCywGc1jk7RQKRo5BYTdPjI\nFwj+7THAYgDg1tr3iCUC/coHdP2tlCHBPPpa/eyfBJHqOb5a15lN6tR+TnYG\nGm8ObAZjIuY973RgFN5FOYRiOZ7BBDF5zF/eIHWPDBJfNb6hfXeVGNPlcP8f\nJ2AD\r\n=oC4C\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIErUu3Zv2JzEqeumtwq0FNgzvoca3N1S99X3DnfpZtpHAiEAh3wiM2jDMGMhopX1NRP8vd5WbnRrYyI7+c/5yv2rIcU="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.60_1638462550229_0.8246830874721991"},"_hasShrinkwrap":false},"2.5.0-alpha.63":{"version":"2.5.0-alpha.63","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"~0.95.2","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"bbfd6be63986d723431d1c4716c129bfe0755f1c","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.63","_nodeVersion":"17.3.0","_npmVersion":"lerna/4.0.0/node@v17.3.0+arm64 (darwin)","dist":{"integrity":"sha512-gbdCWpGeTvyG/p2SmCnUXkXlW2K3ydQmR9wvHz53hpBmAiGe0QOtqOgQlku8xdu7NHLGjqc4dIxtwgFDpu/pGw==","shasum":"dd88424a93750e1c62a3d65c7743aa94e904da7c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.63.tgz","fileCount":56,"unpackedSize":274423,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJiAZBOCRA9TVsSAnZWagAAmRUP/16i8xItwUDAo4K0weYt\nNj5SLI3lMisZSk3VBR1aSSRPoywxtUc+m3M60p/ACnsIV3lawBHGPFJXGoes\n/z6woWiN6MibHtrRQbrC8T1iZdMObnJtc762vYrdUk9xXEQyDmt01lFkkrCi\n4noDnvoQ6uWyT+kMZyfSvyQaigejBPI8uAJ+DCRgKTGwUbzbLBoVphHMiRmz\ncrTPT39FXuCKGRPUv5lhfcEKgov42wu9ISoHt3GKiybHylu+xhsB1cJi/sVa\nIYRavh/Wp+hT2xrZDQKTkL4Jd4wBJpi0SXNYsS/dPXehAMubVfqFcKOnIhSV\npI/XfWa40nvfC2zJo4stgb2F8K9hTBHRktIXwR2boPqUZqvQn0eVb0druH8A\nf1SUi7Y3YBhsPrhAVH3aq/lR87+pQxNC44lKZP7KxCn8PcK1iKs6MOM5K4pZ\nLuAQgOgC8WLkLulJhkzBm6+kqwii0eQJ4pXkgQUOM0dEZpbLAkbs+6fMPszj\nyLMfvi2/DAYlS8/6Dlgr3lkeqV5dGspKrYQAnpiMUaBsF3N0F3GtHckxAZov\nJh8jYMeWPRYuGIXWJQ25PcNTpS74G/j6bNZqpgsZFRbSMVC33+408vWtBZjh\n6dM4R+VVoOlBPpcJVVRznwC/CHOE+hwVuRn6RhE814vE2PUuTIy2+IC2D08i\nk6Yx\r\n=e8gw\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDJCjAAey8NZZDhWWwdNfnNOi9pjUzv3RzR56AgLm9uUwIgRCm0L4GtVgQwPwbEYLoMhVZQ+bU/1zV7NoQd+rbayNE="}]},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.63_1644269646534_0.029000459270173362"},"_hasShrinkwrap":false},"2.5.0-alpha.65":{"version":"2.5.0-alpha.65","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"dd3ed5394833e030f34049a91206b378f4ebbd46","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.65","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-YsCKQtGzln6ATgDEq0AoBYWc5MlAHCyEYPtwbGHNF6AHyhLevmg8jkxWwICEo+MteFCMg7HncdX55CUg5evzMA==","shasum":"96d16578bc09348802811086fa5c483afe1f738c","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.65.tgz","fileCount":56,"unpackedSize":257115,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIC2OglKjsC2vHJ8h/0rCnAf1maMXFWaDS1HaaK194RllAiAak7CIzlvJrl6/ZP8cdyljjc8JosUgvftU72qJiN31sg=="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJiTSBOACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmoCXg//TC32NiKDzcnR1MMzDYKNYMel+OQvgbqyq/wCvz6lsl85M7Fx\r\nUmE8nJnFxLKiyZFV0arlkJquJSBADiDyXKHZnWRZ8v5ABA9Cfd2IvwJSqmQq\r\nffa90a3arJo3WVgkiW0XbuCPPhvV0H2/0U8O9xvXikzzIiMlOql8ExzQV6hT\r\nsQbyYgzFNeC/8bIiqQ2eR3qoFh3LDRAhVZnE+GqWo1bxhTkukbeCfrKd0vtm\r\nxXYcegTSc9KayXs0qQQL22lZUj3wQInbrQzYC6uik/+vSLN/aSV9K3NYTqh2\r\nD2HbiWidPSMXBoShS3Msv07bXiviIy/wAo46Jfmn2lysq2t9ACiJ3TYhis1L\r\nXWdNzkosKgTgj5yB8CJNiPeTfJgUQxcvxTMZmoj2CGRG6yCn2L9CCnYqn44s\r\n9ZVVlfVETH+RBQ5dqL0dhiDj3560m7wBWrJ1nwsm3ztg/pgV5S87k/EKtHi0\r\nkucX2WhlzvBaqkoPani6H5QvoyAoLtrQdbJ07axGgB38aiwv5XhbFU5SlMyB\r\nnVI6EmJK53/f7k7NwLm/i7RaQZF8fGFxsHx2fg5GK8jX33pwSMgTTRMP4jZi\r\nUkM+rqHOVYkGO/tEeFUYLZ1VfV5urHmD4kthxh6AT5j1WtCmpLF9tMA/yAlg\r\ng5AVEi86XKRHr3pKQctjwOxSjTsPW9d6SJ8=\r\n=wmvN\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.65_1649221710027_0.7602691343827896"},"_hasShrinkwrap":false},"2.5.0-alpha.66":{"version":"2.5.0-alpha.66","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"ab6dcb290c5dd579b46603168b1bd45ec70ba4f6","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.66","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-3H9aP+AOZCgg1YgZHYbj5AcjBUMtL2pzWpDKoCluKD9v+8vBDwNZkJFS2cBjVjJQTRrqrfn08KiJckbP7TT+1Q==","shasum":"9610a0dc52ce89e0d4bf090e5d916bf8a7b73ad1","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.66.tgz","fileCount":56,"unpackedSize":257108,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIGSDcYjdAVGMchQeGcx9kAIMpqpJuKVlAK6LBmdp9yU+AiBzJQJnAbVtL9gMGa9dYNu+FN9uzVHOgCfd+P+VkbTkbg=="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJiTSE7ACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2Vmo+PRAAkc8cp9hJzwi+d540TfBWyy2s6rr1bsVpbmK+AMMeGvxK9Cgh\r\nZZC4/HQEg0qP01IOjv9yfwyjxSF5+dTBWalUdpDRts4as41NaaF5i/LG6lUV\r\nxxhuzvci5FIZKFIrugDPIERVnsHbEr5Lx5Cz5CKDuBji40E6Be7V8rp3A0Pv\r\nwTgSf2P/H0uzRxPn9fkIA9H7uK6xUCqEtTjKdlCUET+3iW7JWuayLWy99TCv\r\n2ISAxRBeVTbN3IhP0mysOgKvoZOqyAPJtIgnWtVZvFFBIWFBHPVs2Y/+CqdF\r\nIK0m5LM3T3/DeyGLLMc1vSZXrUMr/vrTaU/T0wkiiFpxQwqbIALQ6WhQ21vq\r\n61g1ARJ4dQ6LN2USR5rd4r9qRd4g9/YvzPiLFvf0xaFaVxY3ohAUdYYUMk5P\r\n14sBOWmXnyM8e+6QVvkWMSz/GJmZ67fc6Th8GTM1Z2kvzmxiQC3F3AOo/NIV\r\nhXL7kPUmYpZmX4SYrOR1C0EnYP78Bj6ZGYrErrl9u9UKeYJEX41PdmPe9V/Q\r\nuQr513CI43m29WHGu3OCNnNVg5jLZlMVxcb6jMpuu2mkl4dv9/tZtwn0EsDf\r\n1xSFDBG8SQZtG0bM9TCDQ8EB+Y+xqsJHIpJW/MGr+wtWLg3YIk1cKz7WfWlu\r\nn9kC4piGjj+yMGuNIjkucwXbJwuqKIN+cKU=\r\n=1FZi\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.66_1649221947659_0.5859989879290068"},"_hasShrinkwrap":false},"2.5.0-alpha.67":{"version":"2.5.0-alpha.67","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"c207d4f072e1fdd5ae31c7f6b22b58e9b4faa973","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.67","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-ScA32Ngplyu/jwobW8Q+xj/TuCYCQXTzO2OsyPZ2pQvT8ddRu96zkHqUnmppdnuiFI8Yb817Cw8rXQV3zmY3pg==","shasum":"18526c727007c61e8b81750eff6b2afcb7deaa26","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.67.tgz","fileCount":56,"unpackedSize":257129,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIEi4W/+EI32KQ8Yyb+KH8RUhzH09P3Oyv5g8A6Re3uL+AiAOD1akxT4GQS3InoZAu+f5AvAeGZj3xYaCPal5TzCu7w=="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJiTSNLACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmouNg/+Jn16X5QEQdJSU8/5KO8hFCSL20yMikOVWXHvrhYiVg0ud1Wo\r\nBv7fyFCnaWAmF2emghp81r0LhzJHS/MoFtX6Rg8Q868EQT8sGyTnW2AJ6XgG\r\nTMIXjEbjOlawXuTwx7fIN+k7/DEvFa1EqUdt0cUPIOv1WoMKSh/wmZ2eOExu\r\n2R52N2LIo2niMT/z8ik/xUajTW0GnEP6sWn80Woz0hotFELprf5nGWGbiFOT\r\ngf2bKfTDjUPsv3rPlVaJfa/dOabezsgnPtGp8spAIh/q4aB7tEgxyB2uy51v\r\nDhq/de3jegYNOf7Mzxp3Saa6a7LRLcMvOAeAQts/a/NoPPLyeND1yyEuFaiV\r\nuF55X76u9WIZY1QcfVPNpMix45Rf1C4HBrON/YP189/mqhoZSdL7caMal5pv\r\niqScPTTArOtejPk7Y6877pwW6kYUwyfOFx8LYORoiC1RkEAURdZct0GDMMnh\r\nGYlUxB3MqLwLLu8Z3xJwe0QwW9BOTxNtKalMQrESlMzE47t++l+42HSgu17s\r\ntr1XzjNv3klXph8ndkbsjuTqu7VEBmw0bkw8u07xbUBT5bIqiVCNM/szqJTr\r\nKoqL0lPjl1z3wXdOk7iPnfNl6PNByNHna2Cu8dlDkCH6oNEuRSio9E77bas9\r\nByMRc8+nxqOzkBkto34yeZO9KDf483e+O0Q=\r\n=+rCt\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.67_1649222475198_0.5866119630167708"},"_hasShrinkwrap":false},"2.5.0-alpha.69":{"version":"2.5.0-alpha.69","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"b7731514cf7199193e6d3a0edf57873baa99c98c","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.69","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-1ypKWlD9Ihoj29YeVKdpiHzXEtUoXp/o5BbXC2QybsAa/AHW5K+D51ywavW8/9ZqqiwUod4JLzQ9PJAa0LmFTw==","shasum":"ab81b982efd075cbf666241640fc5cb4f1b11425","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.69.tgz","fileCount":56,"unpackedSize":257781,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCGOL+MXwMgPSCWolpmzzPJ82KgPmeQHirXGHeSNM1uUwIhAL58z3RmsKAu9iN99bWEKgq7YSneDlNOapuuiZhUQsM0"}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJiTcxUACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2Vmr5cg//XEF1bOZ+J/ySWNJQZzThJziBGYynFWeMwJDeOGIEnHz/hUmF\r\ntQhAw3/u2c/QnpK4h1T1ALpiWaAN/uVvZeHRjW/TsALuhke6ywGK1M1wdU6v\r\nbLl0DxVZNyW6SeOPmOhKtCv53ublZ4ec38t/QeOCQ12JECdRG4nHqbPKFRd5\r\neoOYPgK7Nogal64Vnd5uhUqSta3xSSanftM71OiAfcEWdEivkf9372qEzgrj\r\nSD9enXs2yyTR+o0Rjr29qk4DCJdIMQG9wuK3ppzjpv1ljYFiYMNXI7lxrnd3\r\nMg1+GTKsgV+Z8DnOUyU5siwtJs4OgMcwhvyKSJMzOgSiZjNoKx0L+Fyvy1j/\r\nfdQ517jTEOxPl7D410rHpAXiiEDPGssRsSOIR0Nroytq4ler4i9ZFIZTDSdu\r\nHL6JuLmbDaZzapdG1496mkXCPUE5ZT1J85GgbCUsgCS0k0zj2Qd0nBOHYPqc\r\n/dTw/+AWaqtwOMZtywUBcPM2U01MwpT9xEdAXkT+9D1dOnhWbCuyw1Hc2RSU\r\nX9k9wAu0UBPnGE1mzmTk5QGtnBphiQyShr/KlS0BG9ZK4T4H4jOyUcg6Qe0b\r\nrc6w1hbywcpF+5DO1xiLz8JFIHCgHHjxIbvHfo73kK3rP0nv7Vt6cn49KmBz\r\nqGLzgqp/c1Xw1oaXu6Yvr+JhkGmcnzepyG0=\r\n=5Vag\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.69_1649265748426_0.0026672889110894715"},"_hasShrinkwrap":false},"2.5.0-alpha.70":{"version":"2.5.0-alpha.70","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"e8884cc5563d9b798a6504eedb8ef2a9e94c5084","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.70","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-urSQ55PuH2sVNrMl/pSYhTbiF+j1W8s8aGi5hQU36SrUnsH35LLq+Y0RNhXd6uJYAJ7/4eWH3d4a6TwwTcxpvw==","shasum":"43757263539e705536a9a3395e9e43066f9c6636","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.70.tgz","fileCount":56,"unpackedSize":257863,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCID+a1w2hvY2ts1hf2bLZEczG72K+SV/3vv2gjab9GXz3AiEAykcs7MOZ4J2p+Qp6Rx5n/Ach9dLQdJtvZSYA/mD5/1U="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJiTczkACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmoHHg/7BpZoTZDpvPx66JitBKkEhmQWCoS2R65eAhuxmnQbJDHAVlhk\r\nF0+OPyIP6hZ7fCLtzreCeetxG7QTWQ6G0ndGNv8h0Q5A/ikJ6U8NM+DTOOx3\r\ngW5sPWZeg71ZiB6/z1aBLONzo42nZjGQ3WXv2jB6kq34XC6oMdeK1ZiGLw5X\r\n+UDVKRf5FBIAgO+Pv2ZKGIXFmQKDFzZJHakzwpKdrtcwoD6CFY63jzK68ybr\r\nA2u9PDDiaVW1Ne82ic9PEsRh7tVg8XYIgwD0yv5XOmzIlOR2tBIbrfsUyC/T\r\n4JB19eXnhsRlRZmefWtodnl0dEeeDYM190bDRfOn9mxzjzmZPcoOU0PPMehB\r\n0OdoGnFj+dKvhrKSHZrV4dzwRZi0zKW+0utnljjcxLaIAGtAN9tAS1GoC+Pl\r\n9R5smSxqjPw8nsKiAx9muECiKlS2AjkygoblMz3mj9BPV+8pfjBEwA4aVsmB\r\niNcZq1SLeYvRNTb0zMaSb5kE4wK9NvUXPnwX+GJ0IYsrf8T2I/vQbFJ08+yJ\r\nVgsPnVFid4tASHG6Fo9y5IYr1iQOhosDVUSQGOqDU+LMc4XE7kJZ3XQTgVXd\r\nbTWU/7foXn0FPnWUWrqdn+/DB937lN6EoC+TWgh1fpHRiniFETqZzdbsYo5A\r\nlMd14LL76uAqLbwB0sE17QoygImpC2vtffs=\r\n=aW5D\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.70_1649265892357_0.05655175419383407"},"_hasShrinkwrap":false},"2.5.0-alpha.71":{"version":"2.5.0-alpha.71","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"b1640bd420c7325c7ee2d6820c11eef47fadb4de","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.71","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-8uf9ZFwORrxOCZQmWkv2i9FqxMIC1718Es3vOTuZiL1KuQH89V+OlflEtwg3BOkluXrKgOdG7zU6xqEiTwHL6A==","shasum":"a2442ab26815e2c5e15683b68d19e3da24239f4d","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.71.tgz","fileCount":56,"unpackedSize":258304,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCPoiaX9uW/05+epMhyO/2KVQnlqPUXn4zIORkdIW/SXQIgBREWv/3v1rTnpVRvcvxOgfsZzK0C74LJLISxlFQ0BZg="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJicYFmACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmosRA/7Bj/Qp3GZeBcsgAhG1nJLLeQB32Zr+x0+sKEh6NNE0m3jpoT8\r\nMDqtx8JkUNG7ewLqzPOSLTVsJ4/kYjKCLlt1b6neORTYI+qYtDXcH2tz0YP+\r\nG7jyyt07WFMGCu1dO0apB7pIIVnq77s32Zd3V4WrnxHXEVDCMlfaSN8kmYIC\r\nL8c85AVk3pqcO2X3tJURDTtRTfZxQ/ky5Jj0r5rTzHRUKDvP8bS4+NfD6cMQ\r\n9qNTj+5B/sex3Y4jWr2VvlC4/8verWh+y92Xbq38JureFK+57CSaZYiKmUaq\r\nZ0ZD1Ah7fWnvUNJl18O1r73wb4KX+DDV4a81mR0VWPJyp532Wk5PXj4h/LeH\r\n3J5jD3f6ynyP3tOHARHHZOz3KuBntmWnhIUgu0z7dwSj/sNWKQ7SNKvC5gJ7\r\n6qhukMX4xybhCpstEVHxraf5EdbkNJekSp3w2uk5nawH5mPQ2hejkgS7Ekcl\r\ntF7a0zE5vwe8TQVULXZ5KHfikzNkxFHiBCXUdJiIxIdNisYttoM34V+tdS6k\r\nPdB/0p5hrRO53OvX2Sk7dBO1JI6G6LhzgoKewO0lqhTGrxXgPgMTtXORzchy\r\nEXrjEf2lvfmISRoHLceL2Own/hd7gIeAM6GzpQuJ888IhjrotSNGRLkY7I1H\r\nDIeuCCs74nbXln+Vbq+EcDw77tcWrFYe/0M=\r\n=8Jbf\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.71_1651605861983_0.09280324114558569"},"_hasShrinkwrap":false},"2.5.0-alpha.72":{"version":"2.5.0-alpha.72","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"f4c70649de333634347d277753fa6348f4c74671","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.72","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-B3omEpqBXdrlxy54a8A65VaNC0yv350qhYYvaPP14B2bsyFeXA8dfUmM8x1O5ZUjs/YW1zTbHeHUocA6xlSP1g==","shasum":"265b8d9207aab21d913bf8aabf0d8bc6c8a15838","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.72.tgz","fileCount":56,"unpackedSize":259351,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDZRBQKxp1qvTkG495XpBWKfYSAtJAQ5UVOmNfeMx2avAiBb8OTf08aDvRIr+N08/mge4P4LZOll24YLz4LSmF38rQ=="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJig8pxACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmrpMg/+LDTCQ5rPKJZ6RjmL8tehJqfnVbLxbNO6N2vd+iIyFhNK1ole\r\n6wxh5bTo32irq8MP2DJl0hZAQcF85XQ1YeYEUAym6VDPnF03KQjpV3QBas+3\r\no2u3o6WeTCwcXClkNMBDey+t2B2BWbBw+TA3rEx3wUVMEGFunpBJadBAmocB\r\nfp6Gt697iGuXsCs/O2lnGYQnRmgPOm5SdMV5NKYRKwz2jxrNiaq+0MMIZimH\r\n/BDMmGeDPKF2x8RoFCzr/IDCxyqJqyDmq0TGAAbkAS3ymMgULCNTLoyl08ND\r\nOZvD3XUR8Xy4D0MC3tOTMhzaCX3Sj/2JR0o6CB0cd2maenz7iGI0DCA21BcE\r\nYMPYwW82zpjOqiNxpWhSphNCFQl0dxCSW4+gSpBM/sF4JAbSTqnTVxsyhrJk\r\nKQ6akUKETCtlWa4VOKf+8vpsCXbsza4A55YeJN8zoMLMVP/KtBJ4galuo+z9\r\nBysLBXxIoBNlpWbJDyCJUHCTrYvjJI7Abcwsun9EDexFptxuQxFghz9NG2UT\r\nMNWJHzvvRjPUaPRnpErrCiUryOlQ2NpTxV2oSCGCkMS2evf7bpPGrvAiP17Z\r\nZPZdj5nCiTv3xS/fl4D6c2eytmwB89lewlKe7Gxh/hPct2P4IvdUGBcdveCi\r\nzUL6R2cS7Xy4HVtPz+MSg1x0vtvgFgb4Id8=\r\n=OwSQ\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.72_1652804209294_0.7451909131966372"},"_hasShrinkwrap":false},"2.5.0-alpha.75":{"version":"2.5.0-alpha.75","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"c773d63c598112b8e5690d517a93a5af73682fbb","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.75","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-9jxU6vkyUEq1gupnDi7g8gLtll+SD0d9Zup/QdrU2h85MaVZPcCqKYL37ruZ9T07YR6YQ8f16sjlI3XM9ivgyA==","shasum":"3b0308e5ced5e250811ea8514430e3c934d22d01","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.75.tgz","fileCount":56,"unpackedSize":258830,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDpTY7+reNJi7TNrIuSZ7tuSfkiLGb44Pd/Xba7W2m7rQIhAKaCO8mNCCY3blD+Spch7AlMnH8rNRmPSKoIt/N7e0K8"}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJii8QGACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmqjqQ//QsdZwLH8DKkYsBzK1qpckJZpp5+jveHagJkNjlKZXzoFkUxN\r\nwPwAk65ePS97AWmOEMRr4SDkx3yRORLlIS0Ak0ClGOScEFrkbp4XNDzUHx4T\r\nXOZOCYPBYfhVAG6SGHmjjS/uLxRwg5KGraUO7oB1RT6Wh6ePzZjd8ZMU1yJ0\r\nxPH7no/CO7uyRWYWbzCKB9CO8T0hKfgE32Gv2UzxB4I9xhShJZ+cSbxD2HTA\r\nVOF4uur6yNDaCaeqmA7ESAiQExF32Ikz3qB/ADPntcrh1J7T0tTE9bWnK4Q+\r\nKT5hAQfjIA+JsDKuchXSnHe+LqxoiTYBuT5cQA7lEx9DpVMW4+SB+p0Klacc\r\nqrJizsi2E2uY4ebsmUJVtf8bzkJJ4I66uOQymqgIdoGNsL/RJ+2TICPkY4eW\r\ngdxwgO+8rLKk6aLuiD4R8XUapgAlAXEC6GMK8GOi7zzSS32bSsunltN13hgs\r\nZWVj8y8YRPxtDu2CfnRiXzLNY6iXFOz7xZs1k3YepaS2lu5pmjlD36UGWsb3\r\nqMoqmrTXN9kkI25uVaq1XdIRG8582LhnuFN4qInj9tQjGGmVbqLoZdjjqsWy\r\nnmrlW2EwnkZDoTjhx2Gorhk7TXVRxoG/QZRe0aLzwqiA3CdGODK9rCqCQ3b2\r\nAt5ufH/b4JRqqr2fvSZkuMEyqJNHh9fsMRM=\r\n=7eW5\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.75_1653326853869_0.49186453022700705"},"_hasShrinkwrap":false},"2.5.0-alpha.77":{"version":"2.5.0-alpha.77","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"4f084c93b439753a21f71ecc73778cd0bd70ef6f","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.77","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-JQvt5+W6+luiA56xGB738wARMVWZ2LeLBcE3T6nU2sIkkDSuu48Z/bx30hBuL/LeSaVDDL/0dSAq4CzX6OL55w==","shasum":"a6b62858c399e55e7de211814f17596bd57b8299","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.77.tgz","fileCount":56,"unpackedSize":258911,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBgurm9gUgkSVFT84+HOo++O343EScActvwHaCC9AJ9fAiEAttmGA/u2TKL4V5w+xgUJ4feVeiEoqCNU7sv8eM3aC9I="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJi/Sg5ACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmrnhA/+PmJSG+yEdFEOCoSRJy7yS3tOmbxvCKQeqO+8cmDmZ75GZLgx\r\n3Q5YShEKLPmDLR2JYeVnFSwnEZyF5WW7uiIV/SCuhRPVgnxdGzi2R5HFXuG5\r\n0DFfwEV9CilA7isVPC7H/1XIMhfuND3zbEde8J9kjZwnznnGGJA84wyx4QTw\r\njM9rGcG0tMJAfCD1MaXz0AYkghJtJ+Fm3UShUPNlXb6G5GnmxAfeSGNTjddR\r\njaAbUVrGWSa+jK7Z4g5ofdys8XmWNdYLXhrL04UK6t/cidnvurxYIR6F0P1M\r\nE/OD8My+Y1W5wXQ/C9aqVhlfhXxQKnMRK6W0WRkqZR4zONclSlZs9ogIxcyP\r\nkPGmAbq3Bedcoys75HgMf3ok9AM0Q+D13rWuN5UMxj8QY+eFNNXkGmRRc8pA\r\nk38Y5FoYmzC0onHspim+3wZzH1Aed3es0P3jjvUQU0nHWiY8kDLWZaOrwpuF\r\nG40UoL2CPVVNyiJtyi3/oZCcGcW7rG3QOHo5AuuT6f6RWZKvH5DLhh7ctDdT\r\n82CjtjJENHL8Gr/sEdXdpnGD1JpF7C8OkFkEJdJw1WCTamoZQvD+DPJPURiV\r\nFdorllCXlTR4NNcDAY2ffVvtRsartTdVsDnW2ed5FZHmOnbDggLMeooIe8Gt\r\n7T7fDYHj76LZJ+TNCkIhqZE4IyNp9PfAn9M=\r\n=KVOR\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.77_1660758072807_0.4348396710009921"},"_hasShrinkwrap":false},"2.5.0-alpha.78":{"version":"2.5.0-alpha.78","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"64d72ab117826c3ce3ddf733fa4763726ab33fd4","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.78","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-q6JYNRZjhiaNdpmbQi46euW+St1ovjQnBernhDtSjGpXiwtmtAxIs74qH1xMb2zK+fnPVxPhdm15BLTcRVWkPw==","shasum":"efc678473cb536b64389b26a52c9c59f23a8d9d1","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.78.tgz","fileCount":56,"unpackedSize":258962,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBc34EzIW5HChkX0MAvgHmX/Uny58jZPncYDIJlnUO88AiEAscYOaiz7F5/yIlVrL5PMyvI0FphSdx0/dDf665rS2Jo="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJi/SmwACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmogUw//V5aRf9+5Dhrw4VPPCkFoekqY9o/AjSkIZM/BqOwD2hKWTocZ\r\nwk0PpTjmO6WgQhqIewxCmCE+muTDADiFSmvz3xuZ9HXBXWYFQEP/eparN6qf\r\nJ+2M87Zh4MF2PMEZ4k+Nml9ZeBwflghv0h/dBcDB7OM7n1ypCfz17y8rHwrV\r\nMeuCwpDoyBmi88GH3ALtRwTZeHW0wKrKe2WROuQVRhMtM4K+aBTMRuGuuvKf\r\nY5vRx91QjbbTvFay8PezAJ0afvef74q/X7+RX1QuJ/N10iZItfwXCSED61+p\r\nMHqAAM8NVF1CiUyGF22hHHBX1yzf4RhTBqHGjkYniezwcNGS3eo81JHKISVr\r\npU3FHxhfdin7HcipCrNcoeZN5sYaTX6MYXm2KTlyZzZmLHHBBrJzX8Equnoa\r\njQE9zuOvtfx3GmYV1f9nyNkj37nCCaLTpJq+H/GFfClI9GnG2c8AtZkuf9To\r\n673dbIzHumGVoI5mfC2PFnq/HHDjqYXN2yQqf9zTv5EsXtyC+hdYl7Aet21C\r\njXac2gx99YBU0nniCkJMcFegU9IvadNbf6gRWNkhjA82d0zGruBFTuhyii+c\r\n5OYm+fXvKpw/g0AiNlTVoALsLAwKFO9y9LKmw6NQCsyJ03x+aXuSuTd3U93X\r\nQSnhh+f+SRqh5rDW7xbcmeq4jvVb/VsUWxA=\r\n=ICj0\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.78_1660758448528_0.2753248140446263"},"_hasShrinkwrap":false},"2.5.0-alpha.79":{"version":"2.5.0-alpha.79","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"bb90f6479eeffdf1af0b74ec428737cbfadb81e6","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.79","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-N6cwbmvR5Mu1V1Y/VkELmJM08okbl/4rCmjuPDBevR5utIUROpIUjt0C9Ysd+oOHR64Zn+0NQtX0iKTuCJqqkg==","shasum":"4419e52ae53584fc6bc4fae4e28a4292861296aa","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.79.tgz","fileCount":56,"unpackedSize":260110,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCPCcdsDG7OR30+Ga1NFx6MfhbYJCPKx/1Ax1lJVqzhGwIgJoY7Arzn1Z5DOFPDbnVKhPycB31NABrZ7cJWezoBU1A="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJjLNKNACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmpU1g//R4YCV0Y5YNAlXrO26+XNxJE8aayVXHKgE8EfUeUCvgCtij5j\r\ng3pCZVoJw0G6DkfJYUZGrDmf8Yo4mUOJfs79QiKo+WilYika/Q3JvDdV+jLR\r\nqXTBIMMWi2dLkLEZEDFxuUsgu2qN/rPkgm47O1YIiuMuwHcAuapGxB/iUTi2\r\npMa9Gvprz3B7t0Iwg1jrChReQAMbgNEgodvcX/bepGgGQOvDWGF0E06VsnK0\r\n/zSHrIX5sj9whq3WKSRAhaZE1LZLp04VskiL5xtw90ZOTEYHxfqE0wCLWC+8\r\nsfuLqlVu0SvwAwHh6rENA2J2cORc/XLOe1cXDdedcJSRa1c8BSPj3nVSYA+W\r\nBnGjDDGKbb23d0BWhPqbLvaCduOW2dgtyYqWHn9bsJb98j21lRWepIA62fwr\r\n7EYHAaf/d7M97/C2TVWlvXVBdqLYrDl+whJf0AJUW9+Rl+ugI6ICndwZJMhv\r\nL7fgjWQPZ0xPK73Ozn2tsHzCT/fDjLIuYl9AxUieuKnQIdfgc1+FDeOC6ivj\r\neMUJiqfuV2/nRmjRd8E0CyXarFTSvNmr08/1Cie9qNqZhelCRxRxOvrT1Frq\r\nK52UfCh2ASzRS3JKxqS0ivPKE4eQqV7Xx8AAdA1upW4rYqShhg3oo6GHWNlS\r\nqv8N2hO7zqdabTSqFMxl02YS0OeKTkhy7y4=\r\n=N8I8\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.79_1663881869007_0.07817064901396176"},"_hasShrinkwrap":false},"2.5.0-alpha.80":{"version":"2.5.0-alpha.80","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"6c0f790db6a4ebfbcb7633e71838f303cdc182da","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.80","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-El43vGSiYPc88D8EiXI9s2D/UyQu3bZTWGQaSDUkhQiCFXKgPIR91vl1Q2ml7mHlSGMKuOlElPcSrTHbXCBoSQ==","shasum":"1480f5da8d381e719a6227a12d12419c3cd18914","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.80.tgz","fileCount":56,"unpackedSize":260218,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCp8TxncX2kufDSVueihpw5Z6IhyNw6892qoX9Wfd8tlQIhANeCGMOqjhUj7g9NdwozslJWp2Q+1fXH8XYabA2C8Wi2"}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJjLNWtACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmpVSQ/5AMuwq/YyUvnE6TkSPsZ2s8oQpB5hSfTEy7c+avMjVdvGfjXt\r\nVMz2Ps2Z3zYrOqry17b/mVwpsDi4OYYUn4t65N+WC9Szam9RhF46GVZ+NER2\r\njUCexbrPXrOIzunq66vylvDoNbKMGqvS8fbZhm+ZKmU1AIV7n+faAtApGerc\r\n3DHfPHQU3wZEi71zLlOQH3C3pzC+Brb5An/smFszu5THAsmtWz5iFIP1c1yb\r\nr3TaqbMIe6MVFd8plxeLxCwKtmS66CQtelyEOiE4UI+ZdTI7DeVqtbvh8wne\r\nSm0MxZ6ZJQ80MCkiGRiffJ+xwOoaCBbZjmvXIYnXJdu4rB6i1tAi031REktK\r\newYEEK6e34a+p1VIEvMZVdICJnNXN3eWHiOyELpz/VXKnt41g8Nu2B4WJDxM\r\nS1g+QwPrQ5OpjG/SXADdG5PyAfZBCBzLi3e6Qop9sw/VXWH/J0VRsfzcwKPs\r\nvu21aLRTjhOmXAGojdCcni2xEbHSFOFqe0YtqJmUWkYD2yFzXeyNAXJnLVkc\r\nErLKQtxjewqKinFs1W0m/UgYhaEpuP+Z+cRDmbmYKZElxmsoXbkG/pFKn8ok\r\nHJ6t6sYiHG9z/esIfhXy6PBQwYoUhgcYB9rO36JRB7+iNDuEVBuGEjf9nLD3\r\nJgvGux1NJ4WhH4Lq/tdKKu4RDRnZ+4f4jPI=\r\n=HzfJ\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.80_1663882668950_0.028563343424191867"},"_hasShrinkwrap":false},"2.5.0-alpha.81":{"version":"2.5.0-alpha.81","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"~4.17.1","knex":"^1.0.5","pg":"^8.6.0","yup":"~0.32.9"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"9256d6fef031344b3d47e1332c9f2e3d5736356d","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.81","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-yNqOCWwu/uPPQZaTYsiYqxM9gp7pT1OW6BIuwpKOyRYzcuYMuTZHbHzuk6QMWuDlHeg4KOkFbJLbOxgaTs32Vw==","shasum":"c2c4f20d57e73be14bf24cfbe0e2e78e739f7452","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.81.tgz","fileCount":56,"unpackedSize":261034,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCnNa3S0/taS9lefF2FOUXPhvAlpMQg2dIgDAdG1QyDpQIgX0esc+2ddLifKq/IbNru+31E8O2iGwmB4nzhExK2Tgo="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJjMkoGACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmpM1Q/7B5dsGwDN6G87wxByhAH+1NjS0QWsJCiHTOiamfxhm0Ber2qL\r\neeujcqMTt/GXB4NQhom+BBLEEEQEhHWGKLWvldAgaPl1/djVjH/GcPPimZLm\r\n374/+ftTYGkPnr45sp/qhs7j55mLipiGosD+OrWKdmvRStcax+y+8lCjjqAf\r\nhPslbF4b416za+bveNv/fiH1n8FnxIBRghjBWzaRVnRuLp30MdKK2jJpZ08g\r\nWEhc26uqw0ZQ38+aLvua2BxbnptY8uPhLp/TE9pfbI7gmNoayLdkuSFvZpAP\r\nss9gOq74hfnHWhf7U3ApVJHoXxM3tUCKw1oomvdiJFevF00OJO7+XSNVKH5U\r\nOUnW8CbqUQTuxs50HeI2J7CQr07fgh+4XlTHhtH8RUMNquELkNsKA7ga1Xme\r\nWxjcFAp6Igac47I8Ml5HkRu4sdivuLPWqAWtJdq0UEyc03FDhT3T2UrgcoiO\r\nXTB8B6yVB/Uu6cQed2+T58PzoPmrQNqIfyobaYjrqy2pQJ2gAQxv1r+gcoZc\r\nMbS6mWkqXy7fQbaGFIVv2gFOe5uBXNsjSh6OeGE6W9WO9pd4cxhDJH4nkvGY\r\nRQONcC4WjkMjGrmbYP9v3kj0fs9fhK8VZPNFwgosX5Rd+VBS3zwbtSAfnl0M\r\nB+YOAD+cESLjPacYATC++1uaRrgwCcGf5FQ=\r\n=GA+K\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.81_1664240134432_0.481516335877898"},"_hasShrinkwrap":false},"2.5.0-alpha.82":{"version":"2.5.0-alpha.82","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"*","knex":"*","pg":"*","yup":"*"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.5.2","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"9ca12fb592c8ff6b388dbf77055d7e67a0222b3a","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.82","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-mhIy9949UjH3ZGvGLhiDrXvP92c9VQhq+psZSLDc75AzctLoLBnZH1Q5PI3EqGLssF0q0jgIatDSg2cxMC5m4g==","shasum":"c1b51361f25e61b13cffaf236f6c0b91eba177ce","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.82.tgz","fileCount":56,"unpackedSize":261012,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCH2nDDgN0yMqM0zQ4s/qPGUGkuZceq1pO2cRWhBh/yw0CIQDWTp+Fj5pwnEs0l6P5coNjRmg4TlPzL1Ij7XIzhO11Dg=="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJjvuFFACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmrJXg//RfHppSWnd/MpxM1ftTV48OqfFkBQM/vk8SkPyrCAu4QidgIc\r\nWucY/Cd9MhcpPifNko7Z2vgL3NATU5E1IBnACUk6kNcLYEGAQqImFeaXOJ2G\r\ne3v2Ig7pI/hEtDJFZL3DqUWk6Ln0pS5yYHZCmeuKr9eIz2p0AH0HeSw8vtwK\r\no/HLjVKkAHFPXcT4r4+Ge0qRPhR8AG/WRPopDuUfOQ4AOLUTJyb9GcNthLKz\r\nbl09yW4k5EOTJ5t2O8inHM2N6DCx05rQAC2cMvMPlYTH+g54kMm4bmyEs4Qb\r\nVFRhSEmYMmboNhoEUXGznLuwosQNinWxqdhXeC8l0UCS9D+uoal2cWmunJnh\r\nhc8GbhRxX9Z5vGhiojoR9H/5kY+SRFjzoT8TFRYD3F5xVlqw1cFJorCJKiqh\r\nVP5CK10+eglOZLzZNDvbgKEQQXjsZisNuh6riCVFgkiCCHgw+IVZqeRot6k8\r\nxpQG64i/YYOTN17gKCH/2/fB/sXFLfD8HLCeb+R53ZkEvqZF25z1CvSGOocL\r\nLoFKCdaZtHYlB0mdYf+LtVxzrwE/Fp9Z0PSgm5kwfzA1QEFZ4XE+/80ej6zJ\r\n1L2tg/Hw7eA0Wh0Y5KmCWzfqdJVZiBXSTgyoo0q1vhMYod0OZH9uFhFKtO6G\r\n8EUotaUcSRNOKZqQAl5x6Byj9cX2zLX3HNw=\r\n=2yyW\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.82_1673453892870_0.1656769993285876"},"_hasShrinkwrap":false},"2.5.0-alpha.83":{"version":"2.5.0-alpha.83","license":"MIT","main":"./dist/index.js","source":"./src/index.ts","types":"./dist/index.d.ts","scripts":{"build":"rm -rf dist && tsc","test":"jest","prepublishOnly":"npm run build"},"prettier":{},"name":"@synvox/core","author":{"name":"Ryan Allred"},"peerDependencies":{"express":"*","knex":"*","pg":"*","yup":"*"},"dependencies":{"aws-sdk":"^2.868.0","inflection":"^1.12.0","qs":"^6.11.0","set-value":"^2.0.1"},"jest":{"setupFilesAfterEnv":[],"preset":"ts-jest","testEnvironment":"node","coverageDirectory":"./coverage/","collectCoverage":true,"testMatch":["<rootDir>/test/**/*.(test|spec).ts?(x)"],"moduleFileExtensions":["ts","tsx","js","jsx"],"globalSetup":"<rootDir>/test/setup.ts"},"repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"np":{"yarn":false,"contents":"dist"},"gitHead":"94b1d3b627643c11762ac0b112add18a582a47b7","readme":"# `@synvox/core`\n\nCore is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.\n\nIn development, Core will read your database schema and store its structure in a JSON file. It uses this information to create endpoints to read and write to these tables.\n\nIt has many comfort features along the way:\n\n- Policy restrictions for create, read, update, and delete\n  `GET /answers?userId=2 -> 401`\n- Selective eager loading avoiding N+1 queries\n  `GET /posts?include[]=author`\n- HATEOAS links for traversing the graph and discovery\n  `GET /users/1 -> { id: 1, _links: { posts: '/links?userId=1' } }`\n- Validations and conflict detection\n  `POST /users {username: 'user'} -> { errors: { username: 'is already in use' } }`\n- Customizable yup schema per table\n  `schema: { email: yup().required().string().email() }`\n- Graph updates, inserts, and upserts\n  `POST /questions {answers: [{ label:'A', isCorrect: true }]}`\n- Batch updates\n  `POST /items [{ prop: 'val' }, {prop: 'val' }]`\n- Tenant id enforcement\n  `GET /:tenantId/users -> 400 { errors: { tenantId: 'is required' } }`\n- Cursor and offset based pagination\n  `GET /users -> { nextPage: '/users?cursor=base64cursor', items: [...] }`\n- ID modifiers\n  `GET /users/self` vs `GET /users/1`\n- Query string modifiers\n  `GET /deals?userId=1 -> select * from deals where id in (select id from user_deals where id = ?)`\n- Derive default parameters for a table\n  `POST /posts { body: 'Yo' } -> 200 { id: 1, body: 'Yo', userId: 1}`\n- Before update and after transaction commit hooks\n- Created at and Updated at timestamps\n- Soft deletes with cascading (i.e set `deleted_at` of dependents on soft delete)\n- Selects known columns for a table instead of `select * from table`\n- Support for hidden columns and readOnly columns\n- Exposes a Server Sent Event endpoint to listen to changes visible given the user's policy\n- Unopinionated Authentication\n- Support for read/write replicas\n- Exposes apis in `camelCase` while communicating with the database using `snake_case`\n- uuid support\n- Multiple schema support\n- Add an `EventEmitter` of your choosing to listen to events\n\n## Authentication\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\napp.use(\"/api\", core.router());\n```\n\nCore will create a `context` for each request. This `context` is created from the `req` and `res` objects and should provide enough information about the client sending the request, including information about the entity to which they are authenticating.\n\nYou can use any type of authentication library here to populate the `context` object, read from `req.session`, or similar.\n\n## Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.where(`users.id`, user.id);\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// GET /api/users -> 200\n```\n\nCore tables have a policy method, where you can modify the query with the context defined earlier. In this case every query to `users` will now have `where users.id = ?` appended with `user.id` added as a binding.\n\nA request's `mode` is also given which is `\"insert\" | \"read\" | \"update\" | \"delete\"`. You can use this to create common authorization schemes like a twitter clone:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"tweets\",\n  async policy(stmt, context, mode) {\n    if (mode === \"update\") throw new NotAuthorizedError();\n\n    if (mode !== \"read\") {\n      const user = await context.getUser();\n      stmt.where(`users.id`, user.id);\n    }\n  },\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n\n// POST /api/tweets { body: 'Hello World' } -> 200\n// PUT /api/tweets/:id { body: 'Update' } -> 401\n// GET /api/tweets/:id -> 200\n```\n\n### Role Based Authorization\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"adminReports\",\n  async policy(stmt, context, mode) {\n    const user = await context.getUser();\n    stmt.whereIn(\n      `adminReports.orgId`,\n      knex(\"userRoles\")\n        .select(\"orgId\")\n        .where(\"roleId\", \"admin\")\n        .where(\"userId\", user.id)\n    );\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nEach query to a table can be modified, so you can filter by a role existing in another table for the current user.\n\n## Querying\n\nLets say you have an application that has a `products` table like this:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"products\",\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis will build endpoints for `/api/products`. If a client requests:\n\n```\nGET /api/products?name=Paper\n```\n\nCore will build a query like:\n\n```sql\nselect\n  products.name,\n  products.price\nfrom products\nwhere products.name = ?\norder by products.id\nlimit ?\n```\n\nFilters for each column on `persons` are available as a query param. Additionally, if you need to query given multiple values, use bracket notation: `?id[]=1&id[]=2`. Bracket notation will add a `where in (?)` clause to the query.\n\nIf you need more control over your query, there are a number of operators available:\n\n- `/table?column.eq=1` which adds \"column = 1\"\n- `/table?column.neq=1` which adds \"column <> 1\"\n- `/table?column.lt=1` which adds \"column < 1\"\n- `/table?column.lte=1` which adds \"column <= 1\"\n- `/table?column.gt=1` which adds \"column > 1\"\n- `/table?column.gte=1` which adds \"column >= 1\"\n- `/table?column.fts=Search` runs a full text search on the column\n\nIf you want to query for rows `not` `eq` to a value, add `.not` after the column name:\n\n- `/table?column.not.eq=1` which adds \"not column = 1\"\n\nIf you need to build a more complex query with ANDs and ORs, use bracket notation:\n\n- `/users?isPaid=true&and[isAdmin]=false` which adds \"where is_paid = true and is_admin = false\"\n- `/deals?isWon=true&or[isLost]=true` which adds \"where is_won = true or is_lost = true\",\n\n### More query options\n\nThe predefined query filters are not always enough. For other special cases you can define a `queryModifier`:\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"contacts\",\n  queryModifiers: {\n    async fullName(value, stmt) {\n      stmt.whereRaw(\"contacts.first_name || contacts.last_name = ?\", [value]);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThis way you can call `/contacts?fullName=Billy%20Bob`\n\n### Defining a special ID param\n\nCalling `/users/whoami` or `/users/me` or `/users/self` is pretty common in most apps. With Core, you can define a special `id` value\n\n```js\nconst core = new Core(knex, (req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"users\",\n  idModifiers: {\n    async me(stmt, context) {\n      const user = await context.getUser();\n      stmt.where(\"users.id\", user.id);\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Eager Loading\n\nEager loading is done through a reserved `include` query param.\n\nIf you have an application like this:\n\n```js\ncore.table({ tableName: \"posts\" });\ncore.table({ tableName: \"comments\" });\ncore.table({ tableName: \"users\" });\n\napp.use(\"/api\", core.router());\n```\n\nAnd a client calls `/api/users?include[]=posts&include[]=comments`, then the response will include the posts and comments for users.\n\nThe same works for requests like `/api/comments?include[]=user&include[]=posts`. The response will include the user and post for each comment.\n\nWhen an `include` is selecting a collection without bounds, i.e. one-to-many relations, the sub query will be limited to 10 results. This is to limit eager queries that over fetch without pagination. To get around this, make a separate, paginated request to the collection.\n\n### Eager loading queries\n\nSometimes the data you intend to load is not in a related table, but is in the database. For these scenarios you can use `eagerGetters`.\n\n```js\ncore.table({\n  tableName: \"users\",\n  eagerGetters: {\n    async assignedTicketStats(stmt, context) {\n      const user = await context.getUser();\n      stmt\n        .from(\"tickets\")\n        .whereRaw(\"tickets.user_id = ?\", [user.id])\n        .where(\"isOpen\", true)\n        .countDistinct(\"tickets.id as openTickets\");\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=assignedTicketStats`\n\n### Other Eager loading\n\nFor other times there is no way around a N+1 query. Sometimes this is simple like concatenating a first and last name and sometimes it is making an api call to a billing software to fetch customer information.\n\n```js\ncore.table({\n  tableName: \"users\",\n  getters: {\n    async fullName(row) {\n      return [row.firstName, row.lastName].filter(Boolean).join(\" \");\n    },\n    async paymentStatus(row) {\n      const status = await getStatusFromBilling(row);\n      return status;\n    },\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\nThese are called with the `include` param as well. e.g. `/users?include[]=fullName&include[]=paymentStatus`\n\n## HATEOAS links\n\nTo provide hints to clients about the relations between apis, Core will add three properties to each row table:\n\n```json\n{\n  \"_url\": \"/tasks/1\", // url to this row\n  \"_type\": \"tasks\", // pathname to this resource\n  \"_links\": {\n    \"epic\": \"/epic/2\",\n    \"user\": \"/users/3\",\n    \"comments\": \"/comments?taskId=1\"\n  },\n\n  // assuming these properties are on the row:\n  \"id\": 1,\n  \"epicId\": 2,\n  \"userId\": 3\n}\n```\n\nEach key of `_links` can be included in the `include[]` query parameter.\n\n## Validations\n\nCore uses `yup` to build a `yup` `schema` for validation. After reading the column information for a table, Core will build a basic schema to ensure properties are compatible with the table schema before any transaction is opened.\n\nFor example, if you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  email text not null unique\n);\n```\n\nCore will build a schema similar to:\n\n```js\nobject({\n  id: number(), // not required because a serial column has a default value\n  email: string().required(),\n});\n```\n\nThis is fine, until you realize email needs to be a `yup` `string().email()`. You can define this change as you describe the table to Core:\n\n```js\ncore.table({\n  tableName: \"users\",\n  schema: {\n    email: string().email(),\n  },\n});\n```\n\nCore will `concat()` your defined schema to its internal schema.\n\n### Unique columns\n\nCore adds a yup test when a column is part of a unique constraint. For example, if you `POST /users { email: 'existing@domain.com' }` and that email is already in use, the client will be sent a `400` status code with this body:\n\n```json\n{\n  \"errors\": {\n    \"email\": \"is already in use\"\n  }\n}\n```\n\nThis works with unique constraints on multiple columns as well. If you have this table:\n\n```sql\ncreate table users (\n  id serial primary key,\n  team_id int not null references team(id),\n  email text not null,\n  unique(team_id, email)\n);\n```\n\nThen `POST /users { teamId: 1, email: 'existing@domain.com' }` and both `team_id` and `email` already exist as a pair in `users`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"teamId\": \"is already in use\",\n    \"email\": \"is already in use\"\n  }\n}\n```\n\n### Validate without write\n\nIf you need to validate an update without writing it, append `/validate` to the url. For example, if you wanted to validate that `POST /users { teamId: 1, email: 'existing@domain.com' }` is a valid request, call `POST /users/validate { teamId: 1, email: 'existing@domain.com' }` first.\n\n### Validations on GET\n\nCore will use the yup schema to validate url parameters and query parameters. For example if your table has a `uuid` primary key column and you call `GET /table/abc`, the client will be sent:\n\n```json\n{\n  \"errors\": {\n    \"id\": \"must be a valid UUID\"\n  }\n}\n```\n\n## Graph Updates\n\nTo update several values at a time in a single transaction, you can include related tables in `POST` and `PUT` updates. For example:\n\n```js\ncore.table({ tableName: \"courses\" });\ncore.table({ tableName: \"assignments\" });\n// where a course has many assignments\n```\n\nYou can create a `course` and many `assignments` at the same time:\n\n```\nPOST /courses\n\n{\n  \"name\": \"Course\",\n  \"assignments\": [\n    {\n      \"name\": \"Assignment 1\",\n    },\n    {\n      \"name\": \"Assignment 2\",\n    }\n  ]\n}\n```\n\nThis works in the other direction as well. For example:\n\n```js\ncore.table({ tableName: \"epics\" });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nYou can create a task with an epic at the same time:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Upserts\n\nIf a table has unique columns, you can specify that you would prefer Core to attempt an upsert.\n\n```js\ncore.table({ tableName: \"epics\", allowUpserts: true });\ncore.table({ tableName: \"tasks\" });\n// where an epic has many tasks\n```\n\nThis way, if the epic with slug `epic-name` is already taken and visible in your policy, you can upsert to it:\n\n```\nPOST /tasks\n\n{\n  \"name\": \"Course\",\n  \"epic\": {\"slug\": \"epic-name\"}\n}\n```\n\n### Complexity limits\n\nBy nature, graph upserts allow requests that may update many rows. To guard against abuse you can specify a complexity limit for Core and weight for a table.\n\n```js\nconst core = new Core(\n  knex,\n  (req, res) => {\n    return {\n      async getUser() {\n        return await findUser(req.headers.authorization);\n      },\n    };\n  },\n  { complexityLimit: 20 }\n);\n\ncore.table({ tableName: \"epics\", complexityWeight: 2 });\ncore.table({ tableName: \"tasks\" });\n```\n\nThis way, a request can update 20 rows, but each update to `epics` counts as two.\n\nThe default complexity limit is `100`. This is high so you would not run into the limit under normal use, but low enough that a malicious query has a limit.\n\n## Tenant IDs\n\nIf your application has multiple accounts that do not interact with each other, it may be helpful to require a tenant ID on requests. Doing so paves the way for sharding on tenant ID through tools like Citus.\n\n```js\ncore.table({ tableName: \"tasks\", tenantIdColumnName: \"orgId\" });\n// now any request involving tasks requires a tenant id\n\napp.use(\"/api/:orgId\", core.router());\n// optional, but you can specify the tenant id as a url param\n// and it will be merged into the query parameters.\n// query parameters win over url parameters.\n```\n\nAll queries done by Core involving a table with a `tenantIdColumnName` will include the clause on the query. I.e. `where ?? = ?` with `[tenantIdColumnName, tenantId]`.\n\n## Pagination\n\nCore supports both offset and keyset pagination.\n\nIf you called `GET /items`, you may receive a response like this:\n\n```json\n{\n  \"_links\": {\n    \"count\": \"/items/count\",\n    \"ids\": \"/items/ids\",\n    \"nextPage\": \"/items?cursor={base64cursor}\",\n  },\n  \"_type\": \"items\",\n  \"_url\": \"/test/items\",\n  \"hasMore\": true,\n  \"limit\": 50,\n  \"page\": 0,\n  \"items\": [...]\n}\n```\n\nThe `meta._links` property contains a link for the next page. To use keyset pagination, call the url at `nextPage`. To use offset pagination, specify a `page` as a query parameter: `/items?page=1`.\n\nFor both pagination schemes, you can specify a `limit` to limit the number of items in each response. The default limit is 50, and can be increased to 250.\n\n## Counting\n\nTo get a count of rows in a collection, call `/:tableName/count`. You can add query parameters to count only matching rows: `/tasks/count?userId=1`.\n\n## Getting IDs from a collection\n\nTo get a list of ids of rows in a collection, call `/:tableName/ids`. This endpoint returns 1,000 rows at a time and supports offset pagination. You can add query parameters to get ids for matching rows: `/tasks/ids?userId=1`.\n\n## Default parameters\n\nIf you want to set a property when a table is created or updated, provide a `defaultParams` method.\n\n```js\nconst core = new Core((req, res) => {\n  return {\n    async getUser() {\n      return await findUser(req.headers.authorization);\n    },\n  };\n});\n\ncore.table({\n  tableName: \"posts\",\n  async defaultParams(context, mode) {\n    const user = await context.getUser();\n    switch(mode){\n      case: 'insert':\n        return {\n          userId: user.id\n        }\n      default:\n        return {}\n    }\n  },\n});\n\napp.use(\"/api\", core.router());\n```\n\n## Different Knex instance for reads and writes (read replicas)\n\nThe first parameter to Core is the Knex instance. You may also provide an async function that returns a Knex instance.\n\n```ts\nconst core = new Core(\n  async (mode: \"read\" | \"write\" | \"schema\") => {\n    if (mode === \"write\") {\n      return Knex(); // knex for write\n    } else {\n      return Knex(); // knex for read\n    }\n  },\n  (req, res) => {\n    return {};\n  }\n);\n```\n","readmeFilename":"README.md","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","bugs":{"url":"https://github.com/synvox/core/issues"},"homepage":"https://github.com/synvox/core#readme","_id":"@synvox/core@2.5.0-alpha.83","_nodeVersion":"16.13.2","_npmVersion":"lerna/4.0.0/node@v16.13.2+arm64 (darwin)","dist":{"integrity":"sha512-xhRuEivu5er5sNk53MuIBR/KstInGbLkRorw/Y7KU0rw/4Z1jRLbjPnr+6zOqAOTcKx+ltv1PZeOlrASByxWow==","shasum":"082eb3b8b278ec48f68a6b9640bc92b1196dc8b3","tarball":"https://registry.npmjs.org/@synvox/core/-/core-2.5.0-alpha.83.tgz","fileCount":56,"unpackedSize":261013,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQC344/jeg0tpSetSHFYY9LPz7nuWswXebACQ72o1tppRQIgKr1oit0oXOICDLYtUs1mrMVsLD2KkQB4z6ZrJFND5dQ="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJjwEGjACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmrKRw/+LSwDF/e6ACt0MQlDnw1x2iUWSIWz8NW7Q2i4TEBQDtedfIad\r\npxNZXznKYWelnumZGHCp+f3kp8exsTgPZpCPtA0wVsj99Acs3WVJ6nzmvrOh\r\n1FuoMq8+P67U/Ur0Dg3lBAV7ly0ms6KYVkSgmdY+P/D2vO7ux8nIpMKcSEG7\r\nAIsgc1SvHe3yEUwSqMsIN3vL6hCSh+ttoutrgpgQ7BSMNyzAes8s1hkgI5qc\r\n7sDVY/7tjXAZoY21p9kLGlIVLWG+gztnzS8n81Lf71XmXcA9oZ1enjig3PDE\r\nsFU/uc2O6FnOrJFRDf7BluN6FKgB59mzCuC8rY0pg8BLCtJ5x4jghZRnsJia\r\nfCgktH8FMisLpM6glAPxFntwY+cx62aIZFoWOSkGbqXEyFZZf3bLHnijyFQh\r\nmhXHFu5IAYqV1sY6VadVEc6fEOFn3IcgBv0PE8R0xsJUI+nOC5uElf0t/pNH\r\nD2OHofza5p8OOCVhB/07b+kI8MTHdduQbX992i++PDA3FdJFTE05Y/HX1jVV\r\nDtG+uvMIhG734l/5GHjWz11dw1oxMRCKE9z3hWsIU0pmNOjxCT5uLEJnmTlH\r\nlyqHBfmnxZbZQoElYpMUMzwPzi35X6DQgKvXvOa41G6U9FvuDh5Je1DnJPyG\r\nzCdgYdc9CxVdNcAFZ/n8uxf1lcLE5uiU7Fg=\r\n=rkxJ\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"synvox","email":"ryan@allred.xyz"},"directories":{},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/core_2.5.0-alpha.83_1673544099395_0.6170968015781786"},"_hasShrinkwrap":false}},"time":{"created":"2020-01-18T17:53:44.007Z","0.0.1":"2020-01-18T17:53:44.194Z","modified":"2023-01-12T17:21:39.688Z","0.0.2":"2020-01-19T23:24:58.218Z","0.0.3":"2020-02-08T05:47:51.031Z","0.0.4":"2020-03-14T19:30:07.611Z","0.1.0":"2020-06-08T01:50:30.314Z","0.2.0":"2020-06-16T01:21:44.629Z","0.2.1":"2020-06-16T05:29:22.355Z","0.3.0":"2020-06-20T23:31:15.645Z","0.4.0":"2020-06-23T04:39:49.913Z","0.5.0":"2020-06-25T03:51:39.605Z","0.5.1":"2020-06-27T21:11:15.132Z","0.6.0":"2020-06-27T21:41:37.044Z","0.6.1":"2020-06-29T21:27:44.326Z","0.6.2":"2020-06-29T21:51:50.955Z","0.6.3":"2020-06-30T17:42:50.881Z","0.7.0":"2020-07-16T01:01:56.433Z","0.8.0":"2020-07-16T05:31:35.205Z","0.8.1":"2020-07-18T04:57:23.338Z","0.8.2":"2020-07-19T05:38:24.079Z","0.8.3":"2020-07-19T22:42:06.425Z","0.8.4":"2020-07-22T01:40:16.536Z","0.8.5":"2020-07-22T03:17:43.963Z","0.8.6":"2020-08-07T05:31:44.431Z","0.8.7":"2020-08-08T03:05:19.904Z","0.8.8":"2020-08-08T20:40:27.279Z","0.8.9":"2020-08-08T23:51:24.076Z","0.8.10":"2020-08-08T23:55:42.211Z","0.8.11":"2020-08-09T03:38:26.080Z","0.8.12":"2020-08-09T03:56:29.588Z","0.8.13":"2020-08-27T01:13:37.367Z","0.8.14":"2020-08-27T01:31:18.436Z","0.9.0":"2020-09-13T20:28:47.172Z","0.9.1":"2020-09-23T05:25:30.673Z","0.9.2":"2020-09-27T04:22:50.842Z","0.9.3":"2020-09-29T02:04:43.974Z","0.9.4":"2020-09-29T04:45:55.654Z","0.9.5":"2020-09-29T05:01:28.476Z","0.9.6":"2020-10-06T03:37:42.789Z","0.9.7":"2020-10-07T03:04:09.099Z","0.9.8":"2020-10-11T00:54:43.032Z","0.9.9":"2020-10-26T02:21:49.493Z","0.9.10":"2020-11-15T06:08:17.503Z","0.9.11":"2020-11-15T18:46:53.920Z","0.10.0":"2020-12-22T07:48:39.382Z","0.12.0":"2021-01-04T06:12:50.768Z","1.0.0-0":"2021-03-21T05:24:38.279Z","1.0.0-1":"2021-03-22T02:23:02.995Z","1.0.0-2":"2021-03-22T02:44:06.973Z","1.0.0-3":"2021-03-22T02:53:25.946Z","1.0.0-4":"2021-03-22T03:27:22.634Z","1.0.0-5":"2021-03-27T01:32:29.710Z","1.0.0-6":"2021-03-27T04:18:25.148Z","1.0.0-7":"2021-03-29T03:22:32.075Z","1.0.0-8":"2021-03-30T00:57:45.120Z","1.0.0-9":"2021-03-30T04:22:12.856Z","1.0.0-10":"2021-03-30T05:43:01.675Z","1.0.0-11":"2021-03-31T04:10:38.016Z","1.0.0-12":"2021-04-02T03:30:24.854Z","1.0.0":"2021-04-04T05:36:18.682Z","2.0.0-0":"2021-04-05T04:52:09.092Z","2.0.0-1":"2021-04-06T03:00:44.402Z","2.0.0-2":"2021-04-06T03:59:24.870Z","2.0.0-3":"2021-04-07T04:53:11.819Z","2.0.0-4":"2021-04-10T23:56:43.169Z","2.0.0-5":"2021-04-11T01:10:02.569Z","2.0.0-6":"2021-04-11T18:12:20.593Z","2.0.0-7":"2021-04-11T20:16:29.557Z","2.0.0-8":"2021-04-11T20:26:37.612Z","2.0.0-9":"2021-04-11T20:43:11.376Z","2.0.0-10":"2021-04-11T21:07:26.560Z","2.0.0-11":"2021-04-11T23:33:50.932Z","2.0.0-12":"2021-04-13T00:26:27.938Z","2.0.0-13":"2021-04-13T00:52:55.851Z","2.0.0-15":"2021-04-13T01:44:53.438Z","2.0.0-16":"2021-04-13T05:05:20.243Z","2.0.0-17":"2021-04-13T23:19:05.477Z","2.0.0-18":"2021-04-14T04:42:45.863Z","2.0.0-19":"2021-04-15T23:46:03.738Z","2.0.0-20":"2021-04-16T03:16:52.951Z","2.0.0":"2021-04-16T23:28:20.259Z","2.1.0":"2021-04-21T04:44:18.606Z","2.1.1-0":"2021-04-26T05:17:13.046Z","2.1.1-1":"2021-04-27T02:28:22.751Z","2.1.1-2":"2021-04-27T03:18:54.430Z","2.1.1-3":"2021-04-27T03:53:45.482Z","2.1.1-4":"2021-04-27T04:09:22.872Z","2.1.1-5":"2021-04-27T04:16:51.966Z","2.1.1-6":"2021-04-30T03:51:58.792Z","2.3.1-alpha.0":"2021-04-30T05:03:05.488Z","2.3.1-alpha.1":"2021-05-01T03:03:08.904Z","2.3.1-alpha.2":"2021-05-01T03:15:24.628Z","2.3.1-alpha.3":"2021-05-01T17:20:56.999Z","2.3.1-alpha.4":"2021-05-01T20:07:41.983Z","2.3.1-alpha.5":"2021-05-02T04:35:44.000Z","2.3.1-alpha.6":"2021-05-02T18:12:57.997Z","2.3.1-alpha.7":"2021-05-06T02:54:33.957Z","2.3.1":"2021-05-17T03:40:18.726Z","2.3.2":"2021-05-26T04:08:38.314Z","2.4.0":"2021-06-08T19:20:22.124Z","2.5.0-alpha.0":"2021-06-15T00:50:20.352Z","2.5.0-alpha.1":"2021-06-15T21:51:34.844Z","2.5.0-alpha.2":"2021-06-16T03:03:30.016Z","2.5.0-alpha.3":"2021-06-17T04:01:00.164Z","2.5.0-alpha.4":"2021-06-17T04:25:41.127Z","2.5.0-alpha.5":"2021-06-20T02:57:32.039Z","2.5.0-alpha.7":"2021-06-21T01:16:55.377Z","2.5.0-alpha.10":"2021-06-29T20:21:19.557Z","2.5.0-alpha.11":"2021-06-29T21:25:20.301Z","2.5.0-alpha.12":"2021-07-13T18:25:47.340Z","2.5.0-alpha.13":"2021-07-19T22:38:03.751Z","2.5.0-alpha.14":"2021-07-25T22:55:53.871Z","2.5.0-alpha.15":"2021-07-25T23:15:32.066Z","2.5.0-alpha.16":"2021-07-25T23:25:14.786Z","2.5.0-alpha.17":"2021-07-26T00:13:52.578Z","2.5.0-alpha.18":"2021-07-26T03:47:32.946Z","2.5.0-alpha.19":"2021-07-26T03:54:52.466Z","2.5.0-alpha.20":"2021-09-07T04:44:46.514Z","2.5.0-alpha.21":"2021-09-07T15:23:15.662Z","2.5.0-alpha.22":"2021-09-07T17:26:38.271Z","2.5.0-alpha.23":"2021-09-07T17:34:53.601Z","2.5.0-alpha.24":"2021-09-07T17:58:16.608Z","2.5.0-alpha.55":"2021-11-15T18:17:47.108Z","2.5.0-alpha.56":"2021-11-17T17:25:37.696Z","2.5.0-alpha.57":"2021-11-17T17:32:57.512Z","2.5.0-alpha.58":"2021-11-17T17:40:14.685Z","2.5.0-alpha.59":"2021-12-02T16:09:15.397Z","2.5.0-alpha.60":"2021-12-02T16:29:10.397Z","2.5.0-alpha.63":"2022-02-07T21:34:06.710Z","2.5.0-alpha.65":"2022-04-06T05:08:30.239Z","2.5.0-alpha.66":"2022-04-06T05:12:27.809Z","2.5.0-alpha.67":"2022-04-06T05:21:15.354Z","2.5.0-alpha.69":"2022-04-06T17:22:28.583Z","2.5.0-alpha.70":"2022-04-06T17:24:52.544Z","2.5.0-alpha.71":"2022-05-03T19:24:22.201Z","2.5.0-alpha.72":"2022-05-17T16:16:49.495Z","2.5.0-alpha.75":"2022-05-23T17:27:34.037Z","2.5.0-alpha.77":"2022-08-17T17:41:13.062Z","2.5.0-alpha.78":"2022-08-17T17:47:28.727Z","2.5.0-alpha.79":"2022-09-22T21:24:29.210Z","2.5.0-alpha.80":"2022-09-22T21:37:49.174Z","2.5.0-alpha.81":"2022-09-27T00:55:34.659Z","2.5.0-alpha.82":"2023-01-11T16:18:13.053Z","2.5.0-alpha.83":"2023-01-12T17:21:39.592Z"},"maintainers":[{"name":"synvox","email":"ryan@allred.xyz"}],"author":{"name":"Ryan Allred"},"license":"MIT","readme":"","readmeFilename":"","description":"Core is a middleware for `express` that creates restful endpoints automatically. It uses `knex` to connect to `postgres` and `yup` for validations.","homepage":"https://github.com/synvox/core#readme","repository":{"type":"git","url":"git+https://github.com/synvox/core.git","directory":"core"},"bugs":{"url":"https://github.com/synvox/core/issues"}}