1
0
mirror of synced 2026-02-06 18:00:14 -05:00

Compare commits

...

74 Commits
bug ... b1

Author SHA1 Message Date
Brandon Bayer
e5ead166ac fixes 2020-10-06 11:54:14 -04:00
Brandon Bayer
2b07a8b119 add _ between session prefix and cookie name 2020-10-06 11:34:17 -04:00
Brandon Bayer
33c7bec41f v0.24.0-canary.3 2020-10-06 11:23:48 -04:00
Stratulat Alexandru
3d827c8506 Prefix session cookies with app name (#1229)
(minor)
2020-10-06 11:21:46 -04:00
allcontributors[bot]
b04c6d7469 docs: add Dohxis as a contributor (#1261)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-10-06 11:18:54 -04:00
Domantas Mauruča
242e2eadee Add tests for add-dependency-executor (#1259)
(meta)
2020-10-06 11:17:02 -04:00
Brandon Bayer
08303d337b Add back RouterContext and BlitzRouter which got lost in last canary release (#1260)
(patch)
2020-10-06 11:13:46 -04:00
Brandon Bayer
be861c7919 v0.24.0-canary.2 2020-10-05 19:13:19 -04:00
allcontributors[bot]
092045e807 docs: add Alucard17 as a contributor (#1256)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-10-05 18:59:41 -04:00
Alucard17
1e0f17c93a Changed @blitzjs/core to only export public API (#1246)
Co-authored-by: Brandon Bayer <b@bayer.ws> (meta)
2020-10-05 18:58:57 -04:00
Jamie Davenport
310079b3bc Fix Safari compatibility: shim requestIdleCallback (#1247)
(patch)
2020-10-05 18:46:06 -04:00
Brandon Bayer
a81252aeb4 Fix regression on canary of loss of Prisma error code (#1254)
(patch)
2020-10-05 18:15:02 -04:00
allcontributors[bot]
cc58c72d94 docs: add konradkalemba as a contributor (#1255)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-10-05 18:14:09 -04:00
Konrad Kalemba
2162e1c6b4 Fix DocumentContext import in Material UI recipe (#1253)
(recipe)
2020-10-05 18:13:12 -04:00
अभिनाश (Avinash)
d46d860338 (newapp) Fix docs link on default homepage (#1250) 2020-10-05 18:10:55 -04:00
Satoshi Nitawaki
c65360de09 Fix name of blitz db seed command (was blitz seed) (#1239)
(patch)
2020-10-05 17:57:08 -04:00
Kotaro Chikuba
9873fc22de Fix isBlitzRoot() for blank deps/devDeps (#1242)
(patch)
2020-10-05 17:55:31 -04:00
allcontributors[bot]
e6954fbca8 docs: add mizchi as a contributor (#1252)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-10-05 10:03:03 -04:00
Kotaro Chikuba
048ba5f5cb Fix middleware test for win and mac (#1245)
(meta)
2020-10-05 10:01:14 -04:00
Brandon Bayer
bd1063a965 Upgrade prisma version in examples apps (#1238)
(meta)
2020-10-03 12:34:11 -04:00
Brandon Bayer
ae6b22f4f0 v0.24.0-canary.1 2020-10-02 21:57:55 -04:00
Brandon Bayer
f45d2d5ee3 Change log color of query/mutation input args to yellow (#1237)
(minor)
2020-10-02 21:56:02 -04:00
Brandon Bayer
7bc8a249b4 Make ctx.session.authorize() a type guard (#1222)
(minor)
2020-10-02 20:55:51 -04:00
Brandon Bayer
742ff71a97 Add ability to strongly type PublicData! (#1219)
Co-authored-by: Piotr Monwid-Olechnowicz <hasparus@gmail.com> (major)
2020-10-02 20:35:39 -04:00
Brandon Bayer
9291ae3b38 Upgrade monorepo typescript from 3.8 to 4.0 (#1236)
(meta)
2020-10-02 19:22:48 -04:00
allcontributors[bot]
5a5656078b docs: add hasparus as a contributor (#1235)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>(meta)
2020-10-02 18:35:05 -04:00
Brandon Bayer
ea815e83fa Upgrade superjson to 1.2.3 (#1230)
(patch)
2020-10-02 17:19:59 -04:00
Weilbyte
869c00c950 Fix blitz new --js #1208 (#1211)
(patch)
2020-10-02 16:58:24 -04:00
Brandon Bayer
a670693e9d v0.24.0-canary.0 2020-10-02 16:29:22 -04:00
allcontributors[bot]
5de91ad57b docs: add phillippschmedt as a contributor (#1228)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-10-02 16:21:56 -04:00
Phillipp Schmedt
31899458de Enforce NodeJS Version >= 12 for CLI (#1213)
(patch)
2020-10-02 16:21:40 -04:00
Jamie Davenport
dce462ba53 Improve error message when attempting useQuery with regular functions (#1223)
(patch)
2020-10-02 16:20:15 -04:00
Brandon Bayer
5ebed4b05d Update @engelkes-finstreet as a contributor 2020-10-02 16:17:48 -04:00
Brandon Bayer
13353793af Update @engelkes-finstreet as a contributor 2020-10-02 16:17:27 -04:00
Cody G
3583a59aa8 Refactor and add tests for public data store (#1204)
(meta)
2020-10-02 16:14:35 -04:00
Brandon Bayer
1c5aee7c67 Add invalidateQuery utility (#1226)
(minor)
2020-10-02 16:09:34 -04:00
Justin Hall
c87883dbe8 Fix logout mutation usage in generated app index page (#1201)
(newapp)
2020-10-02 16:09:22 -04:00
allcontributors[bot]
7d84561690 docs: add hmajid2301 as a contributor (#1227)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-10-02 16:08:41 -04:00
Haseeb Majid
3f43ffd4fe Don't run prettier is blitz new fails (#1202)
Co-authored-by: Haseeb Majid <haseeb.majid@sky.uk> (patch)
2020-10-02 16:08:26 -04:00
Bruno Crosier
1ac2092129 Change all generated file paths to be kebab-case (#1197)
(minor)
2020-10-02 16:06:57 -04:00
Brandon Bayer
579807ff20 Add getQueryKey utility (#1224)
* done

* fix the bug

* fix infinite query key to be url first like everything else (minor)
2020-10-02 15:50:22 -04:00
Brandon Bayer
566e8be3c3 Fix query cache not being invalidated on route navigation (#1225)
(patch)
2020-10-02 15:10:20 -04:00
Brandon Bayer
1b9eb77964 Rename ssrQuery to invokeWithMiddleware (#1218)
(major)
2020-10-02 12:23:32 -04:00
Brandon Bayer
9f24ba10b2 Add invoke() — the new way to imperatively call queries/mutations (#1217)
(minor)
2020-10-02 11:41:23 -04:00
Brandon Bayer
f5237c31c4 Lots of logging & error improvements!! Remove result logs, redact passwords, etc (#1212)
(minor)
2020-10-02 11:12:32 -04:00
Brandon Bayer
3ddb3870b9 Fix session race condition that would result in CSRF errors or users being logged out (#1209)
(patch)
2020-10-01 21:03:06 -04:00
Brandon Bayer
763252a5ed Fix husky pre-push script for new apps (#1207)
(patch)
2020-10-01 14:29:49 -04:00
Brandon Bayer
6a37f32322 Update @brunocrosier as a contributor 2020-10-01 14:27:35 -04:00
John Cantrell
48e27be1a7 Fix bug with creating new session after revoking current one (#1200)
(patch)
2020-09-30 18:20:11 -04:00
Brandon Bayer
90df4e8409 v0.23.2-canary.3 2020-09-30 11:50:04 -04:00
Brandon Bayer
3b46d96ec8 Minor updates to pages/queries/mutations templates (add useMutation and use db.findFirst) (#1195)
(minor)
2020-09-30 11:47:58 -04:00
Brandon Bayer
13c5a9b802 Fix a resolver type build error (on canary) (#1194)
(patch)
2020-09-30 11:40:34 -04:00
allcontributors[bot]
e6ddebadf5 docs: add ntgussoni as a contributor (#1189)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-29 22:13:48 -04:00
allcontributors[bot]
1bb4cf33ff docs: add clgeoio as a contributor (#1190)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-29 22:12:56 -04:00
Cody G
36dfbe42f5 Adds tests for core parsePublicDataToken (#1184)
(meta)
2020-09-29 22:09:07 -04:00
Satoshi Nitawaki
6c06f0b62c (newapp) Fix tests not working out of the box (#1165) 2020-09-29 21:49:16 -04:00
Stratulat Alexandru
a83536be21 Reduce re-renders by memoizing useParams and useRouterQuery (#1157)
(patch)
2020-09-29 09:07:51 -04:00
Brandon Bayer
07f9e26827 v0.23.2-canary.2 2020-09-28 18:58:29 -04:00
Brandon Bayer
c5e6221ebb Fix types for useQuery that were broken with useMutation addition (#1181)
(patch)
2020-09-28 18:48:12 -04:00
Brandon Bayer
2b0fe98cf5 v0.23.2-canary.1 2020-09-28 17:32:23 -04:00
allcontributors[bot]
58386ffe2c docs: add lukebennett as a contributor (#1180)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-28 17:25:16 -04:00
Luke Bennett
23fc27027a Use npm for husky pre-push hook instead of yarn (#1179)
(newapp)
2020-09-28 17:24:28 -04:00
Brandon Bayer
e4c00094e5 Disable automatic query request cancellation (#1177)
(patch)
2020-09-28 17:14:53 -04:00
Brandon Bayer
a357fd0445 Fix useQuery's mutate and router.push in same frame will cause error (#1176)
* fix mutate

* fix test

* add comment (patch)
2020-09-28 16:54:27 -04:00
Brandon Bayer
c43967984b Add @Kamshak as a contributor 2020-09-28 11:28:52 -04:00
Rudi Yardley
e47d947dc0 Fix bug in dev where files were not being removed correctly when deleted (#1161)
(patch)
2020-09-28 11:06:05 -04:00
Satoshi Nitawaki
ffb54ec064 Add .node-version (#1148)
(meta)
2020-09-28 11:04:46 -04:00
Brandon Bayer
08abc33494 v0.23.2-canary.0 2020-09-26 22:21:14 -04:00
Brandon Bayer
34722f952c 🔥 Add useMutation() and strong typing for ctx and ctx.session (#1160)
(major)
2020-09-26 22:12:29 -04:00
Brandon Bayer
4003b8ac01 Big refactor of internal types for react-query, RPC, and resolvers (#1172)
(meta)
2020-09-26 21:19:19 -04:00
allcontributors[bot]
160b5fc062 docs: add brunocrosier as a contributor (#1159)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-24 20:29:01 -04:00
Bruno Crosier
b722c39f79 (newapp) Add "restart server" to instruction to index page (#1140)
Co-authored-by: Brandon Bayer <b@bayer.ws>
2020-09-24 20:28:14 -04:00
allcontributors[bot]
8da7bd7cd4 docs: add jorisre as a contributor (#1158)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Co-authored-by: Brandon Bayer <b@bayer.ws> (meta)
2020-09-24 20:26:00 -04:00
Joris
712cb172eb Fix: pass undefined through the RPC layer (#1156) 2020-09-24 20:24:57 -04:00
140 changed files with 2269 additions and 1570 deletions

View File

@@ -353,7 +353,8 @@
"profile": "https://github.com/ntgussoni",
"contributions": [
"test",
"code"
"code",
"review"
]
},
{
@@ -971,7 +972,8 @@
"avatar_url": "https://avatars2.githubusercontent.com/u/37571416?v=4",
"profile": "https://github.com/clgeoio",
"contributions": [
"code"
"code",
"test"
]
},
{
@@ -1009,8 +1011,8 @@
"avatar_url": "https://avatars1.githubusercontent.com/u/36962022?v=4",
"profile": "https://github.com/engelkes-finstreet",
"contributions": [
"code",
"doc"
"doc",
"code"
]
},
{
@@ -1168,6 +1170,98 @@
"contributions": [
"code"
]
},
{
"login": "jorisre",
"name": "Joris",
"avatar_url": "https://avatars1.githubusercontent.com/u/7545547?v=4",
"profile": "https://github.com/jorisre",
"contributions": [
"code"
]
},
{
"login": "Kamshak",
"name": "Valentin Funk",
"avatar_url": "https://avatars3.githubusercontent.com/u/337968?v=4",
"profile": "https://github.com/Kamshak",
"contributions": [
"doc"
]
},
{
"login": "lukebennett",
"name": "Luke Bennett",
"avatar_url": "https://avatars1.githubusercontent.com/u/135390?v=4",
"profile": "https://lukebennett.com",
"contributions": [
"code"
]
},
{
"login": "hmajid2301",
"name": "Haseeb Majid",
"avatar_url": "https://avatars0.githubusercontent.com/u/998807?v=4",
"profile": "https://haseebmajid.dev",
"contributions": [
"code"
]
},
{
"login": "phillippschmedt",
"name": "Phillipp Schmedt",
"avatar_url": "https://avatars0.githubusercontent.com/u/16028406?v=4",
"profile": "https://github.com/phillippschmedt",
"contributions": [
"code"
]
},
{
"login": "hasparus",
"name": "Piotr Monwid-Olechnowicz",
"avatar_url": "https://avatars0.githubusercontent.com/u/15332326?v=4",
"profile": "https://haspar.us",
"contributions": [
"code"
]
},
{
"login": "mizchi",
"name": "Kotaro Chikuba",
"avatar_url": "https://avatars2.githubusercontent.com/u/73962?v=4",
"profile": "https://mizchi.dev",
"contributions": [
"code",
"test"
]
},
{
"login": "konradkalemba",
"name": "Konrad Kalemba",
"avatar_url": "https://avatars0.githubusercontent.com/u/8682104?v=4",
"profile": "https://github.com/konradkalemba",
"contributions": [
"code"
]
},
{
"login": "Alucard17",
"name": "Alucard17",
"avatar_url": "https://avatars1.githubusercontent.com/u/26205172?v=4",
"profile": "https://github.com/Alucard17",
"contributions": [
"code"
]
},
{
"login": "Dohxis",
"name": "Domantas Mauruča",
"avatar_url": "https://avatars2.githubusercontent.com/u/8768909?v=4",
"profile": "https://github.com/Dohxis",
"contributions": [
"test",
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -24,7 +24,12 @@ module.exports = {
},
],
"@typescript-eslint/no-floating-promises": "error",
"no-use-before-define": ["error", {functions: false, classes: false}],
// note you must disable the base rule as it can report incorrect errors
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["error"],
// note you must disable the base rule as it can report incorrect errors
"no-redeclare": "off",
"@typescript-eslint/no-redeclare": ["error"],
},
ignorePatterns: ["packages/cli/", "packages/generator/templates", ".eslintrc.js"],
overrides: [

1
.node-version Normal file
View File

@@ -0,0 +1 @@
12.16.1

View File

@@ -6,7 +6,7 @@
<img alt="" src="https://img.shields.io/badge/Join%20our%20community-6700EB.svg?style=for-the-badge&labelColor=000000&logoWidth=20&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ9SURBVHgB7d3dVdtAEIbhcSpICUoH0IEogQqSVBBSAU4FSSpIOoAORAfQgSghHXzZ1U/YcMD4R9rZmf2ec3y448LyiNf27iLiGIAmPLrweC9Un3DhrzG6EarLNP09nlwJ1SOZ/lQr5N80/S/p2QMVCBf5N17XCfm1Y/rBHqjAG9PPHvBsz+mf9WAP+HLA9M/YA14cOP2payH7jpj+VCtk1wnTP+vj7xCy6cTpn7EHLMLp059iD1iD8eveJbVCNsSLheX1YA/YgOWnf8YeKB3Wmf7Ud6Fy4f/FHmtpxbl3YlC4MJ/Cj0bWdwPnPbARg+L0S54XQHS32WwuxClzd4CM0z9rPfeAuTtA5ulPXYQ7wZ04Y+oOoDD9KZc9YOoOoDj9s4dwFzgXR6w1wIPoOvPWA9buAHEJ173o3gWiy3AnuBUHLEbgmYwvAk1/wuM8vAgexThzbwPDkx7/DHwVXfFOxP2GmsKd4Ab6zPeAyU8CI7AHFmH2BRCBPXAyk18GzUrqAXCTiR4ssyj0VFw/oCU8+e+RZ33AWz6KMaYbIIWxB+JSLs1bsbkeMN0AqakHvoku9oA2sAfqBvbAQdw0QArsgb25aYBUQT3QgT2gB+yBuqGcHij2UCqXDZACe2Anlw2QYg/QAOyBuoE98CL3DZDCuK4/rh/Q7oGL6U+TOvcNkJoijN8X1C48+T+g75eQDrAH/qmqAVJgDwyqaoAUe4AGYA/UDZX3QLUNkEIZPRCd5+6BahsgVUgPROwBTSijB7jpVAvGHriHvmw9wAZ4BpX1ABvgmakHtPcbRuwBTWAPULgAV9D/jKDY9YRvwvgEaurD44uQHvAol7qBW7WKluVtIHiUS7GyvA0s6CiXDnxrpQfsgbqBS7GKk/2jYHCrVlGyfxTMrVo0ALdq1Q3sgSKofh0M9oA61a+D2QM0AHugbmAPqClmSRjK2apVVQ8UsySsoK1aHdgDesCtWnUDeyCrIpeFg1u3sylyWTi3btMA7IG6gT2wuuK3hoE9sKrit4YVslWLPaAN7IG6ocKt2zmY2h4O9sDiTG0PZw/QANy6XTewBxZj9ogYVHy025LMHhEz9cBn0We6B0yfERReBLfhx0/R1YQHPx/QBPbA0VwcEwf2wNFcHBPHHjiem3MC2QPHcXdSaJjA+KfgTPQ8hhfjBzHC40mhlzJ+Xq9lK4a4PCs43AVaGTed5mZq+iOXZwWHi3AnOj2wFWNcnxYe7gTxLtBKHuamP/J+Wnh8a5irB7ZC5Yk9gPX1QuXC+usHWqGyhYvUYR0a7zboUOFCNVhnk0krZAOW7wFOvzXhom2xnEbIHizTA1wEYhWW6YFGyC6c1gOcfg9wfA80Qj7g8B7g9HuCww+haIR8wf49wOn3Cvv9k8tGyC/s7gFOv3fY3QONkH+v9MBWqB7PeqDn9FcIT//kcitUn6kHOu/T/xfWzlQy3dEHhwAAAABJRU5ErkJggg==">
</a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-122-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-132-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<a aria-label="License" href="https://github.com/blitz-js/blitz/blob/canary/LICENSE">
<img alt="" src="https://img.shields.io/npm/l/blitz.svg?style=for-the-badge&labelColor=000000&color=blue">
@@ -260,7 +260,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<td align="center"><a href="https://mikeattara.com"><img src="https://avatars1.githubusercontent.com/u/31483629?v=4" width="100px;" alt=""/><br /><sub><b>Mike Perry Y Attara</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mikeattara" title="Documentation">📖</a></td>
<td align="center"><a href="https://devanthe.dev"><img src="https://avatars0.githubusercontent.com/u/354652?v=4" width="100px;" alt=""/><br /><sub><b>Devan</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=DevanB" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jclancy93"><img src="https://avatars2.githubusercontent.com/u/7850202?v=4" width="100px;" alt=""/><br /><sub><b>Jack Clancy</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jclancy93" title="Code">💻</a> <a href="#maintenance-jclancy93" title="Maintenance">🚧</a></td>
<td align="center"><a href="https://github.com/ntgussoni"><img src="https://avatars0.githubusercontent.com/u/10161067?v=4" width="100px;" alt=""/><br /><sub><b>Nicolas Torres</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ntgussoni"><img src="https://avatars0.githubusercontent.com/u/10161067?v=4" width="100px;" alt=""/><br /><sub><b>Nicolas Torres</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Antgussoni" title="Reviewed Pull Requests">👀</a></td>
</tr>
<tr>
<td align="center"><a href="http://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Tests">⚠️</a> <a href="#maintenance-Skn0tt" title="Maintenance">🚧</a></td>
@@ -346,11 +346,11 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<tr>
<td align="center"><a href="https://github.com/jschepmans"><img src="https://avatars2.githubusercontent.com/u/5782977?v=4" width="100px;" alt=""/><br /><sub><b>Johan Schepmans</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jschepmans" title="Code">💻</a></td>
<td align="center"><a href="https://twitter.com/dillonraphael"><img src="https://avatars0.githubusercontent.com/u/3496193?v=4" width="100px;" alt=""/><br /><sub><b>Dillon Raphael</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dillonraphael" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/clgeoio"><img src="https://avatars2.githubusercontent.com/u/37571416?v=4" width="100px;" alt=""/><br /><sub><b>Cody G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/clgeoio"><img src="https://avatars2.githubusercontent.com/u/37571416?v=4" width="100px;" alt=""/><br /><sub><b>Cody G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/madflow"><img src="https://avatars0.githubusercontent.com/u/183248?v=4" width="100px;" alt=""/><br /><sub><b>madflow</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=madflow" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/nitaking_"><img src="https://avatars2.githubusercontent.com/u/10850034?v=4" width="100px;" alt=""/><br /><sub><b>Satoshi Nitawaki</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nitaking" title="Code">💻</a> <a href="#maintenance-nitaking" title="Maintenance">🚧</a> <a href="#question-nitaking" title="Answering Questions">💬</a></td>
<td align="center"><a href="https://github.com/sirmyron"><img src="https://avatars2.githubusercontent.com/u/1430136?v=4" width="100px;" alt=""/><br /><sub><b>sirmyron</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sirmyron" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/pixelscommander"><img src="https://avatars2.githubusercontent.com/u/810671?v=4" width="100px;" alt=""/><br /><sub><b>Denis Radin</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3APixelsCommander" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Documentation">📖</a></td>
@@ -374,11 +374,24 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<td align="center"><a href="http://enricoschaaf.com"><img src="https://avatars1.githubusercontent.com/u/54645197?v=4" width="100px;" alt=""/><br /><sub><b>Enrico Schaaf</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=enricoschaaf" title="Code">💻</a></td>
<td align="center"><a href="http://kitze.io"><img src="https://avatars0.githubusercontent.com/u/1160594?v=4" width="100px;" alt=""/><br /><sub><b>Kitze</b></sub></a><br /><a href="#ideas-kitze" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/drmas"><img src="https://avatars3.githubusercontent.com/u/644440?v=4" width="100px;" alt=""/><br /><sub><b>Mohamed Shaban</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=drmas" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jorisre"><img src="https://avatars1.githubusercontent.com/u/7545547?v=4" width="100px;" alt=""/><br /><sub><b>Joris</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jorisre" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Kamshak"><img src="https://avatars3.githubusercontent.com/u/337968?v=4" width="100px;" alt=""/><br /><sub><b>Valentin Funk</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Kamshak" title="Documentation">📖</a></td>
<td align="center"><a href="https://lukebennett.com"><img src="https://avatars1.githubusercontent.com/u/135390?v=4" width="100px;" alt=""/><br /><sub><b>Luke Bennett</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=lukebennett" title="Code">💻</a></td>
<td align="center"><a href="https://haseebmajid.dev"><img src="https://avatars0.githubusercontent.com/u/998807?v=4" width="100px;" alt=""/><br /><sub><b>Haseeb Majid</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hmajid2301" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/phillippschmedt"><img src="https://avatars0.githubusercontent.com/u/16028406?v=4" width="100px;" alt=""/><br /><sub><b>Phillipp Schmedt</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=phillippschmedt" title="Code">💻</a></td>
<td align="center"><a href="https://haspar.us"><img src="https://avatars0.githubusercontent.com/u/15332326?v=4" width="100px;" alt=""/><br /><sub><b>Piotr Monwid-Olechnowicz</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hasparus" title="Code">💻</a></td>
<td align="center"><a href="https://mizchi.dev"><img src="https://avatars2.githubusercontent.com/u/73962?v=4" width="100px;" alt=""/><br /><sub><b>Kotaro Chikuba</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mizchi" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=mizchi" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/konradkalemba"><img src="https://avatars0.githubusercontent.com/u/8682104?v=4" width="100px;" alt=""/><br /><sub><b>Konrad Kalemba</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=konradkalemba" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Alucard17"><img src="https://avatars1.githubusercontent.com/u/26205172?v=4" width="100px;" alt=""/><br /><sub><b>Alucard17</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Alucard17" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Dohxis"><img src="https://avatars2.githubusercontent.com/u/8768909?v=4" width="100px;" alt=""/><br /><sub><b>Domantas Mauruča</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Dohxis" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=Dohxis" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,5 +1,4 @@
import React from "react"
import {Link} from "blitz"
import {Link, useMutation} from "blitz"
import {LabeledTextField} from "app/components/LabeledTextField"
import {Form, FORM_ERROR} from "app/components/Form"
import login from "app/auth/mutations/login"
@@ -10,6 +9,7 @@ type LoginFormProps = {
}
export const LoginForm = (props: LoginFormProps) => {
const [loginMutation] = useMutation(login)
return (
<div>
<h1>Login</h1>
@@ -19,7 +19,7 @@ export const LoginForm = (props: LoginFormProps) => {
initialValues={{email: undefined, password: undefined}}
onSubmit={async (values) => {
try {
await login({email: values.email, password: values.password})
await loginMutation(values)
props.onSuccess && props.onSuccess()
} catch (error) {
if (error.name === "AuthenticationError") {

View File

@@ -1,15 +1,15 @@
import {SessionContext} from "blitz"
import {Ctx} from "blitz"
import {authenticateUser} from "app/auth/auth-utils"
import {LoginInput, LoginInputType} from "../validations"
export default async function login(input: LoginInputType, ctx: {session?: SessionContext} = {}) {
export default async function login(input: LoginInputType, {session}: Ctx) {
// This throws an error if input is invalid
const {email, password} = LoginInput.parse(input)
// This throws an error if credentials are invalid
const user = await authenticateUser(email, password)
await ctx.session!.create({userId: user.id, roles: [user.role]})
await session.create({userId: user.id, roles: [user.role]})
return user
}

View File

@@ -1,5 +1,5 @@
import {SessionContext} from "blitz"
import {Ctx} from "blitz"
export default async function logout(_ = null, ctx: {session?: SessionContext} = {}) {
return await ctx.session!.revoke()
export default async function logout(_: any, {session}: Ctx) {
return await session.revoke()
}

View File

@@ -1,9 +1,9 @@
import {Ctx} from "blitz"
import db from "db"
import {SessionContext} from "blitz"
import {hashPassword} from "app/auth/auth-utils"
import {SignupInput, SignupInputType} from "app/auth/validations"
export default async function signup(input: SignupInputType, ctx: {session?: SessionContext} = {}) {
export default async function signup(input: SignupInputType, {session}: Ctx) {
// This throws an error if input is invalid
const {email, password} = SignupInput.parse(input)
@@ -13,7 +13,7 @@ export default async function signup(input: SignupInputType, ctx: {session?: Ses
select: {id: true, name: true, email: true, role: true},
})
await ctx.session!.create({userId: user.id, roles: [user.role]})
await session.create({userId: user.id, roles: [user.role]})
return user
}

View File

@@ -1,4 +1,3 @@
import React from "react"
import {Head, useRouter, BlitzPage} from "blitz"
import {LoginForm} from "app/auth/components/LoginForm"

View File

@@ -1,5 +1,4 @@
import React from "react"
import {Head, useRouter, BlitzPage} from "blitz"
import {Head, useRouter, BlitzPage, useMutation} from "blitz"
import {Form, FORM_ERROR} from "app/components/Form"
import {LabeledTextField} from "app/components/LabeledTextField"
import signup from "app/auth/mutations/signup"
@@ -7,6 +6,7 @@ import {SignupInput} from "app/auth/validations"
const SignupPage: BlitzPage = () => {
const router = useRouter()
const [signupMutation] = useMutation(signup)
return (
<>
@@ -23,7 +23,7 @@ const SignupPage: BlitzPage = () => {
schema={SignupInput}
onSubmit={async (values) => {
try {
await signup({email: values.email, password: values.password})
await signupMutation(values)
router.push("/")
} catch (error) {
if (error.code === "P2002" && error.meta?.target?.includes("email")) {

View File

@@ -1,4 +1,4 @@
import React, {ReactNode, PropsWithoutRef} from "react"
import {ReactNode, PropsWithoutRef} from "react"
import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form"
import * as z from "zod"
export {FORM_ERROR} from "final-form"

View File

@@ -1,4 +1,4 @@
import React, {PropsWithoutRef} from "react"
import {forwardRef, PropsWithoutRef} from "react"
import {useField} from "react-final-form"
export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
@@ -11,7 +11,7 @@ export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElem
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
}
export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFieldProps>(
export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
({name, label, outerProps, ...props}, ref) => {
const {
input,

View File

@@ -1,16 +1,17 @@
import {useSession, useRouter} from "blitz"
import {useSession, useRouter, useMutation} from "blitz"
import logout from "app/auth/mutations/logout"
export default function Layout({children}: {children: React.ReactNode}) {
const session = useSession()
const router = useRouter()
const [logoutMutation] = useMutation(logout)
return (
<div>
{session.userId && (
<button
onClick={async () => {
router.push("/")
await logout()
await logoutMutation()
}}
>
Logout

View File

@@ -3,6 +3,10 @@ import {ErrorBoundary} from "react-error-boundary"
import {queryCache} from "react-query"
import LoginForm from "app/auth/components/LoginForm"
if (typeof window !== "undefined") {
window["DEBUG_BLITZ"] = 1
}
export default function App({Component, pageProps}: AppProps) {
const router = useRouter()
return (

View File

@@ -1,9 +1,10 @@
import {Suspense} from "react"
import {Head, Link, useSession, useRouterQuery} from "blitz"
import {Head, Link, useSession, useRouterQuery, useMutation, invoke} from "blitz"
import getUser from "app/users/queries/getUser"
import trackView from "app/users/mutations/trackView"
import Layout from "app/layouts/Layout"
import {useCurrentUser} from "app/hooks/useCurrentUser"
// import getUsers from "app/users/queries/getUsers"
const CurrentUserInfo = () => {
const currentUser = useCurrentUser()
@@ -11,12 +12,21 @@ const CurrentUserInfo = () => {
return <pre>{JSON.stringify(currentUser, null, 2)}</pre>
}
// const Users = () => {
// const [users] = useQuery(getUsers, {})
//
// return <pre style={{maxWidth: "30rem"}}>{JSON.stringify(users, null, 2)}</pre>
// }
const UserStuff = () => {
const session = useSession()
const query = useRouterQuery()
const [trackViewMutation] = useMutation(trackView)
if (session.isLoading) return <div>Loading...</div>
console.log(session.views)
return (
<div>
{!session.userId && (
@@ -40,10 +50,15 @@ const UserStuff = () => {
<Suspense fallback="Loading...">
<CurrentUserInfo />
</Suspense>
{/*
<Suspense fallback="Loading...">
<Users />
</Suspense>
*/}
<button
onClick={async () => {
try {
const user = await getUser({where: {id: session.userId as number}})
const user = await invoke(getUser, {where: {id: session.userId as number}})
alert(JSON.stringify(user))
} catch (error) {
alert("error: " + JSON.stringify(error))
@@ -55,7 +70,7 @@ const UserStuff = () => {
<button
onClick={async () => {
try {
await trackView()
await trackViewMutation()
} catch (error) {
alert("error: " + error)
console.log(error)

View File

@@ -1,11 +1,12 @@
import * as React from "react"
import {FC} from "react"
import {getSessionContext} from "@blitzjs/server"
import {
ssrQuery,
invokeWithMiddleware,
useRouter,
GetServerSideProps,
PromiseReturnType,
ErrorComponent as ErrorPage,
useMutation,
} from "blitz"
import getUser from "app/users/queries/getUser"
import logout from "app/auth/mutations/logout"
@@ -30,9 +31,9 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, re
const session = await getSessionContext(req, res)
console.log("Session id:", session.userId)
try {
const user = await ssrQuery(
const user = await invokeWithMiddleware(
getUser,
{where: {id: Number(session.userId)}, select: {id: true}},
{where: {id: Number(session.userId)}},
{res, req},
)
return {props: {user}}
@@ -42,8 +43,7 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, re
res.end()
return {props: {}}
} else if (error.name === "AuthenticationError") {
res.writeHead(302, {location: "/login"})
res.end()
res.writeHead(302, {location: "/login"}).end()
return {props: {}}
} else if (error.name === "AuthorizationError") {
return {
@@ -60,8 +60,9 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, re
}
}
const Test: React.FC<PageProps> = ({user, error}: PageProps) => {
const Test: FC<PageProps> = ({user, error}: PageProps) => {
const router = useRouter()
const [logoutMutation] = useMutation(logout)
if (error) {
return <ErrorPage statusCode={error.statusCode} title={error.message} />
@@ -72,7 +73,7 @@ const Test: React.FC<PageProps> = ({user, error}: PageProps) => {
<div>Logged in user id: {user?.id}</div>
<button
onClick={async () => {
await logout()
await logoutMutation()
router.push("/")
}}
>

View File

@@ -1,23 +0,0 @@
import React from "react"
type UserFormProps = {
initialValues: any
onSubmit: React.FormEventHandler<HTMLFormElement>
}
const UserForm = ({initialValues, onSubmit}: UserFormProps) => {
return (
<form
onSubmit={(event) => {
event.preventDefault()
onSubmit(event)
}}
>
<div>Put your form fields here. But for now, just click submit</div>
<div>{JSON.stringify(initialValues)}</div>
<button>Submit</button>
</form>
)
}
export default UserForm

View File

@@ -1,10 +0,0 @@
import db, {UserCreateArgs} from "db"
type CreateUserInput = {
data: UserCreateArgs["data"]
}
export default async function createUser({data}: CreateUserInput, ctx: Record<any, any> = {}) {
const user = await db.user.create({data})
return user
}

View File

@@ -1,11 +0,0 @@
import db, {UserDeleteArgs} from "db"
type DeleteUserInput = {
where: UserDeleteArgs["where"]
}
export default async function deleteUser({where}: DeleteUserInput, ctx: Record<any, any> = {}) {
const user = await db.user.delete({where})
return user
}

View File

@@ -1,9 +1,9 @@
import {SessionContext} from "blitz"
import {Ctx} from "blitz"
export default async function trackView(_ = null, ctx: {session?: SessionContext} = {}) {
const currentViews = ctx.session!.publicData.views || 0
await ctx.session!.setPublicData({views: currentViews + 1})
await ctx.session!.setPrivateData({views: currentViews + 1})
export default async function trackView(_ = null, {session}: Ctx) {
const currentViews = session.publicData.views || 0
await session.setPublicData({views: currentViews + 1})
await session.setPrivateData({views: currentViews + 1})
return
}

View File

@@ -1,15 +0,0 @@
import db, {UserUpdateArgs} from "db"
type UpdateUserInput = {
where: UserUpdateArgs["where"]
data: UserUpdateArgs["data"]
}
export default async function updateUser(
{where, data}: UpdateUserInput,
ctx: Record<any, any> = {},
) {
const user = await db.user.update({where, data})
return user
}

View File

@@ -1,62 +0,0 @@
import React, {Suspense} from "react"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import getUser from "app/users/queries/getUser"
import deleteUser from "app/users/mutations/deleteUser"
export const User = () => {
const router = useRouter()
const userId = useParam("userId", "number")
const [user] = useQuery(getUser, {where: {id: userId}})
return (
<div>
<h1>User {user.id}</h1>
<pre>{JSON.stringify(user, null, 2)}</pre>
{
<Link href="/users/[userId]/edit" as={`/users/${user.id}/edit`}>
<a>Edit</a>
</Link>
}
<button
type="button"
onClick={async () => {
if (window.confirm("This will be deleted")) {
await deleteUser({where: {id: user.id}})
router.push("/users")
}
}}
>
Delete
</button>
</div>
)
}
const ShowUserPage: BlitzPage = () => {
return (
<div>
<Head>
<title>User</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<p>
{
<Link href="/users">
<a>Users</a>
</Link>
}
</p>
<Suspense fallback={<div>Loading...</div>}>
<User />
</Suspense>
</main>
</div>
)
}
export default ShowUserPage

View File

@@ -1,63 +0,0 @@
import React, {Suspense} from "react"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import getUser from "app/users/queries/getUser"
import updateUser from "app/users/mutations/updateUser"
import UserForm from "app/users/components/UserForm"
export const EditUser = () => {
const router = useRouter()
const userId = useParam("userId", "number")
const [user, {mutate}] = useQuery(getUser, {where: {id: userId}})
return (
<div>
<h1>Edit User {user.id}</h1>
<pre>{JSON.stringify(user)}</pre>
<UserForm
initialValues={user}
onSubmit={async () => {
try {
const updated = await updateUser({
where: {id: user.id},
data: {name: "MyNewName"},
})
mutate(updated)
alert("Success!" + JSON.stringify(updated))
router.push("/users/[userId]", `/users/${updated.id}`)
} catch (error) {
console.log(error)
alert("Error creating user " + JSON.stringify(error, null, 2))
}
}}
/>
</div>
)
}
const EditUserPage: BlitzPage = () => {
return (
<div>
<Head>
<title>Edit User</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<Suspense fallback={<div>Loading...</div>}>
<EditUser />
</Suspense>
<p>
{
<Link href="/users">
<a>Users</a>
</Link>
}
</p>
</main>
</div>
)
}
export default EditUserPage

View File

@@ -1,49 +0,0 @@
import React, {Suspense} from "react"
import {Head, Link, useQuery, BlitzPage} from "blitz"
import getUsers from "app/users/queries/getUsers"
import Layout from "app/layouts/Layout"
export const UsersList = () => {
const [users] = useQuery(getUsers, {orderBy: {id: "desc"}})
return (
<ul>
{users?.map((user) => (
<li key={user.id}>
<Link href="/users/[userId]" as={`/users/${user.id}`}>
<a>{user.email}</a>
</Link>
</li>
))}
</ul>
)
}
const UsersPage: BlitzPage = () => {
return (
<Layout>
<Head>
<title>Users</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1>Users</h1>
<p>
{
<Link href="/users/new">
<a>Create User</a>
</Link>
}
</p>
<Suspense fallback={<div>Loading...</div>}>
<UsersList />
</Suspense>
</main>
</Layout>
)
}
export default UsersPage

View File

@@ -1,44 +0,0 @@
import React from "react"
import {Head, Link, useRouter, BlitzPage} from "blitz"
import createUser from "app/users/mutations/createUser"
import UserForm from "app/users/components/UserForm"
const NewUserPage: BlitzPage = () => {
const router = useRouter()
return (
<div>
<Head>
<title>New User</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1>Create New User </h1>
<UserForm
initialValues={{}}
onSubmit={async () => {
try {
const user = await createUser({data: {name: "MyName"}})
alert("Success!" + JSON.stringify(user))
router.push("/users/[userId]", `/users/${user.id}`)
} catch (error) {
alert("Error creating user " + JSON.stringify(error, null, 2))
}
}}
/>
<p>
{
<Link href="/users">
<a>Users</a>
</Link>
}
</p>
</main>
</div>
)
}
export default NewUserPage

View File

@@ -1,11 +1,11 @@
import {Ctx} from "blitz"
import db from "db"
import {SessionContext} from "blitz"
export default async function getCurrentUser(_ = null, ctx: {session?: SessionContext} = {}) {
if (!ctx.session?.userId) return null
export default async function getCurrentUser(_ = null, ctx: Ctx) {
if (!ctx.session.userId) return null
const user = await db.user.findOne({
where: {id: ctx.session!.userId},
where: {id: ctx.session.userId},
select: {id: true, name: true, email: true, role: true},
})

View File

@@ -1,22 +1,19 @@
import {Ctx, NotFoundError} from "blitz"
import db, {FindOneUserArgs} from "db"
import {SessionContext, NotFoundError} from "blitz"
type GetUserInput = {
where: FindOneUserArgs["where"]
select?: FindOneUserArgs["select"]
// Only available if a model relationship exists
// include?: FindOneUserArgs['include']
}
export default async function getUser(
{where, select}: GetUserInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session?.authorize(["admin", "user"])
export default async function getUser({where}: GetUserInput, ctx: Ctx) {
ctx.session.authorize()
console.log(ctx.session.userId)
const user = await db.user.findOne({where, select})
const user = await db.user.findOne({where})
if (!user) throw new NotFoundError(`User with id ${where.id} does not exist`)
return user
const {hashedPassword, ...rest} = user
return rest
}

View File

@@ -1,5 +1,5 @@
import {Ctx} from "blitz"
import db, {FindManyUserArgs} from "db"
import {SessionContext} from "blitz"
type GetUsersInput = {
where?: FindManyUserArgs["where"]
@@ -7,18 +7,17 @@ type GetUsersInput = {
cursor?: FindManyUserArgs["cursor"]
take?: FindManyUserArgs["take"]
skip?: FindManyUserArgs["skip"]
// Only available if a model relationship exists
// include?: FindManyUserArgs['include']
}
export default async function getUsers(
{where, orderBy, cursor, take, skip}: GetUsersInput,
ctx: {session?: SessionContext} = {},
ctx: Ctx,
) {
ctx.session!.authorize(["admin"])
ctx.session.authorize(["admin", "user"])
const users = await db.user.findMany({
where,
select: {id: true},
orderBy,
cursor,
take,

View File

@@ -7,7 +7,7 @@ module.exports = withBundleAnalyzer({
middleware: [
sessionMiddleware({
unstable_isAuthorized: unstable_simpleRolesIsAuthorized,
// sessionExpiryMinutes: 1,
sessionExpiryMinutes: 4,
}),
],
/*

View File

@@ -15,6 +15,7 @@
/**
* @type {Cypress.PluginConfig}
*/
//@ts-ignore
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/auth",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"scripts": {
"start": "blitz start",
"studio": "blitz db studio",
@@ -15,6 +15,9 @@
"browserslist": [
"defaults"
],
"prisma": {
"schema": "db/schema.prisma"
},
"prettier": {
"semi": false,
"printWidth": 100,
@@ -33,9 +36,9 @@
]
},
"dependencies": {
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"@prisma/cli": "2.8.0",
"@prisma/client": "2.8.0",
"blitz": "0.24.0-canary.3",
"final-form": "4.20.1",
"passport-auth0": "1.3.3",
"passport-github2": "0.1.11",

12
examples/auth/types.ts Normal file
View File

@@ -0,0 +1,12 @@
import {DefaultCtx, SessionContext, DefaultPublicData} from "blitz"
import {User} from "db"
declare module "blitz" {
export interface Ctx extends DefaultCtx {
session: SessionContext
}
export interface PublicData extends DefaultPublicData {
userId: User["id"]
views?: number
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "no-prisma",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"scripts": {
"start": "blitz start",
"build": "blitz build",
@@ -26,7 +26,7 @@
]
},
"dependencies": {
"blitz": "0.23.1-canary.0",
"blitz": "0.24.0-canary.3",
"knex": "0.21.2",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/plain-js",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"scripts": {
"start": "blitz start",
"build": "blitz db migrate && blitz build",
@@ -31,7 +31,7 @@
"dependencies": {
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"blitz": "0.24.0-canary.3",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8"
},

View File

@@ -2,7 +2,6 @@ import {Suspense} from "react"
import {Link, useRouter, useQuery, useParam} from "blitz"
import getProduct from "app/products/queries/getProduct"
import ProductForm from "app/products/components/ProductForm"
import {queryCache} from "react-query"
function Product() {
const router = useRouter()
@@ -12,9 +11,8 @@ function Product() {
return (
<ProductForm
product={product}
onSuccess={() => {
queryCache.invalidateQueries("/api/products/queries/getProducts")
router.push("/admin/products")
onSuccess={async () => {
await router.push("/admin/products")
}}
/>
)

View File

@@ -1,7 +1,7 @@
import {Suspense} from "react"
import {useQuery, Link, useRouterQuery} from "blitz"
import {useQuery, Link, useRouterQuery, invalidateQuery} from "blitz"
import getProducts from "app/products/queries/getProducts"
import getProduct from "app/products/queries/getProduct"
// import getProduct from "app/products/queries/getProduct"
function ProductsList() {
const {orderby = "id", order = "desc"} = useRouterQuery()
@@ -17,7 +17,12 @@ function ProductsList() {
{products.map((product) => (
<li key={product.id}>
<Link href="/admin/products/[id]" as={`/admin/products/${product.id}`}>
<a onMouseEnter={() => getProduct({where: {id: product.id}})}>{product.name}</a>
<a
// Disable until prefetch api added
//onMouseEnter={() => getProduct({where: {id: product.id}})}
>
{product.name}
</a>
</Link>{" "}
- Created: {product.createdAt.toISOString()}
</li>
@@ -31,6 +36,8 @@ function AdminProducts() {
<div>
<h1>Products</h1>
<button onClick={() => invalidateQuery(getProducts)}>Invalidate query</button>
<p>
<Link href="/admin/products/new">
<a>Create Product</a>

View File

@@ -2,6 +2,7 @@ import {Form, Field} from "react-final-form"
import {Product, ProductCreateInput, ProductUpdateInput} from "db"
import createProduct from "../mutations/createProduct"
import updateProduct from "../mutations/updateProduct"
import {useMutation} from "blitz"
type ProductInput = ProductCreateInput | Product
@@ -16,13 +17,15 @@ type ProductFormProps = {
}
function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
const [createProductMutation] = useMutation(createProduct)
const [updateProductMutation] = useMutation(updateProduct)
return (
<Form
initialValues={product || {name: null, handle: null, description: null, price: null}}
onSubmit={async (data: any) => {
if (isNew(data)) {
try {
const product = await createProduct({data})
const product = await createProductMutation({data})
onSuccess(product)
} catch (error) {
alert("Error creating product " + JSON.stringify(error, null, 2))
@@ -32,7 +35,7 @@ function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
// Can't update id
const id = data.id
delete data.id
const product = await updateProduct({where: {id}, data})
const product = await updateProductMutation({where: {id}, data})
onSuccess(product)
} catch (error) {
alert("Error updating product " + JSON.stringify(error, null, 2))

View File

@@ -1,11 +1,12 @@
import db, {ProductUpdateArgs} from "db"
import {Ctx} from "blitz"
type UpdateProductInput = {
where: ProductUpdateArgs["where"]
data: ProductUpdateArgs["data"]
}
export default async function updateProduct({where, data}: UpdateProductInput) {
export default async function updateProduct({where, data}: UpdateProductInput, _ctx: Ctx) {
const product = await db.product.update({where, data})
return product

View File

@@ -10,7 +10,7 @@ type StaticProps = {
}
export const getStaticProps: GetStaticProps<StaticProps> = async (ctx) => {
const product = await getProduct({where: {handle: ctx.params!.handle as string}})
const product = await getProduct({where: {handle: ctx.params!.handle as string}}, {} as any)
const dataString = superjson.stringify(product)
return {
props: {dataString},

View File

@@ -1,5 +1,5 @@
import {useMemo} from "react"
import {ssrQuery, GetServerSideProps, Link, BlitzPage, PromiseReturnType} from "blitz"
import {invokeWithMiddleware, GetServerSideProps, Link, BlitzPage, PromiseReturnType} from "blitz"
import getProducts from "app/products/queries/getProducts"
import superjson from "superjson"
@@ -10,7 +10,7 @@ type PageProps = {
type Products = PromiseReturnType<typeof getProducts>
export const getServerSideProps: GetServerSideProps = async ({req, res}) => {
const products = await ssrQuery(getProducts, {orderBy: {id: "desc"}}, {req, res})
const products = await invokeWithMiddleware(getProducts, {orderBy: {id: "desc"}}, {req, res})
const dataString = superjson.stringify(products)
return {
props: {

View File

@@ -1,4 +1,4 @@
import {NotFoundError} from "blitz"
import {NotFoundError, Ctx} from "blitz"
import db, {FindOneProductArgs} from "db"
type GetProductInput = {
@@ -7,7 +7,7 @@ type GetProductInput = {
// include?: FindOneProductArgs['include']
}
export default async function getProduct({where}: GetProductInput) {
export default async function getProduct({where}: GetProductInput, _ctx: Ctx) {
const product = await db.product.findOne({where})
if (!product) throw new NotFoundError()

View File

@@ -43,8 +43,7 @@ describe("admin/products/[handle] page", () => {
cy.get("button").click()
cy.location("pathname").should("equal", "/admin/products")
// Todo - make test work for this
// cy.get("ul > li:last-child").contains(data[0] + random)
cy.get("ul > li:last-child").contains(data[0] + random)
})
})

View File

@@ -22,9 +22,9 @@ const randomProduct = () => {
const seed = async () => {
for (let i = 0; i < 5; i++) {
await db.product.create({ data: randomProduct() })
await db.product.create({data: randomProduct()})
}
await db.user.create({ data: { email: "foo@bar.com", name: "Foobar" } })
await db.user.create({data: {email: "foo@bar.com", name: "Foobar"}})
}
export default seed

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/store",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"private": true,
"scripts": {
"build": "blitz db migrate && blitz build",
@@ -9,6 +9,9 @@
"test:start": "blitz db migrate && blitz start --production -p 3099",
"test": "start-server-and-test test:start http://localhost:3099 cy:run"
},
"prisma": {
"schema": "db/schema.prisma"
},
"prettier": {
"semi": false,
"printWidth": 100,
@@ -16,9 +19,9 @@
"trailingComma": "all"
},
"dependencies": {
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"@prisma/cli": "2.8.0",
"@prisma/client": "2.8.0",
"blitz": "0.24.0-canary.3",
"final-form": "4.19.1",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",

View File

@@ -1,6 +1,6 @@
{
"name": "tailwind",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"scripts": {
"build": "blitz db migrate && blitz build",
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
@@ -30,7 +30,7 @@
"dependencies": {
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"blitz": "0.24.0-canary.3",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",
"typescript": "3.8.3"

View File

@@ -1,5 +1,5 @@
{
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"packages": ["packages/*"],
"npmClient": "yarn",
"useWorkspaces": true,

View File

@@ -81,8 +81,8 @@
"@types/vinyl": "2.0.4",
"@types/vinyl-fs": "2.4.11",
"@types/webpack": "4.41.13",
"@typescript-eslint/eslint-plugin": "2.x",
"@typescript-eslint/parser": "2.x",
"@typescript-eslint/eslint-plugin": "4.3.1-alpha.1",
"@typescript-eslint/parser": "4.3.1-alpha.1",
"@wessberg/cjs-to-esm-transformer": "0.0.22",
"@wessberg/rollup-plugin-ts": "1.3.3",
"babel-eslint": "10.x",
@@ -133,7 +133,7 @@
"ts-jest": "24.3.0",
"tsdx": "0.13.3",
"tslib": "1.11.1",
"typescript": "3.8.3",
"typescript": "4.0.3",
"wait-on": "4.0.2"
},
"husky": {

View File

@@ -1,7 +1,7 @@
{
"name": "blitz",
"description": "Blitz is a Rails-like framework for monolithic, full-stack React apps — built on Next.js",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
@@ -39,11 +39,11 @@
"url": "https://github.com/blitz-js/blitz"
},
"dependencies": {
"@blitzjs/cli": "0.23.1-canary.0",
"@blitzjs/core": "0.23.1-canary.0",
"@blitzjs/generator": "0.23.1-canary.0",
"@blitzjs/installer": "0.23.1-canary.0",
"@blitzjs/server": "0.23.1-canary.0",
"@blitzjs/cli": "0.24.0-canary.3",
"@blitzjs/core": "0.24.0-canary.3",
"@blitzjs/generator": "0.24.0-canary.3",
"@blitzjs/installer": "0.24.0-canary.3",
"@blitzjs/server": "0.24.0-canary.3",
"envinfo": "7.7.2",
"os-name": "3.1.0",
"pkg-dir": "4.2.0",

View File

@@ -19,9 +19,10 @@ async function main() {
if (parseSemver(process.version).major < 12) {
console.log(
chalk.yellow(
`You are using an unsupported version of Node.js. Consider switching to v12 or newer.\n`,
`You are using an unsupported version of Node.js. Please switch to v12 or newer.\n`,
),
)
process.exit()
}
const globalBlitzPath = resolveFrom(__dirname, "blitz")

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/cli",
"description": "Blitz.js CLI",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"license": "MIT",
"scripts": {
"b": "./bin/run",
@@ -30,8 +30,8 @@
"/lib"
],
"dependencies": {
"@blitzjs/display": "0.23.1-canary.0",
"@blitzjs/repl": "0.23.1-canary.0",
"@blitzjs/display": "0.24.0-canary.3",
"@blitzjs/repl": "0.24.0-canary.3",
"@oclif/command": "1.5.20",
"@oclif/config": "1.15.1",
"@oclif/plugin-autocomplete": "0.2.0",
@@ -59,9 +59,9 @@
"v8-compile-cache": "2.1.1"
},
"devDependencies": {
"@blitzjs/generator": "0.23.1-canary.0",
"@blitzjs/installer": "0.23.1-canary.0",
"@blitzjs/server": "0.23.1-canary.0",
"@blitzjs/generator": "0.24.0-canary.3",
"@blitzjs/installer": "0.24.0-canary.3",
"@blitzjs/server": "0.24.0-canary.3",
"@oclif/dev-cli": "1.22.2",
"@oclif/test": "1.2.5",
"@prisma/cli": "2.4.1",

View File

@@ -148,6 +148,44 @@ export function getDbName(connectionString: string): string {
return dbName
}
async function runSeed() {
const projectRoot = require("../utils/get-project-root").projectRoot
const seedPath = require("path").join(projectRoot, "db/seeds")
const dbPath = require("path").join(projectRoot, "db/index")
log.branded("Seeding database")
let spinner = log.spinner("Loading seeds\n").start()
let seeds: Function | undefined
try {
seeds = require(seedPath).default
if (seeds === undefined) {
throw new Error(`Cant find default export from db/seeds`)
}
} catch (err) {
log.error(`Couldn't import default from db/seeds.ts or db/seeds/index.ts file`)
throw err
}
spinner.succeed()
spinner = log.spinner("Checking for database migrations\n").start()
await runMigrate({}, `--schema=${require("path").join(process.cwd(), "db", "schema.prisma")}`)
spinner.succeed()
try {
console.log(log.withCaret("Seeding..."))
seeds && await seeds()
} catch (err) {
log.error(err)
log.error(`Couldn't run imported function, are you sure it's a function?`)
throw err
}
const db = require(dbPath).default
await db.$disconnect()
log.success("Done seeding")
}
export class Db extends Command {
static description = `Run database commands
@@ -166,6 +204,10 @@ ${require("chalk").bold(
${require("chalk").bold(
"reset",
)} Reset the database and run a fresh migration via Prisma 2. You can also pass --force to skip all the user prompts.
${require("chalk").bold(
"seed",
)} Generates seeded data in database via Prisma 2. You need db/seeds.ts or db/seeds/index.ts.
`
static args = [
@@ -262,6 +304,14 @@ ${require("chalk").bold(
return Db.run(["--help"])
}
if (command === "seed") {
try {
return await runSeed()
} catch {
process.exit(1)
}
}
this.log("\nUh oh, Blitz does not support that command.")
this.log("You can try running a prisma command directly with:")
this.log("\n `npm run prisma COMMAND` or `yarn prisma COMMAND`\n")

View File

@@ -1,46 +0,0 @@
import {Command} from "@oclif/command"
import {join} from "path"
import pkgDir from "pkg-dir"
import {log} from "@blitzjs/display"
import {runMigrate} from "./db"
const projectRoot = pkgDir.sync() || process.cwd()
export class Seed extends Command {
static description = `Fill database with seed data`
async run() {
log.branded("Seeding database")
let spinner = log.spinner("Loading seeds").start()
let seeds: Function
try {
seeds = require(join(projectRoot, "db/seeds")).default
if (seeds === undefined) {
throw new Error(`Cant find default export from db/seeds`)
}
} catch (err) {
log.error(err)
this.error(`Couldn't import default from db/seeds.ts or db/seeds/index.ts file`)
}
spinner.succeed()
spinner = log.spinner("Checking for database migrations").start()
await runMigrate({}, `--schema=${join(process.cwd(), "db", "schema.prisma")}`)
spinner.succeed()
try {
console.log(log.withCaret("Seeding..."))
await seeds()
} catch (err) {
log.error(err)
this.error(`Couldn't run imported function, are you sure it's a function?`)
}
const db = require(join(projectRoot, "db/index")).default
await db.disconnect()
log.success("Done seeding")
}
}

View File

@@ -39,7 +39,7 @@ export const isBlitzRoot = async (): Promise<{
try {
const local = await readJSON("./package.json")
if (local) {
if (local.dependencies["blitz"] || local.devDependencies["blitz"]) {
if (local.dependencies?.["blitz"] || local.devDependencies?.["blitz"]) {
return {err: false}
} else {
return {

View File

@@ -1,15 +1,17 @@
import * as path from "path"
import {resolveBinAsync} from "@blitzjs/server"
import pkgDir from "pkg-dir"
import {join} from "path"
import {Db} from "../../src/commands/db"
let onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
callback(0)
})
const spawn = jest.fn(() => ({on: onSpy, off: jest.fn()}))
jest.doMock("cross-spawn", () => ({spawn}))
import {Db} from "../../src/commands/db"
pkgDir.sync = jest.fn(() => join(__dirname, "../__fixtures__/"))
let schemaArg: string
let prismaBin: string
@@ -18,6 +20,7 @@ let migrateUpDevParams: any[]
let migrateUpProdParams: any[]
let migrateSaveWithNameParams: any[]
let migrateSaveWithUnknownParams: any[]
beforeAll(async () => {
schemaArg = `--schema=${path.join(process.cwd(), "db", "schema.prisma")}`
prismaBin = await resolveBinAsync("@prisma/cli", "prisma")
@@ -89,6 +92,13 @@ describe("Db command", () => {
expect(onSpy).toHaveBeenCalledTimes(3)
}
function expectDbSeedOutcome() {
expect(spawn).toBeCalledWith(...migrateSaveParams)
expect(spawn.mock.calls.length).toBe(3)
expect(onSpy).toHaveBeenCalledTimes(3)
expect(spawn).toBeCalledWith(...migrateUpDevParams)
}
it("runs db help when no command given", async () => {
// When running the help command oclif exits with code 0
// Unfortantely it treats this as an exception and throws accordingly
@@ -189,4 +199,24 @@ describe("Db command", () => {
expect(spawn.mock.calls.length).toBe(0)
})
describe("runs db seed", () => {
let $disconnect: jest.Mock
beforeAll(() => {
jest.doMock("../__fixtures__/db", () => {
$disconnect = jest.fn()
return {default: {$disconnect}}
})
})
it("runs migrations and closes db at the end", async () => {
await Db.run(["seed"])
expectDbSeedOutcome()
})
it("closes connection at the end", async () => {
await Db.run(["seed"])
expect($disconnect).toBeCalled()
})
})
})

View File

@@ -1,81 +0,0 @@
import {join} from "path"
import pkgDir from "pkg-dir"
import {resolveBinAsync} from "@blitzjs/server"
let onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
callback(0)
})
const spawn = jest.fn(() => ({on: onSpy, off: jest.fn()}))
jest.doMock("cross-spawn", () => ({spawn}))
pkgDir.sync = jest.fn(() => join(__dirname, "../__fixtures__/"))
let seedsFn: jest.Mock
jest.doMock("../__fixtures__/db/seeds", () => {
seedsFn = jest.fn()
return {default: seedsFn}
})
let disconnect: jest.Mock
jest.doMock("../__fixtures__/db", () => {
disconnect = jest.fn()
return {default: {disconnect}}
})
import {Seed} from "../../src/commands/seed"
let schemaArg: string
let prismaBin: string
let migrateUpDevParams: any[]
let migrateSaveParams: any[]
beforeAll(async () => {
schemaArg = `--schema=${join(process.cwd(), "db", "schema.prisma")}`
prismaBin = await resolveBinAsync("@prisma/cli", "prisma")
migrateSaveParams = [
prismaBin,
["migrate", "save", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit", env: process.env},
]
migrateUpDevParams = [
prismaBin,
["migrate", "up", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit", env: process.env},
]
jest.spyOn(global.console, "log").mockImplementation(jest.fn((output: string) => {}))
})
describe("Start command", () => {
beforeEach(() => {
jest.clearAllMocks()
})
afterEach(() => {
process.env.NODE_ENV = "test"
})
function expectDbMigrateOutcome() {
expect(spawn).toBeCalledWith(...migrateSaveParams)
expect(spawn.mock.calls.length).toBe(3)
expect(onSpy).toHaveBeenCalledTimes(3)
expect(spawn).toBeCalledWith(...migrateUpDevParams)
}
it("runs migrations and closes db at the end", async () => {
await Seed.run()
expectDbMigrateOutcome()
})
it("seeds the db", async () => {
await Seed.run()
expect(seedsFn).toBeCalled()
})
it("closes connection at the end", async () => {
await Seed.run()
expect(disconnect).toBeCalled()
})
})

View File

@@ -7,7 +7,7 @@
"config"
],
"author": "Fran Zekan <zekan.fran369@gmail.com>",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",

View File

@@ -3,7 +3,17 @@ import {join} from "path"
import {existsSync} from "fs"
import {PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_SERVER} from "next/constants"
export const getProjectRoot = () => {
return pkgDir.sync() || process.cwd()
}
export const getPackageJson = () => {
const projectRoot = getProjectRoot()
return require(join(projectRoot, "package.json"))
}
const configFiles = ["blitz.config.js", "next.config.js"]
/**
* @param {boolean | undefined} reload - reimport config files to reset global cache
*/
@@ -13,7 +23,7 @@ export const getConfig = (reload?: boolean): Record<string, unknown> => {
}
let blitzConfig = {}
const projectRoot = pkgDir.sync() || process.cwd()
const projectRoot = getProjectRoot()
for (const configFile of configFiles) {
if (existsSync(join(projectRoot, configFile))) {

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/core",
"description": "Blitz.js core functionality",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
@@ -40,8 +40,8 @@
"url": "https://github.com/blitz-js/blitz"
},
"dependencies": {
"@blitzjs/config": "0.23.1-canary.0",
"@blitzjs/display": "0.23.1-canary.0",
"@blitzjs/config": "0.24.0-canary.3",
"@blitzjs/display": "0.24.0-canary.3",
"bad-behavior": "1.0.1",
"cookie-session": "1.4.0",
"deepmerge": "4.2.2",
@@ -51,7 +51,7 @@
"pretty-ms": "6.0.1",
"react-query": "2.23.0",
"serialize-error": "6.0.0",
"superjson": "1.2.2",
"superjson": "1.2.3",
"url": "0.11.0"
},
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"

View File

@@ -4,10 +4,17 @@ export class AuthenticationError extends Error {
constructor(message = "You must be logged in to access this") {
super(message)
}
get _clearStack() {
return true
}
}
export class CSRFTokenMismatchError extends AuthenticationError {
export class CSRFTokenMismatchError extends Error {
name = "CSRFTokenMismatchError"
statusCode = 401
get _clearStack() {
return true
}
}
export class AuthorizationError extends Error {
@@ -16,6 +23,9 @@ export class AuthorizationError extends Error {
constructor(message = "You are not authorized to access this") {
super(message)
}
get _clearStack() {
return true
}
}
export class NotFoundError extends Error {
@@ -24,4 +34,7 @@ export class NotFoundError extends Error {
constructor(message = "This could not be found") {
super(message)
}
get _clearStack() {
return true
}
}

View File

@@ -1,20 +1,50 @@
import {NextPage, NextComponentType, NextPageContext} from "next"
import {AppProps as NextAppProps} from "next/app"
export * from "./use-query"
export * from "./use-paginated-query"
export * from "./use-params"
export * from "./use-infinite-query"
export * from "./ssr-query"
export * from "./rpc"
export * from "./with-router"
export * from "./use-router"
export * from "./use-router-query"
export * from "./middleware"
export * from "./types"
export * from "./supertokens"
export * from "./passport-adapter"
export * from "./errors"
export {useQuery, usePaginatedQuery, useInfiniteQuery} from "./use-query-hooks"
export {getQueryKey, invalidateQuery} from "./utils/react-query-utils"
export {useParam, useParams} from "./use-params"
export {withRouter, RouterContext, BlitzRouter} from "./with-router"
export {useRouter} from "./use-router"
export {useRouterQuery} from "./use-router-query"
export {passportAuth} from "./passport-adapter"
export {getIsomorphicEnhancedResolver} from "./rpc"
export {useMutation} from "./use-mutation"
export {invoke, invokeWithMiddleware} from "./invoke"
export {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
MiddlewareResponse,
MiddlewareRequest,
connectMiddleware,
Ctx,
DefaultCtx,
} from "./middleware"
export {
TOKEN_SEPARATOR,
HANDLE_SEPARATOR,
SESSION_TYPE_OPAQUE_TOKEN_SIMPLE,
SESSION_TYPE_ANONYMOUS_JWT,
SESSION_TOKEN_VERSION_0,
COOKIE_ANONYMOUS_SESSION_TOKEN,
COOKIE_SESSION_TOKEN,
COOKIE_REFRESH_TOKEN,
COOKIE_CSRF_TOKEN,
COOKIE_PUBLIC_DATA_TOKEN,
HEADER_CSRF,
HEADER_PUBLIC_DATA_TOKEN,
HEADER_SESSION_REVOKED,
HEADER_CSRF_ERROR,
LOCALSTORAGE_PREFIX,
getAntiCSRFToken,
useSession,
SessionConfig, // new
PublicData,
SessionContext,
DefaultPublicData,
} from "./supertokens"
// --------------------
// Exports from Next.js

View File

@@ -3,26 +3,26 @@ import listen from "test-listen"
import fetch from "isomorphic-unfetch"
import delay from "delay"
import {ssrQuery} from "./ssr-query"
import {EnhancedResolverModule} from "./rpc"
import {invokeWithMiddleware} from "./invoke"
import {EnhancedResolver} from "./types"
describe("ssrQuery", () => {
describe("invokeWithMiddleware", () => {
it("works without middleware", async () => {
console.log = jest.fn()
const resolverModule = (jest.fn().mockImplementation(async (input) => {
await delay(1)
return input
}) as unknown) as EnhancedResolverModule
}) as unknown) as EnhancedResolver<unknown, unknown>
resolverModule._meta = {
name: "getTest",
type: "query",
path: "some/test/path",
filePath: "some/test/path",
apiUrl: "some/test/path",
}
await mockServer(
async (req, res) => {
const result = await ssrQuery(resolverModule as any, "test", {req, res})
const result = await invokeWithMiddleware(resolverModule as any, "test", {req, res})
expect(result).toBe("test")
},
@@ -38,11 +38,11 @@ describe("ssrQuery", () => {
const resolverModule = (jest.fn().mockImplementation(async (input) => {
await delay(1)
return input
}) as unknown) as EnhancedResolverModule
}) as unknown) as EnhancedResolver<unknown, unknown>
resolverModule._meta = {
name: "getTest",
type: "query",
path: "some/test/path",
filePath: "some/test/path",
apiUrl: "some/test/path",
}
resolverModule.middleware = [
@@ -58,7 +58,49 @@ describe("ssrQuery", () => {
await mockServer(
async (req, res) => {
const result = await ssrQuery(resolverModule as any, "test", {req, res})
const result = await invokeWithMiddleware(resolverModule as any, "test", {req, res})
expect(result).toBe("test")
},
async (url) => {
const res = await fetch(url)
expect(res.status).toBe(201)
expect(res.headers.get("test")).toBe("works")
},
)
})
it("works with extra middleware in config", async () => {
console.log = jest.fn()
const resolverModule = (jest.fn().mockImplementation(async (input) => {
await delay(1)
return input
}) as unknown) as EnhancedResolver<unknown, unknown>
resolverModule._meta = {
name: "getTest",
type: "query",
filePath: "some/test/path",
apiUrl: "some/test/path",
}
resolverModule.middleware = [
(_req, res, next) => {
res.statusCode = 201
return next()
},
]
await mockServer(
async (req, res) => {
const result = await invokeWithMiddleware(resolverModule as any, "test", {
req,
res,
middleware: [
(_req, res, next) => {
res.setHeader("test", "works")
return next()
},
],
})
expect(result).toBe("test")
},

View File

@@ -0,0 +1,80 @@
import {
QueryFn,
FirstParam,
PromiseReturnType,
Resolver,
EnhancedResolver,
EnhancedResolverRpcClient,
InvokeWithMiddlewareConfig,
} from "./types"
import {isClient} from "./utils"
import {baseLogger, log as displayLog, chalk} from "@blitzjs/display"
import prettyMs from "pretty-ms"
import {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
MiddlewareResponse,
} from "./middleware"
export function invoke<T extends QueryFn, TInput = FirstParam<T>, TResult = PromiseReturnType<T>>(
queryFn: T,
params: TInput,
) {
if (typeof queryFn === "undefined") {
throw new Error(
"invoke is missing the first argument - it must be a query or mutation function",
)
}
if (isClient) {
const fn = (queryFn as unknown) as EnhancedResolverRpcClient<TInput, TResult>
return fn(params, {fromInvoke: true})
} else {
const fn = (queryFn as unknown) as EnhancedResolver<TInput, TResult>
return fn(params) as ReturnType<T>
}
}
export async function invokeWithMiddleware<TInput, TResult>(
resolver: Resolver<TInput, TResult>,
params: TInput,
ctx: InvokeWithMiddlewareConfig,
): Promise<TResult> {
if (!ctx.req) {
throw new Error("You must provide `req` in third argument of invokeWithMiddleware()")
}
if (!ctx.res) {
throw new Error("You must provide `res` in third argument of invokeWithMiddleware()")
}
const enhancedResolver = (resolver as unknown) as EnhancedResolver<TInput, TResult>
const middleware = getAllMiddlewareForModule(enhancedResolver)
if (ctx.middleware) {
middleware.push(...ctx.middleware)
}
middleware.push(async (_req, res, next) => {
const log = baseLogger.getChildLogger({prefix: [enhancedResolver._meta.name + "()"]})
displayLog.newline()
try {
log.info(chalk.dim("Starting with input:"), params)
const startTime = new Date().getTime()
const result = await enhancedResolver(params, res.blitzCtx)
const duration = prettyMs(new Date().getTime() - startTime)
log.info(chalk.dim("Finished", "in", duration))
displayLog.newline()
res.blitzResult = result
return next()
} catch (error) {
throw error
}
})
await handleRequestWithMiddleware(ctx.req, ctx.res, middleware)
return (ctx.res as MiddlewareResponse).blitzResult as TResult
}

View File

@@ -4,7 +4,8 @@ import fetch from "isomorphic-unfetch"
import {apiResolver} from "next/dist/next-server/server/api-utils"
import {BlitzApiRequest, BlitzApiResponse} from "."
import {Middleware, handleRequestWithMiddleware} from "./middleware"
import {handleRequestWithMiddleware} from "./middleware"
import {Middleware} from "./types"
describe("handleRequestWithMiddleware", () => {
it("works without await", async () => {
@@ -21,7 +22,7 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url)
const res = await fetch(url, {method: "POST"})
expect(res.status).toBe(201)
expect(res.headers.get("test")).toBe("works")
})
@@ -40,7 +41,7 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url)
const res = await fetch(url, {method: "POST"})
expect(res.status).toBe(201)
expect(res.headers.get("test")).toBe("works")
})
@@ -59,12 +60,13 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url)
const res = await fetch(url, {method: "POST"})
expect(res.status).toBe(201)
expect(res.headers.get("test")).toBe("works")
})
})
// Failing on windows for unknown reason
it("middleware can throw", async () => {
console.log = jest.fn()
console.error = jest.fn()
@@ -77,12 +79,13 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url)
const res = await fetch(url, {method: "POST"})
expect(forbiddenMiddleware).not.toBeCalled()
expect(res.status).toBe(500)
})
})
// Failing on windows for unknown reason
it("middleware can return error", async () => {
console.log = jest.fn()
const forbiddenMiddleware = jest.fn()
@@ -94,7 +97,7 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url)
const res = await fetch(url, {method: "POST"})
expect(forbiddenMiddleware).not.toBeCalled()
expect(res.status).toBe(500)
})
@@ -103,8 +106,13 @@ describe("handleRequestWithMiddleware", () => {
async function mockServer(middleware: Middleware[], callback: (url: string) => Promise<void>) {
const apiEndpoint = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
await handleRequestWithMiddleware(req, res, middleware)
res.end()
try {
await handleRequestWithMiddleware(req, res, middleware)
} catch (err) {
res.status(500)
} finally {
res.end()
}
return
}

View File

@@ -1,10 +1,12 @@
/* eslint-disable es5/no-for-of -- file only used on the server */
/* eslint-disable es5/no-es6-methods -- file only used on the server */
import {BlitzApiRequest, BlitzApiResponse} from "."
import {IncomingMessage, ServerResponse} from "http"
import {EnhancedResolverModule} from "./rpc"
import {getConfig} from "@blitzjs/config"
import {log} from "@blitzjs/display"
import {log, baseLogger} from "@blitzjs/display"
import {Middleware, MiddlewareNext, ConnectMiddleware, EnhancedResolver} from "./types"
export interface DefaultCtx {}
export interface Ctx extends DefaultCtx {}
export interface MiddlewareRequest extends BlitzApiRequest {
protocol?: string
@@ -23,26 +25,10 @@ export interface MiddlewareResponse extends BlitzApiResponse {
*/
blitzResult: unknown
}
export type MiddlewareNext = (error?: Error) => Promise<void> | void
export type Middleware = (
req: MiddlewareRequest,
res: MiddlewareResponse,
next: MiddlewareNext,
) => Promise<void> | void
export type ConnectMiddleware = (
req: IncomingMessage,
res: ServerResponse,
next: (error?: Error) => void,
) => void
export type ResolverModule = {
default: (args: any, ctx: any) => Promise<unknown>
middleware?: Middleware[]
}
export function getAllMiddlewareForModule(resolverModule: EnhancedResolverModule) {
export function getAllMiddlewareForModule<TInput, TResult>(
resolverModule: EnhancedResolver<TInput, TResult>,
) {
const middleware: Middleware[] = []
const config = getConfig()
if (config.middleware) {
@@ -64,6 +50,7 @@ export async function handleRequestWithMiddleware(
req: BlitzApiRequest | IncomingMessage,
res: BlitzApiResponse | ServerResponse,
middleware: Middleware | Middleware[],
{throwOnError = true}: {throwOnError?: boolean} = {},
) {
if (!(res as MiddlewareResponse).blitzCtx) {
;(res as MiddlewareResponse).blitzCtx = {}
@@ -89,20 +76,22 @@ export async function handleRequestWithMiddleware(
log.newline()
if (req.method === "GET") {
// This GET method check is so we don't .end() the request for SSR requests
log.error("Error while processing the request:\n")
log.error(error)
baseLogger.error("Error while processing the request")
} else if (res.writableFinished) {
baseLogger.error(
"Error occured in middleware after the response was already sent to the browser",
)
} else {
if (!res.writableFinished) {
res.statusCode = (error as any).statusCode || (error as any).status || 500
res.end(error.message || res.statusCode.toString())
log.error("Error while processing the request:\n")
} else {
log.error(
"Error occured in middleware after the response was already sent to the browser:\n",
)
}
res.statusCode = (error as any).statusCode || (error as any).status || 500
res.end(error.message || res.statusCode.toString())
baseLogger.error("Error while processing the request")
}
throw error
if (error._clearStack) {
delete error.stack
}
baseLogger.prettyError(error)
log.newline()
if (throwOnError) throw error
}
}

View File

@@ -5,28 +5,14 @@ import {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
connectMiddleware,
Middleware,
} from "./middleware"
import {SessionContext, PublicData} from "./supertokens"
import {SessionContext} from "./supertokens"
import {log} from "@blitzjs/display"
import passport, {AuthenticateOptions, Strategy} from "passport"
import passport from "passport"
import cookieSession from "cookie-session"
import {isLocalhost} from "./utils/index"
import {secureProxyMiddleware} from "./secure-proxy-middleware"
export type BlitzPassportConfig = {
successRedirectUrl?: string
errorRedirectUrl?: string
authenticateOptions?: AuthenticateOptions
strategies: Required<Strategy>[]
secureProxy?: boolean
}
export type VerifyCallbackResult = {
publicData: PublicData
privateData?: Record<string, any>
redirectUrl?: string
}
import {VerifyCallbackResult, BlitzPassportConfig, Middleware} from "./types"
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
@@ -76,7 +62,7 @@ export function passportAuth(config: BlitzPassportConfig) {
middleware.push(async (req, res, next) => {
const session = res.blitzCtx.session as SessionContext
assert(session, "Missing Blitz sessionMiddleware!")
await session.setPublicData({[INTERNAL_REDIRECT_URL_KEY]: req.query.redirectUrl})
await session.setPublicData({[INTERNAL_REDIRECT_URL_KEY]: req.query.redirectUrl} as any)
return next()
})
}
@@ -113,9 +99,9 @@ export function passportAuth(config: BlitzPassportConfig) {
const redirectUrlFromVerifyResult =
result && typeof result === "object" && (result as any).redirectUrl
let redirectUrl =
let redirectUrl: string =
redirectUrlFromVerifyResult ||
session.publicData[INTERNAL_REDIRECT_URL_KEY] ||
(session.publicData as any)[INTERNAL_REDIRECT_URL_KEY] ||
(error ? config.errorRedirectUrl : config.successRedirectUrl) ||
"/"
@@ -129,10 +115,9 @@ export function passportAuth(config: BlitzPassportConfig) {
assert(isVerifyCallbackResult(result), "Passport verify callback is invalid")
await session.create(
{...result.publicData, [INTERNAL_REDIRECT_URL_KEY]: undefined},
result.privateData,
)
delete (result.publicData as any)[INTERNAL_REDIRECT_URL_KEY]
await session.create(result.publicData, result.privateData)
res.setHeader("Location", redirectUrl)
res.statusCode = 302

View File

@@ -0,0 +1,95 @@
import {publicDataStore} from "./public-data-store"
import {COOKIE_PUBLIC_DATA_TOKEN, parsePublicDataToken} from "./supertokens"
import {deleteCookie, readCookie} from "./utils/cookie"
import {queryCache} from "react-query"
jest.mock("./supertokens", () => ({
parsePublicDataToken: jest.fn(),
}))
jest.mock("./utils/cookie", () => ({
readCookie: jest.fn(),
deleteCookie: jest.fn(),
}))
jest.mock("react-query")
describe("publicDataStore", () => {
afterEach(() => {
jest.clearAllMocks()
})
it("calls readCookie token on init", () => {
// note: As public-data-store has side effects, this test might be fickle
expect(readCookie).toHaveBeenCalledWith(COOKIE_PUBLIC_DATA_TOKEN)
})
describe("updateState", () => {
let localStorageSpy: jest.SpyInstance
beforeAll(() => {
localStorageSpy = jest.spyOn(Storage.prototype, "setItem")
})
afterAll(() => {
localStorageSpy.mockRestore()
})
it("sets local storage", () => {
publicDataStore.updateState()
expect(localStorageSpy).toBeCalledTimes(1)
})
it("publishes data on observable", () => {
let ret: any = null
publicDataStore.observable.subscribe((data) => {
ret = data
})
publicDataStore.updateState()
expect(ret).not.toEqual(null)
})
})
describe("clear", () => {
it("clears the cookie", () => {
publicDataStore.clear()
expect(deleteCookie).toHaveBeenCalledWith(COOKIE_PUBLIC_DATA_TOKEN)
})
it("clears the cache", () => {
publicDataStore.clear()
expect(queryCache.clear).toHaveBeenCalledTimes(1)
})
it("publishes empty data", () => {
let ret: any = null
publicDataStore.observable.subscribe((data) => {
ret = data
})
publicDataStore.clear()
expect(ret).toEqual(publicDataStore.emptyPublicData)
})
})
describe("getData", () => {
const setPublicDataToken = (value: string) => {
;(parsePublicDataToken as jest.MockedFunction<typeof parsePublicDataToken>).mockReturnValue({
publicData: value as any,
})
}
xdescribe("when the cookie is falsy", () => {
it("returns empty data if cookie is falsy", () => {
const ret = publicDataStore.getData()
expect(ret).toEqual(publicDataStore.emptyPublicData)
})
})
describe("when the cookie has a value", () => {
beforeEach(() => {
;(readCookie as jest.MockedFunction<typeof readCookie>).mockReturnValue("readCookie")
})
it("returns publicData", () => {
setPublicDataToken("foo")
const ret = publicDataStore.getData()
expect(ret).toEqual("foo")
})
})
})
})

View File

@@ -0,0 +1,55 @@
import {
LOCALSTORAGE_PREFIX,
COOKIE_PUBLIC_DATA_TOKEN,
PublicData,
parsePublicDataToken,
} from "./supertokens"
import {readCookie, deleteCookie} from "./utils/cookie"
import BadBehavior from "bad-behavior"
import {queryCache} from "react-query"
class PublicDataStore {
private eventKey = `${LOCALSTORAGE_PREFIX}publicDataUpdated`
readonly emptyPublicData: PublicData = {userId: null, roles: []}
readonly observable = BadBehavior<PublicData>()
constructor() {
if (typeof window !== "undefined") {
// Set default value
this.updateState()
window.addEventListener("storage", (event) => {
if (event.key === this.eventKey) {
this.updateState()
}
})
}
}
updateState(value?: PublicData) {
// We use localStorage as a message bus between tabs.
// Setting the current time in ms will cause other tabs to receive the `storage` event
localStorage.setItem(this.eventKey, Date.now().toString())
this.observable.next(value ?? this.getData())
}
clear() {
deleteCookie(COOKIE_PUBLIC_DATA_TOKEN)
queryCache.clear()
this.updateState(this.emptyPublicData)
}
getData() {
const publicDataToken = this.getToken()
if (!publicDataToken) {
return this.emptyPublicData
}
const {publicData} = parsePublicDataToken(publicDataToken)
return publicData
}
private getToken() {
return readCookie(COOKIE_PUBLIC_DATA_TOKEN)
}
}
export const publicDataStore = new PublicDataStore()

View File

@@ -1,26 +1,41 @@
import {deserializeError} from "serialize-error"
import {queryCache} from "react-query"
import {getQueryKey} from "./utils"
import {ResolverModule, Middleware} from "./middleware"
import {isClient, isServer, clientDebug} from "./utils"
import {
getAntiCSRFToken,
publicDataStore,
HEADER_CSRF,
HEADER_SESSION_REVOKED,
HEADER_CSRF_ERROR,
HEADER_PUBLIC_DATA_TOKEN,
} from "./supertokens"
import {publicDataStore} from "./public-data-store"
import {CSRFTokenMismatchError} from "./errors"
import {serialize, deserialize} from "superjson"
import merge from "deepmerge"
import {
ResolverType,
ResolverModule,
EnhancedResolver,
EnhancedResolverRpcClient,
CancellablePromise,
ResolverRpc,
RpcOptions,
} from "./types"
import {SuperJSONResult} from "superjson/dist/types"
import {getQueryKeyFromUrlAndParams} from "./utils/react-query-utils"
type Options = {
fromQueryHook?: boolean
resultOfGetFetchMore?: any
}
export const executeRpcCall = <TInput, TResult>(
apiUrl: string,
params: TInput,
opts: RpcOptions = {},
) => {
if (!opts.fromQueryHook && !opts.fromInvoke) {
console.warn(
"[Deprecation] Directly calling queries/mutations is deprecated in favor of invoke(queryFn, params)",
)
}
export function executeRpcCall(url: string, params: any, opts: Options = {}) {
if (typeof window === "undefined") return
if (isServer) return (Promise.resolve() as unknown) as CancellablePromise<TResult>
clientDebug("Starting request for", apiUrl)
const headers: Record<string, any> = {
"Content-Type": "application/json",
@@ -28,20 +43,19 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
const antiCSRFToken = getAntiCSRFToken()
if (antiCSRFToken) {
clientDebug("Adding antiCSRFToken cookie header", antiCSRFToken)
headers[HEADER_CSRF] = antiCSRFToken
} else {
clientDebug("No antiCSRFToken cookie found")
}
let serialized
if (opts.fromQueryHook) {
// We have to serialize query arguments inside the hooks, otherwise react-query will use
// JSON.parse(JSON.stringify) so by the time the arguments come here the real JS objects are lost
serialized = params
if (opts.resultOfGetFetchMore) {
// useInfiniteQuery usually passes in extra pageParams here that come from getFetchMore()
// This isn't serialized inside useInfiniteQuery because this data is provided separately
// by react-query
serialized = merge(params, serialize(opts.resultOfGetFetchMore))
}
let serialized: SuperJSONResult
if (opts.alreadySerialized) {
// params is already serialized with superjson when it gets here
// We have to serialize the params before passing to react-query in the query key
// because otherwise react-query will use JSON.parse(JSON.stringify)
// so by the time the arguments come here the real JS objects are lost
serialized = (params as unknown) as SuperJSONResult
} else {
serialized = serialize(params)
}
@@ -49,15 +63,14 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
// Create a new AbortController instance for this request
const controller = new AbortController()
const promise: CancellablePromise<any> = window
.fetch(url, {
const promise = window
.fetch(apiUrl, {
method: "POST",
headers,
credentials: "include",
redirect: "follow",
body: JSON.stringify({
// TODO remove `|| null` once superjson allows `undefined`
params: serialized.json || null,
params: serialized.json,
meta: {
params: serialized.meta,
},
@@ -65,15 +78,20 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
signal: controller.signal,
})
.then(async (result) => {
clientDebug("Received request for", apiUrl)
if (result.headers) {
if (result.headers.get(HEADER_PUBLIC_DATA_TOKEN)) {
publicDataStore.updateState()
clientDebug("Public data updated")
}
if (result.headers.get(HEADER_SESSION_REVOKED)) {
clientDebug("Sessin revoked")
publicDataStore.clear()
}
if (result.headers.get(HEADER_CSRF_ERROR)) {
throw new CSRFTokenMismatchError()
const err = new CSRFTokenMismatchError()
delete err.stack
throw err
}
}
@@ -81,15 +99,25 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
try {
payload = await result.json()
} catch (error) {
throw new Error(`Failed to parse json from request to ${url}`)
throw new Error(`Failed to parse json from request to ${apiUrl}`)
}
if (payload.error) {
const error = deserializeError(payload.error)
let error = deserializeError(payload.error) as any
// We don't clear the publicDataStore for anonymous users
if (error.name === "AuthenticationError" && publicDataStore.getData().userId) {
publicDataStore.clear()
}
const prismaError = error.message.match(/invalid.*prisma.*invocation/i)
if (prismaError && !("code" in error)) {
error = new Error(prismaError[0])
error.statusCode = 500
}
// Prevent client-side error popop from showing
delete error.stack
throw error
} else {
const data =
@@ -98,81 +126,95 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
: deserialize({json: payload.result, meta: payload.meta?.result})
if (!opts.fromQueryHook) {
const queryKey = getQueryKey(url, params)
const queryKey = getQueryKeyFromUrlAndParams(apiUrl, params)
queryCache.setQueryData(queryKey, data)
}
return data
return data as TResult
}
})
}) as CancellablePromise<TResult>
promise.cancel = () => controller.abort()
// Disable react-query request cancellation for now
// Having too many weird bugs with it enabled
// promise.cancel = () => controller.abort()
return promise
}
executeRpcCall.warm = (url: string) => {
if (typeof window !== "undefined") {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
window.fetch(url, {method: "HEAD"})
executeRpcCall.warm = (apiUrl: string) => {
if (isClient) {
return window.fetch(apiUrl, {method: "HEAD"})
} else {
return
}
}
interface ResolverEnhancement {
_meta: {
name: string
type: string
path: string
apiUrl: string
}
}
const getApiUrlFromResolverFilePath = (resolverFilePath: string) =>
resolverFilePath.replace(/^app\/_resolvers/, "/api")
interface CancellablePromise<T> extends Promise<T> {
cancel?: Function
}
export interface RpcFunction {
(params: any, opts: any): CancellablePromise<any>
}
export interface EnhancedRpcFunction extends RpcFunction, ResolverEnhancement {}
export interface EnhancedResolverModule extends ResolverEnhancement {
(input: any, ctx: Record<string, any>): CancellablePromise<unknown>
middleware?: Middleware[]
}
export function getIsomorphicRpcHandler(
resolver: ResolverModule,
resolverPath: string,
/*
* Overloading signature so you can specify server/client and get the
* correct return type
*/
export function getIsomorphicEnhancedResolver<TInput, TResult>(
// resolver is undefined on the client
resolver: ResolverModule<TInput, TResult> | undefined,
resolverFilePath: string,
resolverName: string,
resolverType: string,
) {
const apiUrl = resolverPath.replace(/^app\/_resolvers/, "/api")
const enhance = <T extends ResolverEnhancement>(fn: T): T => {
fn._meta = {
resolverType: ResolverType,
): EnhancedResolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>
export function getIsomorphicEnhancedResolver<TInput, TResult>(
// resolver is undefined on the client
resolver: ResolverModule<TInput, TResult> | undefined,
resolverFilePath: string,
resolverName: string,
resolverType: ResolverType,
target: "client",
): EnhancedResolverRpcClient<TInput, TResult>
export function getIsomorphicEnhancedResolver<TInput, TResult>(
// resolver is undefined on the client
resolver: ResolverModule<TInput, TResult> | undefined,
resolverFilePath: string,
resolverName: string,
resolverType: ResolverType,
target: "server",
): EnhancedResolver<TInput, TResult>
export function getIsomorphicEnhancedResolver<TInput, TResult>(
// resolver is undefined on the client
resolver: ResolverModule<TInput, TResult> | undefined,
resolverFilePath: string,
resolverName: string,
resolverType: ResolverType,
target: "server" | "client" = isClient ? "client" : "server",
): EnhancedResolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult> {
const apiUrl = getApiUrlFromResolverFilePath(resolverFilePath)
if (target === "client") {
const resolverRpc: ResolverRpc<TInput, TResult> = (params, opts) =>
executeRpcCall(apiUrl, params, opts)
const enhancedResolverRpcClient = resolverRpc as EnhancedResolverRpcClient<TInput, TResult>
enhancedResolverRpcClient._meta = {
name: resolverName,
type: resolverType,
path: resolverPath,
filePath: resolverFilePath,
apiUrl: apiUrl,
}
return fn
}
if (typeof window !== "undefined") {
let rpcFn: EnhancedRpcFunction = ((params: any, opts = {}) =>
executeRpcCall(apiUrl, params, opts)) as any
rpcFn = enhance(rpcFn)
// Warm the lambda
// eslint-disable-next-line @typescript-eslint/no-floating-promises
executeRpcCall.warm(apiUrl)
return rpcFn
return enhancedResolverRpcClient
} else {
let handler: EnhancedResolverModule = resolver.default as any
handler.middleware = resolver.middleware
handler = enhance(handler)
return handler
if (!resolver) throw new Error("resolver is missing on the server")
const enhancedResolver = (resolver.default as unknown) as EnhancedResolver<TInput, TResult>
enhancedResolver.middleware = resolver.middleware
enhancedResolver._meta = {
name: resolverName,
type: resolverType,
filePath: resolverFilePath,
apiUrl: apiUrl,
}
return enhancedResolver
}
}

View File

@@ -1,4 +1,5 @@
import {Middleware, MiddlewareRequest, MiddlewareResponse} from "middleware"
import {MiddlewareRequest, MiddlewareResponse} from "middleware"
import {Middleware} from "types"
export const secureProxyMiddleware: Middleware = function (
req: MiddlewareRequest,

View File

@@ -1,45 +0,0 @@
import {IncomingMessage, ServerResponse} from "http"
import {log} from "@blitzjs/display"
import {InferUnaryParam} from "./types"
import {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
MiddlewareResponse,
} from "./middleware"
import {EnhancedResolverModule} from "./rpc"
type QueryFn = (...args: any) => Promise<any>
type SsrQueryContext = {
req: IncomingMessage
res: ServerResponse
}
export async function ssrQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T>,
{req, res}: SsrQueryContext,
): Promise<ReturnType<T>> {
const handler = (queryFn as unknown) as EnhancedResolverModule
const middleware = getAllMiddlewareForModule(handler)
middleware.push(async (_req, res, next) => {
const logPrefix = `${handler._meta.name}`
log.newline()
try {
log.progress(`Running ${logPrefix}(${JSON.stringify(params, null, 2)})`)
const result = await handler(params, res.blitzCtx)
log.success(`${logPrefix} returned ${log.variable(JSON.stringify(result, null, 2))}\n`)
res.blitzResult = result
return next()
} catch (error) {
log.error(`${logPrefix} failed: ${error}\n`)
throw error
}
})
await handleRequestWithMiddleware(req, res, middleware)
return (res as MiddlewareResponse).blitzResult as ReturnType<T>
}

View File

@@ -0,0 +1,31 @@
import {parsePublicDataToken, TOKEN_SEPARATOR} from "./supertokens"
describe("supertokens", () => {
describe("parsePublicDataToken", () => {
it("throws if token is empty", () => {
const ret = () => parsePublicDataToken("")
expect(ret).toThrow("[parsePublicDataToken] Failed: token is empty")
})
it("throws if the token cannot be parsed", () => {
const invalidJSON = "{"
const ret = () => parsePublicDataToken(btoa(invalidJSON))
expect(ret).toThrowError("[parsePublicDataToken] Failed to parse publicDataStr: {")
})
it("parses the public data", () => {
const validJSON = '"foo"'
expect(parsePublicDataToken(btoa(validJSON))).toEqual({
publicData: "foo",
})
})
it("only uses the first separated tokens", () => {
const data = `"foo"${TOKEN_SEPARATOR}123`
expect(parsePublicDataToken(btoa(data))).toEqual({
publicData: "foo",
})
})
})
})

View File

@@ -1,7 +1,9 @@
import {useState} from "react"
import BadBehavior from "bad-behavior"
import {publicDataStore} from "./public-data-store"
import {useIsomorphicLayoutEffect} from "./utils/hooks"
import {queryCache} from "react-query"
import {readCookie} from "./utils/cookie"
const sessionPrefix = (process.env.SESSION_PREFIX || "").replace(/[^a-zA-Z0-9-_]/g, "_") + "_"
export const TOKEN_SEPARATOR = ";"
export const HANDLE_SEPARATOR = ":"
@@ -9,11 +11,11 @@ export const SESSION_TYPE_OPAQUE_TOKEN_SIMPLE = "ots"
export const SESSION_TYPE_ANONYMOUS_JWT = "ajwt"
export const SESSION_TOKEN_VERSION_0 = "v0"
export const COOKIE_ANONYMOUS_SESSION_TOKEN = "sAnonymousSessionToken"
export const COOKIE_SESSION_TOKEN = "sSessionToken"
export const COOKIE_REFRESH_TOKEN = "sIdRefreshToken"
export const COOKIE_CSRF_TOKEN = "sAntiCrfToken"
export const COOKIE_PUBLIC_DATA_TOKEN = "sPublicDataToken"
export const COOKIE_ANONYMOUS_SESSION_TOKEN = sessionPrefix + "AnonymousSessionToken"
export const COOKIE_SESSION_TOKEN = sessionPrefix + "SessionToken"
export const COOKIE_REFRESH_TOKEN = sessionPrefix + "IdRefreshToken"
export const COOKIE_CSRF_TOKEN = sessionPrefix + "AntiCrfToken"
export const COOKIE_PUBLIC_DATA_TOKEN = sessionPrefix + "PublicDataToken"
// Headers always all lower case
export const HEADER_CSRF = "anti-csrf"
@@ -27,14 +29,16 @@ function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
}
export interface PublicData extends Record<any, any> {
export interface DefaultPublicData {
userId: any
roles: string[]
}
export interface PublicData extends DefaultPublicData {}
export interface SessionModel extends Record<any, any> {
handle: string
userId?: any
userId?: PublicData["userId"]
expiresAt?: Date
hashedSessionToken?: string
antiCSRFToken?: string
@@ -47,23 +51,20 @@ export type SessionConfig = {
method?: "essential" | "advanced"
sameSite?: "none" | "lax" | "strict"
getSession: (handle: string) => Promise<SessionModel | null>
getSessions: (userId: any) => Promise<SessionModel[]>
getSessions: (userId: PublicData["userId"]) => Promise<SessionModel[]>
createSession: (session: SessionModel) => Promise<SessionModel>
updateSession: (handle: string, session: Partial<SessionModel>) => Promise<SessionModel>
deleteSession: (handle: string) => Promise<SessionModel>
unstable_isAuthorized: (userRoles: string[], input?: any) => boolean
}
export interface SessionContext {
/**
* null if anonymous
*/
userId: any
export interface SessionContextBase {
userId: unknown
roles: string[]
handle: string | null
publicData: PublicData
authorize: (input?: any) => void
isAuthorized: (input?: any) => boolean
publicData: unknown
authorize(input?: any): asserts this is AuthenticatedSessionContext
isAuthorized(input?: any): boolean
// authorize: (roleOrRoles?: string | string[]) => void
// isAuthorized: (roleOrRoles?: string | string[]) => boolean
create: (publicData: PublicData, privateData?: Record<any, any>) => Promise<void>
@@ -71,97 +72,38 @@ export interface SessionContext {
revokeAll: () => Promise<void>
getPrivateData: () => Promise<Record<any, any>>
setPrivateData: (data: Record<any, any>) => Promise<void>
setPublicData: (data: Record<any, any>) => Promise<void>
setPublicData: (data: Partial<Omit<PublicData, "userId">>) => Promise<void>
}
// Taken from https://github.com/HenrikJoreteg/cookie-getter
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
export function readCookie(name: string) {
if (typeof document === "undefined") return null
const cookie = document.cookie
const setPos = cookie.search(new RegExp("\\b" + name + "="))
const stopPos = cookie.indexOf(";", setPos)
let res
if (!~setPos) return null
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1])
return res.charAt(0) === "{" ? JSON.parse(res) : res
// Could be anonymous
export interface SessionContext extends SessionContextBase {
userId: PublicData["userId"] | null
publicData: Partial<PublicData>
}
export const setCookie = (name: string, value: string, expires: string) => {
const result = `${name}=${value};path=/;expires=${expires}`
document.cookie = result
export interface AuthenticatedSessionContext extends SessionContextBase {
userId: PublicData["userId"]
publicData: PublicData
}
export const deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")
export const getAntiCSRFToken = () => readCookie(COOKIE_CSRF_TOKEN)
export const getPublicDataToken = () => readCookie(COOKIE_PUBLIC_DATA_TOKEN)
export const parsePublicDataToken = (token: string) => {
assert(token, "[parsePublicDataToken] Failed - token is empty")
assert(token, "[parsePublicDataToken] Failed: token is empty")
const [publicDataStr, expireAt] = atob(token).split(TOKEN_SEPARATOR)
let publicData: PublicData
const [publicDataStr] = atob(token).split(TOKEN_SEPARATOR)
try {
publicData = JSON.parse(publicDataStr)
const publicData: PublicData = JSON.parse(publicDataStr)
return {
publicData,
}
} catch (error) {
throw new Error("Failed to parse publicDataToken: " + publicDataStr)
}
return {
publicData,
expireAt: expireAt && new Date(expireAt),
throw new Error(`[parsePublicDataToken] Failed to parse publicDataStr: ${publicDataStr}`)
}
}
const emptyPublicData: PublicData = {userId: null, roles: []}
export const publicDataStore = {
eventKey: LOCALSTORAGE_PREFIX + "publicDataUpdated",
observable: BadBehavior<PublicData>(),
initialize() {
if (typeof window !== "undefined") {
// Set default value
publicDataStore.updateState()
window.addEventListener("storage", (event) => {
if (event.key === this.eventKey) {
publicDataStore.updateState()
}
})
}
},
getToken() {
return getPublicDataToken()
},
getData() {
const publicDataToken = this.getToken()
if (!publicDataToken) {
return emptyPublicData
}
const {publicData, expireAt} = parsePublicDataToken(publicDataToken)
if (expireAt < new Date()) {
this.clear()
return emptyPublicData
}
return publicData
},
updateState() {
// We use localStorage as a message bus between tabs.
// Setting the current time in ms will cause other tabs to receive the `storage` event
localStorage.setItem(this.eventKey, Date.now().toString())
publicDataStore.observable.next(this.getData())
},
clear() {
deleteCookie(COOKIE_PUBLIC_DATA_TOKEN)
queryCache.clear()
this.updateState()
},
}
publicDataStore.initialize()
export const useSession = () => {
const [publicData, setPublicData] = useState(emptyPublicData)
const [publicData, setPublicData] = useState(publicDataStore.emptyPublicData)
const [isLoading, setIsLoading] = useState(true)
useIsomorphicLayoutEffect(() => {
@@ -172,7 +114,7 @@ export const useSession = () => {
return subscription.unsubscribe
}, [])
return {...publicData, isLoading}
return {...publicData, isLoading} as PublicData & {isLoading: boolean}
}
/*

View File

@@ -1,7 +1,13 @@
import {IncomingMessage, ServerResponse} from "http"
import {MiddlewareRequest, MiddlewareResponse} from "./middleware"
import {AuthenticateOptions, Strategy} from "passport"
import {PublicData} from "./supertokens"
import {MutationResult, MutateConfig} from "react-query"
/**
* Infer the type of the parameter from function that takes a single argument
*/
export type InferUnaryParam<F extends Function> = F extends (args: infer A) => any ? A : never
export type FirstParam<F extends QueryFn> = Parameters<F>[0]
/**
* Get the type of the value, that the Promise holds.
@@ -13,4 +19,135 @@ export type PromiseType<T extends PromiseLike<any>> = T extends PromiseLike<infe
*/
export type PromiseReturnType<T extends (...args: any) => Promise<any>> = PromiseType<ReturnType<T>>
export interface CancellablePromise<T> extends Promise<T> {
cancel?: Function
}
export type QueryFn = (...args: any) => Promise<any>
export type ParsedUrlQueryValue = string | string[] | undefined
export type Options = {
fromQueryHook?: boolean
resultOfGetFetchMore?: any
}
export type MiddlewareNext = (error?: Error) => Promise<void> | void
export type Middleware = (
req: MiddlewareRequest,
res: MiddlewareResponse,
next: MiddlewareNext,
) => Promise<void> | void
export type ConnectMiddleware = (
req: IncomingMessage,
res: ServerResponse,
next: (error?: Error) => void,
) => void
export type BlitzPassportConfig = {
successRedirectUrl?: string
errorRedirectUrl?: string
authenticateOptions?: AuthenticateOptions
strategies: Required<Strategy>[]
secureProxy?: boolean
}
export type VerifyCallbackResult = {
publicData: PublicData
privateData?: Record<string, any>
redirectUrl?: string
}
export {MiddlewareRequest, MiddlewareResponse}
// The actual resolver source definition
export type Resolver<TInput, TResult> = (input: TInput, ctx?: any) => Promise<TResult>
// Resolver type when imported with require()
export type ResolverModule<TInput, TResult> = {
default: Resolver<TInput, TResult>
middleware?: Middleware[]
}
export type RpcOptions = {
fromQueryHook?: boolean
fromInvoke?: boolean
alreadySerialized?: boolean
}
// The compiled rpc resolver available on client
export type ResolverRpc<TInput, TResult> = (
input?: TInput,
opts?: RpcOptions,
) => CancellablePromise<TResult>
export interface ResolverRpcExecutor<TInput, TResult> {
(apiUrl: string, params: TInput, opts?: RpcOptions): CancellablePromise<TResult>
warm: (apiUrl: string) => undefined | Promise<unknown>
}
export type ResolverType = "query" | "mutation"
export interface ResolverEnhancement {
_meta: {
name: string
type: ResolverType
filePath: string
apiUrl: string
}
}
export interface EnhancedResolver<TInput, TResult>
extends Resolver<TInput, TResult>,
ResolverEnhancement {
middleware?: Middleware[]
}
export interface EnhancedResolverRpcClient<TInput, TResult>
extends ResolverRpc<TInput, TResult>,
ResolverEnhancement {}
type RequestIdleCallbackHandle = any
type RequestIdleCallbackOptions = {
timeout: number
}
type RequestIdleCallbackDeadline = {
readonly didTimeout: boolean
timeRemaining: () => number
}
declare global {
interface Window {
requestIdleCallback: (
callback: (deadline: RequestIdleCallbackDeadline) => void,
opts?: RequestIdleCallbackOptions,
) => RequestIdleCallbackHandle
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void
}
}
export type InvokeWithMiddlewareConfig = {
req: IncomingMessage
res: ServerResponse
middleware?: Middleware[]
[prop: string]: any
}
export declare type MutateFunction<
TResult,
TError = unknown,
TVariables = unknown,
TSnapshot = unknown
> = (
variables?: TVariables,
config?: MutateConfig<TResult, TError, TVariables, TSnapshot>,
) => Promise<TResult>
export declare type MutationResultPair<TResult, TError, TVariables, TSnapshot> = [
MutateFunction<TResult, TError, TVariables, TSnapshot>,
MutationResult<TResult, TError>,
]
export declare type MutationFunction<TResult, TVariables = unknown> = (
variables: TVariables,
ctx?: any,
) => Promise<TResult>

View File

@@ -1,55 +0,0 @@
import {
useInfiniteQuery as useInfiniteReactQuery,
InfiniteQueryResult,
InfiniteQueryConfig,
} from "react-query"
import {emptyQueryFn, retryFunction} from "./use-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {getQueryCacheFunctions, QueryCacheFunctions, getInfiniteQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
type RestQueryResult<T extends QueryFn> = Omit<
InfiniteQueryResult<PromiseReturnType<T>, any>,
"resolvedData"
> &
QueryCacheFunctions<PromiseReturnType<T>[]>
const isServer = typeof window === "undefined"
export function useInfiniteQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options: InfiniteQueryConfig<PromiseReturnType<T>, any>,
): [PromiseReturnType<T>[], RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"useInfiniteQuery is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getInfiniteQueryKey(queryFn, params)
const {data, ...queryRest} = useInfiniteReactQuery({
queryKey,
queryFn: (_infinite: boolean, _apiUrl: string, params: any, resultOfGetFetchMore?: any) =>
queryRpcFn(params, {fromQueryHook: true, resultOfGetFetchMore}),
config: {
suspense: true,
retry: retryFunction,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
}
return [data as PromiseReturnType<T>[], rest as RestQueryResult<T>]
}

View File

@@ -0,0 +1,24 @@
import {useMutation as useReactQueryMutation, MutationConfig} from "react-query"
import {validateQueryFn} from "./utils/react-query-utils"
import {MutationFunction, MutationResultPair} from "./types"
/*
* We have to override react-query's MutationFunction and MutationResultPair
* types so because we have throwOnError:true by default. And by the RQ types
* have the mutate function result typed as TResult|undefined which isn't typed
* properly with throwOnError.
*
* So this fixes that.
*/
export function useMutation<TResult, TError = unknown, TVariables = undefined, TSnapshot = unknown>(
mutationResolver: MutationFunction<TResult, TVariables>,
config?: MutationConfig<TResult, TError, TVariables, TSnapshot>,
) {
validateQueryFn(mutationResolver)
return useReactQueryMutation(mutationResolver, {
throwOnError: true,
...config,
}) as MutationResultPair<TResult, TError, TVariables, TSnapshot>
}

View File

@@ -1,54 +0,0 @@
import {
usePaginatedQuery as usePaginatedReactQuery,
PaginatedQueryResult,
PaginatedQueryConfig,
} from "react-query"
import {emptyQueryFn, retryFunction} from "./use-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
type RestQueryResult<T extends QueryFn> = Omit<
PaginatedQueryResult<PromiseReturnType<T>>,
"resolvedData"
> &
QueryCacheFunctions<PromiseReturnType<T>>
const isServer = typeof window === "undefined"
export function usePaginatedQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options?: PaginatedQueryConfig<PromiseReturnType<T>>,
): [PromiseReturnType<T>, RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"usePaginatedQuery is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getQueryKey(queryFn, params)
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
queryKey,
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
config: {
suspense: true,
retry: retryFunction,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
}
return [resolvedData as PromiseReturnType<T>, rest as RestQueryResult<T>]
}

View File

@@ -1,8 +1,8 @@
import {useMemo} from "react"
import {fromPairs} from "lodash"
import {useRouter} from "next/router"
import {useRouterQuery} from "./use-router-query"
import {fromPairs} from "lodash"
type ParsedUrlQueryValue = string | string[] | undefined
import {ParsedUrlQueryValue} from "./types"
export interface ParsedUrlQuery {
[key: string]: ParsedUrlQueryValue
@@ -48,39 +48,43 @@ export function useParams(returnType?: "string" | "number" | "array") {
const router = useRouter()
const query = useRouterQuery()
const rawParams = extractRouterParams(router.query, query)
const params = useMemo(() => {
const rawParams = extractRouterParams(router.query, query)
if (returnType === "string") {
const params: Record<string, string> = {}
for (const key in rawParams) {
if (typeof rawParams[key] === "string") {
params[key] = rawParams[key] as string
if (returnType === "string") {
const params: Record<string, string> = {}
for (const key in rawParams) {
if (typeof rawParams[key] === "string") {
params[key] = rawParams[key] as string
}
}
return params
}
return params
}
if (returnType === "number") {
const params: Record<string, number> = {}
for (const key in rawParams) {
if (rawParams[key]) {
params[key] = Number(rawParams[key])
if (returnType === "number") {
const params: Record<string, number> = {}
for (const key in rawParams) {
if (rawParams[key]) {
params[key] = Number(rawParams[key])
}
}
return params
}
return params
}
if (returnType === "array") {
const params: Record<string, Array<string | undefined>> = {}
for (const key in rawParams) {
if (Array.isArray(rawParams[key])) {
params[key] = rawParams[key] as Array<string | undefined>
if (returnType === "array") {
const params: Record<string, Array<string | undefined>> = {}
for (const key in rawParams) {
if (Array.isArray(rawParams[key])) {
params[key] = rawParams[key] as Array<string | undefined>
}
}
return params
}
return params
}
return rawParams
return rawParams
}, [router.query, query, returnType])
return params
}
export function useParam(key: string): undefined | string | string[]

View File

@@ -0,0 +1,145 @@
import {
useQuery as useReactQuery,
QueryResult,
QueryConfig,
usePaginatedQuery as usePaginatedReactQuery,
PaginatedQueryResult,
PaginatedQueryConfig,
useInfiniteQuery as useInfiniteReactQuery,
InfiniteQueryResult,
InfiniteQueryConfig as RQInfiniteQueryConfig,
queryCache,
} from "react-query"
import {FirstParam, QueryFn, PromiseReturnType} from "./types"
import {
QueryCacheFunctions,
getQueryCacheFunctions,
getQueryKey,
sanitize,
defaultQueryConfig,
} from "./utils/react-query-utils"
import Router from "next/router"
Router.events.on("routeChangeComplete", async () => {
await queryCache.invalidateQueries()
})
// -------------------------
// useQuery
// -------------------------
type RestQueryResult<TResult> = Omit<QueryResult<TResult>, "data"> & QueryCacheFunctions<TResult>
export function useQuery<T extends QueryFn, TResult = PromiseReturnType<T>>(
queryFn: T,
params: FirstParam<T>,
options?: QueryConfig<TResult>,
): [TResult, RestQueryResult<TResult>] {
if (typeof queryFn === "undefined") {
throw new Error("useQuery is missing the first argument - it must be a query function")
}
const enhancedResolverRpcClient = sanitize(queryFn)
const queryKey = getQueryKey(queryFn, params)
const {data, ...queryRest} = useReactQuery({
queryKey,
queryFn: (_apiUrl: string, params: any) =>
enhancedResolverRpcClient(params, {fromQueryHook: true, alreadySerialized: true}),
config: {
...defaultQueryConfig,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<TResult>(queryKey),
}
return [data as TResult, rest as RestQueryResult<TResult>]
}
// -------------------------
// usePaginatedQuery
// -------------------------
type RestPaginatedResult<TResult> = Omit<PaginatedQueryResult<TResult>, "resolvedData"> &
QueryCacheFunctions<TResult>
export function usePaginatedQuery<T extends QueryFn, TResult = PromiseReturnType<T>>(
queryFn: T,
params: FirstParam<T>,
options?: PaginatedQueryConfig<TResult>,
): [TResult, RestPaginatedResult<TResult>] {
if (typeof queryFn === "undefined") {
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
}
const enhancedResolverRpcClient = sanitize(queryFn)
const queryKey = getQueryKey(queryFn, params)
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
queryKey,
queryFn: (_apiUrl: string, params: any) =>
enhancedResolverRpcClient(params, {fromQueryHook: true, alreadySerialized: true}),
config: {
...defaultQueryConfig,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<TResult>(queryKey),
}
return [resolvedData as TResult, rest as RestPaginatedResult<TResult>]
}
// -------------------------
// useInfiniteQuery
// -------------------------
type RestInfiniteResult<TResult> = Omit<InfiniteQueryResult<TResult>, "resolvedData"> &
QueryCacheFunctions<TResult>
interface InfiniteQueryConfig<TResult, TFetchMoreResult> extends RQInfiniteQueryConfig<TResult> {
getFetchMore?: (lastPage: TResult, allPages: TResult[]) => TFetchMoreResult
}
// TODO - Fix TFetchMoreResult not actually taking affect in apps.
// It shows as 'unknown' in the params() input argumunt, but should show as TFetchMoreResult
export function useInfiniteQuery<
T extends QueryFn,
TFetchMoreResult = any,
TResult = PromiseReturnType<T>
>(
queryFn: T,
params: (fetchMoreResult: TFetchMoreResult) => FirstParam<T>,
options: InfiniteQueryConfig<TResult, TFetchMoreResult>,
): [TResult[], RestInfiniteResult<TResult>] {
if (typeof queryFn === "undefined") {
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
}
const enhancedResolverRpcClient = sanitize(queryFn)
const queryKey = getQueryKey(queryFn)
const {data, ...queryRest} = useInfiniteReactQuery({
// we need an extra cache key for infinite loading so that the cache for
// for this query is stored separately since the hook result is an array of results.
// Without this cache for usePaginatedQuery and this will conflict and break.
queryKey: [...queryKey, "infinite"],
queryFn: (_apiUrl: string, _infinite: string, resultOfGetFetchMore: TFetchMoreResult) =>
enhancedResolverRpcClient(params(resultOfGetFetchMore), {fromQueryHook: true}),
config: {
...defaultQueryConfig,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<TResult>(queryKey),
}
return [data as TResult[], rest as RestInfiniteResult<TResult>]
}

View File

@@ -1,66 +0,0 @@
import {useQuery as useReactQuery, QueryResult, QueryConfig} from "react-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
type RestQueryResult<T extends QueryFn> = Omit<QueryResult<PromiseReturnType<T>>, "data"> &
QueryCacheFunctions<PromiseReturnType<T>>
export const emptyQueryFn: EnhancedRpcFunction = (() => {
const fn = () => new Promise(() => {})
fn._meta = {
name: "emptyQueryFn",
type: "n/a",
path: "n/a",
apiUrl: "",
}
return fn
})()
const isServer = typeof window === "undefined"
export const retryFunction = (failureCount: number, error: any) => {
if (process.env.NODE_ENV !== "production") return false
// Retry (max. 3 times) only if network error detected
if (error.message === "Network request failed" && failureCount <= 3) return true
return false
}
export function useQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options?: QueryConfig<PromiseReturnType<T>>,
): [PromiseReturnType<T>, RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("useQuery is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"useQuery is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getQueryKey(queryFn, params)
const {data, ...queryRest} = useReactQuery({
queryKey,
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
config: {
suspense: true,
retry: retryFunction,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
}
return [data as PromiseReturnType<T>, rest as RestQueryResult<T>]
}

View File

@@ -1,8 +1,15 @@
import {useMemo} from "react"
import {useRouter} from "next/router"
import {parse} from "url"
export function useRouterQuery() {
const router = useRouter()
const {query} = parse(router.asPath, true)
const query = useMemo(() => {
const {query} = parse(router.asPath, true)
return query
}, [router.asPath])
return query
}

View File

@@ -0,0 +1,18 @@
// Taken from https://github.com/HenrikJoreteg/cookie-getter
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
export function readCookie(name: string) {
if (typeof document === "undefined") return null
const cookie = document.cookie
const setPos = cookie.search(new RegExp("\\b" + name + "="))
const stopPos = cookie.indexOf(";", setPos)
let res
if (!~setPos) return null
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1])
return res.charAt(0) === "{" ? JSON.parse(res) : res
}
export const setCookie = (name: string, value: string, expires: string) => {
const result = `${name}=${value};path=/;expires=${expires}`
document.cookie = result
}
export const deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")

View File

@@ -1,12 +1,8 @@
import {QueryKey} from "react-query"
import {BlitzApiRequest} from "../"
import {IncomingMessage} from "http"
export const isServer = typeof window === "undefined"
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKey[]] {
return [cacheKey, typeof params === "function" ? (params as Function)() : params]
}
export const isClient = typeof window !== "undefined"
export function isLocalhost(req: BlitzApiRequest | IncomingMessage): boolean {
let {host} = req.headers
@@ -17,3 +13,9 @@ export function isLocalhost(req: BlitzApiRequest | IncomingMessage): boolean {
}
return localhost
}
export function clientDebug(...args: any) {
if (typeof window !== "undefined" && (window as any)["DEBUG_BLITZ"]) {
console.log("[BLITZ]", ...args)
}
}

View File

@@ -1,65 +0,0 @@
import {queryCache, QueryKey} from "react-query"
import {serialize} from "superjson"
import {InferUnaryParam, QueryFn} from "../types"
import {EnhancedRpcFunction} from "rpc"
type MutateOptions = {
refetch?: boolean
}
export interface QueryCacheFunctions<T> {
mutate: (newData: T | ((oldData: T | undefined) => T), opts?: MutateOptions) => void
}
export const getQueryCacheFunctions = <T>(queryKey: QueryKey): QueryCacheFunctions<T> => ({
mutate: (newData, opts = {refetch: true}) => {
queryCache.setQueryData(queryKey, newData)
if (opts.refetch) {
return queryCache.invalidateQueries(queryKey, {refetchActive: true})
}
return null
},
})
export function getQueryKey<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
) {
if (typeof queryFn === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"getQueryKey is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryKey: [string, Record<string, any>] = [
((queryFn as unknown) as EnhancedRpcFunction)._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
]
return queryKey
}
export function getInfiniteQueryKey<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
) {
if (typeof queryFn === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"getQueryKey is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryKey: ["infinite", string, Record<string, any>] = [
// we need an extra cache key for infinite loading so that the cache for
// for this query is stored separately since the hook result is an array of results. Without this cache for usePaginatedQuery and this will conflict and break.
"infinite",
((queryFn as unknown) as EnhancedRpcFunction)._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
]
return queryKey
}

View File

@@ -0,0 +1,129 @@
import {queryCache, QueryKey} from "react-query"
import {serialize} from "superjson"
import {Resolver, EnhancedResolverRpcClient, QueryFn} from "../types"
import {isServer, isClient} from "."
import {requestIdleCallback} from "./request-idle-callback"
type MutateOptions = {
refetch?: boolean
}
function isEnhancedResolverRpcClient(f: any): f is EnhancedResolverRpcClient<any, any> {
return !!f._meta
}
export interface QueryCacheFunctions<T> {
mutate: (
newData: T | ((oldData: T | undefined) => T),
opts?: MutateOptions,
) => Promise<void | ReturnType<typeof queryCache.invalidateQueries>>
}
export const getQueryCacheFunctions = <T>(queryKey: QueryKey): QueryCacheFunctions<T> => ({
mutate: (newData, opts = {refetch: true}) => {
return new Promise((res) => {
queryCache.setQueryData(queryKey, newData)
let result: void | ReturnType<typeof queryCache.invalidateQueries>
if (opts.refetch) {
result = res(queryCache.invalidateQueries(queryKey, {refetchActive: true}))
}
if (isClient) {
// Fix for https://github.com/blitz-js/blitz/issues/1174
requestIdleCallback(() => {
res(result)
})
} else {
res(result)
}
})
},
})
export const emptyQueryFn: EnhancedResolverRpcClient<unknown, unknown> = (() => {
const fn = () => new Promise(() => {})
fn._meta = {
name: "emptyQueryFn",
type: "n/a" as any,
filePath: "n/a",
apiUrl: "",
}
return fn
})()
export const validateQueryFn = <TInput, TResult>(
queryFn: Resolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>,
) => {
if (!isEnhancedResolverRpcClient(queryFn)) {
throw new Error(
`It looks like you are trying to use Blitz's useQuery to fetch from third-party APIs. To do that, import useQuery directly from "react-query"`,
)
}
}
export const sanitize = <TInput, TResult>(
queryFn: Resolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>,
) => {
if (isServer) {
// Prevents logging garbage during static pre-rendering
return emptyQueryFn
}
validateQueryFn(queryFn)
return queryFn as EnhancedResolverRpcClient<TInput, TResult>
}
export const getQueryKeyFromUrlAndParams = (url: string, params: unknown) => {
const queryKey = [url]
const args = typeof params === "function" ? (params as Function)() : params
queryKey.push(serialize(args) as any)
return queryKey as [string, any]
}
export function getQueryKey<TInput, TResult, T extends QueryFn>(
resolver: T | Resolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>,
params?: TInput,
) {
if (typeof resolver === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a resolver function")
}
return getQueryKeyFromUrlAndParams(sanitize(resolver)._meta.apiUrl, params)
}
export function invalidateQuery<TInput, TResult, T extends QueryFn>(
resolver: T | Resolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>,
params?: TInput,
) {
if (typeof resolver === "undefined") {
throw new Error(
"invalidateQuery is missing the first argument - it must be a resolver function",
)
}
const fullQueryKey = getQueryKey(resolver, params)
let queryKey: any
if (params) {
queryKey = fullQueryKey
} else {
// Params not provided, only use first query key item (url)
queryKey = fullQueryKey[0]
}
return queryCache.invalidateQueries(queryKey)
}
export const retryFunction = (failureCount: number, error: any) => {
if (process.env.NODE_ENV !== "production") return false
// Retry (max. 3 times) only if network error detected
if (error.message === "Network request failed" && failureCount <= 3) return true
return false
}
export const defaultQueryConfig = {
suspense: true,
retry: retryFunction,
}

View File

@@ -0,0 +1,18 @@
import {isClient} from "."
// Shim from https://developers.google.com/web/updates/2015/08/using-requestidlecallback
function requestIdleCallbackShim(cb: any) {
var start = Date.now()
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start))
},
})
}, 1)
}
export const requestIdleCallback = isClient
? window.requestIdleCallback || requestIdleCallbackShim
: requestIdleCallbackShim

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`useQuery a "query" that converts the string parameter to uppercase shouldn't work with regular functions 1`] = `"It looks like you are trying to use Blitz's useQuery to fetch from third-party APIs. To do that, import useQuery directly from \\"react-query\\""`;

View File

@@ -1,18 +0,0 @@
import {getQueryCacheFunctions} from "../src/utils/query-cache"
import {queryCache} from "react-query"
jest.mock("react-query")
describe("getQueryCacheFunctions", () => {
it("returns a mutate function with working options", () => {
const spyRefetchQueries = jest.spyOn(queryCache, "invalidateQueries")
const {mutate} = getQueryCacheFunctions("testQueryKey")
expect(mutate).toBeTruthy()
mutate({newData: true})
expect(spyRefetchQueries).toBeCalledTimes(1)
mutate({newData: true}, {refetch: false})
expect(spyRefetchQueries).toBeCalledTimes(1)
mutate({newData: true}, {refetch: true})
expect(spyRefetchQueries).toBeCalledTimes(2)
})
})

View File

@@ -0,0 +1,36 @@
import {getQueryCacheFunctions} from "../src/utils/react-query-utils"
import {queryCache} from "react-query"
jest.mock("react-query")
describe("getQueryCacheFunctions", () => {
const spyRefetchQueries = jest.spyOn(queryCache, "invalidateQueries")
beforeEach(() => spyRefetchQueries.mockReset())
it("returns a mutate function with working options", async () => {
window.requestIdleCallback = jest.fn((fn) => {
fn({} as any)
})
const {mutate} = getQueryCacheFunctions("testQueryKey")
expect(mutate).toBeTruthy()
await mutate({newData: true})
expect(spyRefetchQueries).toBeCalledTimes(1)
await mutate({newData: true}, {refetch: false})
expect(spyRefetchQueries).toBeCalledTimes(1)
await mutate({newData: true}, {refetch: true})
expect(spyRefetchQueries).toBeCalledTimes(2)
})
it("works even when requestIdleCallback is undefined", async () => {
window.requestIdleCallback = undefined as any
const {mutate} = getQueryCacheFunctions("testQueryKey")
expect(mutate).toBeTruthy()
await mutate({newData: true})
expect(spyRefetchQueries).toBeCalledTimes(1)
await mutate({newData: true}, {refetch: false})
expect(spyRefetchQueries).toBeCalledTimes(1)
await mutate({newData: true}, {refetch: true})
expect(spyRefetchQueries).toBeCalledTimes(2)
})
})

View File

@@ -1,6 +1,5 @@
import {executeRpcCall, getIsomorphicRpcHandler} from "@blitzjs/core"
global.fetch = jest.fn(() => Promise.resolve({json: () => ({result: null, error: null})}))
import {getIsomorphicEnhancedResolver} from "@blitzjs/core"
import {executeRpcCall} from "../src/rpc"
declare global {
namespace NodeJS {
@@ -10,11 +9,13 @@ declare global {
}
}
global.fetch = jest.fn(() => Promise.resolve({json: () => ({result: null, error: null})}))
describe("RPC", () => {
describe("HEAD", () => {
it("warms the endpoint", () => {
it("warms the endpoint", async () => {
expect.assertions(1)
executeRpcCall.warm("/api/endpoint")
await executeRpcCall.warm("/api/endpoint")
expect(global.fetch).toBeCalled()
})
})
@@ -32,15 +33,16 @@ describe("RPC", () => {
const resolverModule = {
default: jest.fn(),
}
const rpcFn = getIsomorphicRpcHandler(
const rpcFn = getIsomorphicEnhancedResolver(
resolverModule,
"app/_resolvers/queries/getProduct",
"testResolver",
"query",
"client",
)
try {
const result = await rpcFn("/api/endpoint", {paramOne: 1234})
const result = await rpcFn({paramOne: 1234}, {fromQueryHook: true})
expect(result).toBe("result")
expect(fetchMock).toBeCalled()
} finally {
@@ -59,15 +61,16 @@ describe("RPC", () => {
const resolverModule = {
default: jest.fn(),
}
const rpcFn = getIsomorphicRpcHandler(
const rpcFn = getIsomorphicEnhancedResolver(
resolverModule,
"app/_resolvers/queries/getProduct",
"testResolver",
"query",
"client",
)
try {
await expect(rpcFn("/api/endpoint", {paramOne: 1234})).rejects.toThrowError(
await expect(rpcFn({paramOne: 1234}, {fromQueryHook: true})).rejects.toThrowError(
/something broke/,
)
} finally {

View File

@@ -1,34 +1,31 @@
import React from "react"
import {act, render, waitForElementToBeRemoved, screen} from "./test-utils"
import {useQuery} from "../src/use-query"
import {useQuery} from "../src/use-query-hooks"
import {deserialize} from "superjson"
// This enhance fn does what getIsomorphicEnhancedResolver does during build time
const enhance = (fn: any) => {
const newFn = (...args: any) => {
const [data, ...rest] = args
return fn(deserialize(data), ...rest)
}
newFn._meta = {
name: "testResolver",
type: "query",
path: "app/test",
apiUrl: "test/url",
}
return newFn
}
describe("useQuery", () => {
const setupHook = (
params: any,
queryFn: (...args: any) => Promise<any>,
): [{data?: any}, Function] => {
// This enhance fn does what getIsomorphicRpcHandler does during build time
const enhance = (fn: any) => {
const newFn = (...args: any) => {
const [data, ...rest] = args
return fn(deserialize(data), ...rest)
}
newFn._meta = {
name: "testResolver",
type: "query",
path: "app/test",
apiUrl: "test/url",
}
return newFn
}
let res = {}
function TestHarness() {
useQuery(
enhance((num: number) => num),
1,
)
const [data] = useQuery(enhance(queryFn), params)
const [data] = useQuery(queryFn, params)
Object.assign(res, {data})
return <div id="harness">{data ? "Ready" : "Missing Dependency"}</div>
}
@@ -48,13 +45,17 @@ describe("useQuery", () => {
const upcase = async (args: string): Promise<string> => {
return args.toUpperCase()
}
it("should work", async () => {
const [res] = setupHook("test", upcase)
it("should work with Blitz queries", async () => {
const [res] = setupHook("test", enhance(upcase))
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
await act(async () => {
await screen.findByText("Ready")
expect(res.data).toBe("TEST")
})
})
it("shouldn't work with regular functions", () => {
expect(() => setupHook("test", upcase)).toThrowErrorMatchingSnapshot()
})
})
})

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/display",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"description": "Display package for the Blitz CLI",
"homepage": "https://github.com/blitz-js/blitz#readme",
"license": "MIT",
@@ -31,6 +31,7 @@
},
"dependencies": {
"chalk": "4.0.0",
"ora": "4.0.4"
"ora": "4.0.4",
"tslog": "2.9.0"
}
}

View File

@@ -1,6 +1,9 @@
import chalk from "chalk"
import c from "chalk"
import ora from "ora"
import readline from "readline"
import {Logger} from "tslog"
export const chalk = c
// const blitzTrueBrandColor = '6700AB'
const blitzBrightBrandColor = "8a3df0"
@@ -9,23 +12,27 @@ const blitzBrightBrandColor = "8a3df0"
const brandColor = blitzBrightBrandColor
const withBrand = (str: string) => {
return chalk.hex(brandColor).bold(str)
return c.hex(brandColor).bold(str)
}
const withWarning = (str: string) => {
return `⚠️ ${chalk.yellow(str)}`
return `⚠️ ${c.yellow(str)}`
}
const withCaret = (str: string) => {
return `${chalk.gray(">")} ${str}`
return `${c.gray(">")} ${str}`
}
const withCheck = (str: string) => {
return `${chalk.green("✔")} ${str}`
return `${c.green("✔")} ${str}`
}
const withX = (str: string) => {
return `${chalk.red.bold("✕")} ${str}`
return `${c.red.bold("✕")} ${str}`
}
const withProgress = (str: string) => {
return withCaret(c.bold(str))
}
/**
@@ -34,7 +41,7 @@ const withX = (str: string) => {
* @param {string} msg
*/
const branded = (msg: string) => {
console.log(chalk.hex(brandColor).bold(msg))
console.log(c.hex(brandColor).bold(msg))
}
/**
@@ -63,7 +70,7 @@ const warning = (msg: string) => {
* @param {string} msg
*/
const error = (msg: string) => {
console.error(withX(chalk.red.bold(msg)))
console.error(withX(c.red.bold(msg)))
}
/**
@@ -72,7 +79,7 @@ const error = (msg: string) => {
* @param {string} msg
*/
const meta = (msg: string) => {
console.log(withCaret(chalk.gray(msg)))
console.log(withCaret(c.gray(msg)))
}
/**
@@ -81,11 +88,11 @@ const meta = (msg: string) => {
* @param {string} msg
*/
const progress = (msg: string) => {
console.log(withCaret(chalk.bold(msg)))
console.log(withCaret(c.bold(msg)))
}
const info = (msg: string) => {
console.log(chalk.bold(msg))
console.log(c.bold(msg))
}
const spinner = (str: string) => {
@@ -105,7 +112,7 @@ const spinner = (str: string) => {
* @param {string} msg
*/
const success = (msg: string) => {
console.log(withCheck(chalk.green(msg)))
console.log(withCheck(c.green(msg)))
}
const newline = () => {
@@ -118,7 +125,7 @@ const newline = () => {
* @param {string} val
*/
const variable = (val: any) => {
return chalk.cyan.bold(`${val}`)
return c.cyan.bold(`${val}`)
}
/**
@@ -135,6 +142,7 @@ export const log = {
withCaret,
withCheck,
withX,
withProgress,
branded,
clearLine,
error,
@@ -148,3 +156,20 @@ export const log = {
info,
debug,
}
export const baseLogger = new Logger({
dateTimePattern:
process.env.NODE_ENV === "production"
? "year-month-day hour:minute:second.millisecond"
: "hour:minute:second.millisecond",
displayFunctionName: false,
displayFilePath: "hidden",
displayRequestId: false,
dateTimeTimezone:
process.env.NODE_ENV === "production"
? "utc"
: Intl.DateTimeFormat().resolvedOptions().timeZone,
prettyInspectHighlightStyles: {name: "yellow"},
maskValuesOfKeys: ["password", "passwordConfirmation"],
exposeErrorCodeFrame: process.env.NODE_ENV !== "production",
})

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/file-pipeline",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"description": "Display package for the Blitz CLI",
"homepage": "https://github.com/blitz-js/blitz#readme",
"license": "MIT",

View File

@@ -1,23 +1,22 @@
import {unlink as unlinkFile, pathExists} from "fs-extra"
import * as fs from "fs-extra"
import {relative, resolve} from "path"
import {transform} from "../transform"
import {transform} from "../../transform"
import {EventedFile} from "types"
function getDestPath(folder: string, file: EventedFile) {
const {history, cwd} = file
const [firstPath] = history
return resolve(folder, relative(cwd, firstPath))
return resolve(folder, relative(file.cwd, file.path))
}
/**
* Deletes a file in the stream from the filesystem
* @param folder The destination folder
*/
export function unlink(folder: string) {
export function unlink(folder: string, unlinkFile = fs.unlink, pathExists = fs.pathExists) {
return transform.file(async (file) => {
if (file.event === "unlink" || file.event === "unlinkDir") {
if (await pathExists(getDestPath(folder, file))) await unlinkFile(getDestPath(folder, file))
const destPath = getDestPath(folder, file)
if (await pathExists(destPath)) await unlinkFile(destPath)
}
return file

View File

@@ -0,0 +1,30 @@
import {unlink} from "."
import {normalize, resolve} from "path"
import {take} from "../../test-utils"
import File from "vinyl"
describe("unlink", () => {
it("should unlink the correct path", async () => {
const unlinkFile = jest.fn(() => Promise.resolve())
const pathExists = jest.fn(() => Promise.resolve(true))
const unlinkStream = unlink(normalize("/dest"), unlinkFile, pathExists)
unlinkStream.write(
new File({
cwd: normalize("/src"),
path: normalize("/src/bar/baz.tz"),
content: null,
event: "unlink",
}),
)
await take(unlinkStream, 1)
// Test the file exists before attempting to unlink it
expect(pathExists).toHaveBeenCalledWith(resolve(normalize("/dest/bar/baz.tz")))
// Remove the correct file
expect(unlinkFile).toHaveBeenCalledWith(resolve(normalize("/dest/bar/baz.tz")))
})
})

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/generator",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.3",
"description": "File generation for the Blitz CLI",
"homepage": "https://github.com/blitz-js/blitz#readme",
"license": "MIT",
@@ -36,7 +36,7 @@
"dependencies": {
"@babel/core": "7.9.0",
"@babel/plugin-transform-typescript": "7.9.4",
"@blitzjs/display": "0.23.1-canary.0",
"@blitzjs/display": "0.24.0-canary.3",
"@types/jscodeshift": "0.7.1",
"chalk": "4.0.0",
"cross-spawn": "7.0.3",

View File

@@ -22,6 +22,7 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
sourceRoot: string = resolve(__dirname, "./templates/app")
// Disable file-level prettier because we manually run prettier at the end
prettierDisabled = true
packageInstallSuccess: boolean = false
filesToIgnore() {
if (!this.options.useTs) {
@@ -45,43 +46,33 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
async preCommit() {
this.fs.move(this.destinationPath("gitignore"), this.destinationPath(".gitignore"))
const pkg = this.fs.readJSON(this.destinationPath("package.json"))
const ext = this.options.useTs ? "tsx" : "js"
let type: string
switch (this.options.form) {
case "React Final Form":
this.fs.move(
this.destinationPath("_forms/finalform/Form.tsx"),
this.destinationPath("app/components/Form.tsx"),
)
this.fs.move(
this.destinationPath("_forms/finalform/LabeledTextField.tsx"),
this.destinationPath("app/components/LabeledTextField.tsx"),
)
type = "finalform"
pkg.dependencies["final-form"] = "4.x"
pkg.dependencies["react-final-form"] = "6.x"
break
case "React Hook Form":
this.fs.move(
this.destinationPath("_forms/hookform/Form.tsx"),
this.destinationPath("app/components/Form.tsx"),
)
this.fs.move(
this.destinationPath("_forms/hookform/LabeledTextField.tsx"),
this.destinationPath("app/components/LabeledTextField.tsx"),
)
type = "hookform"
pkg.dependencies["react-hook-form"] = "6.x"
break
case "Formik":
this.fs.move(
this.destinationPath("_forms/formik/Form.tsx"),
this.destinationPath("app/components/Form.tsx"),
)
this.fs.move(
this.destinationPath("_forms/formik/LabeledTextField.tsx"),
this.destinationPath("app/components/LabeledTextField.tsx"),
)
type = "formik"
pkg.dependencies["formik"] = "2.x"
break
}
this.fs.move(
this.destinationPath(`_forms/${type}/Form.${ext}`),
this.destinationPath(`app/components/Form.${ext}`),
)
this.fs.move(
this.destinationPath(`_forms/${type}/LabeledTextField.${ext}`),
this.destinationPath(`app/components/LabeledTextField.${ext}`),
)
this.fs.delete(this.destinationPath("_forms"))
this.fs.writeJSON(this.destinationPath("package.json"), pkg)
@@ -186,6 +177,7 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
if (code !== 0) spinners[spinners.length - 1].fail()
else {
spinners[spinners.length - 1].succeed()
this.packageInstallSuccess = true
}
}
resolve()
@@ -201,16 +193,18 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
}
// Ensure the generated files are formatted with the installed prettier version
const formattingSpinner = log.spinner(log.withBrand("Formatting your code")).start()
const prettierResult = runLocalNodeCLI("prettier --loglevel silent --write .")
if (prettierResult.status !== 0) {
formattingSpinner.fail(
chalk.yellow.bold(
"We had an error running Prettier, but don't worry your app will still run fine :)",
),
)
} else {
formattingSpinner.succeed()
if (this.packageInstallSuccess) {
const formattingSpinner = log.spinner(log.withBrand("Formatting your code")).start()
const prettierResult = runLocalNodeCLI("prettier --loglevel silent --write .")
if (prettierResult.status !== 0) {
formattingSpinner.fail(
chalk.yellow.bold(
"We had an error running Prettier, but don't worry your app will still run fine :)",
),
)
} else {
formattingSpinner.succeed()
}
}
} else {
console.log("") // New line needed

View File

@@ -47,8 +47,11 @@ export class PageGenerator extends Generator<PageGeneratorOptions> {
}
getModelNamesPath() {
const context = this.options.context ? `${this.options.context}/` : ""
return context + this.options.modelNames
const kebabCaseContext = this.options.context
? `${camelCaseToKebabCase(this.options.context)}/`
: ""
const kebabCaseModelNames = camelCaseToKebabCase(this.options.modelNames)
return kebabCaseContext + kebabCaseModelNames
}
getTargetDirectory() {

View File

@@ -1,5 +1,5 @@
import React from "react"
import { Link } from "blitz"
import { Link, useMutation } from "blitz"
import { LabeledTextField } from "app/components/LabeledTextField"
import { Form, FORM_ERROR } from "app/components/Form"
import login from "app/auth/mutations/login"
@@ -10,6 +10,8 @@ type LoginFormProps = {
}
export const LoginForm = (props: LoginFormProps) => {
const [loginMutation] = useMutation(login)
return (
<div>
<h1>Login</h1>
@@ -20,7 +22,7 @@ export const LoginForm = (props: LoginFormProps) => {
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
try {
await login({ email: values.email, password: values.password })
await loginMutation(values)
props.onSuccess?.()
} catch (error) {
if (error.name === "AuthenticationError") {

View File

@@ -1,4 +1,5 @@
import React from "react"
import { useMutation } from "blitz"
import { LabeledTextField } from "app/components/LabeledTextField"
import { Form, FORM_ERROR } from "app/components/Form"
import signup from "app/auth/mutations/signup"
@@ -9,6 +10,8 @@ type SignupFormProps = {
}
export const SignupForm = (props: SignupFormProps) => {
const [signupMutation] = useMutation(signup)
return (
<div>
<h1>Create an Account</h1>
@@ -19,7 +22,7 @@ export const SignupForm = (props: SignupFormProps) => {
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
try {
await signup({ email: values.email, password: values.password })
await signupMutation(values)
props.onSuccess?.()
} catch (error) {
if (error.code === "P2002" && error.meta?.target?.includes("email")) {

Some files were not shown because too many files have changed in this diff Show More