1
0
mirror of synced 2026-02-07 03:00:10 -05:00

Compare commits

..

59 Commits
bug ... protect

Author SHA1 Message Date
Brandon Bayer
d5e5ca87ef change protect to return authenticated session ctx 2020-10-02 21:03:45 -04:00
Brandon Bayer
03f860ee6f Merge branch 'canary' into protect 2020-10-02 20:58:49 -04:00
Brandon Bayer
7bc8a249b4 Make ctx.session.authorize() a type guard (#1222)
(minor)
2020-10-02 20:55:51 -04:00
Brandon Bayer
1e3d306eb5 Merge branch 'canary' into protect 2020-10-02 20:46: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
0656c94885 more fixes 2020-10-02 18:31:09 -04:00
Brandon Bayer
d1f2e624e9 more 2020-10-02 18:19:24 -04:00
Brandon Bayer
e4d646a643 more stuff 2020-10-02 18:16:40 -04:00
Brandon Bayer
a8a8325176 change templates 2020-10-02 18:00:59 -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
135 changed files with 2171 additions and 1593 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,60 @@
"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"
]
}
],
"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-128-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,20 @@ 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>
</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,9 @@
import {Suspense} from "react"
import {Head, Link, useSession, useRouterQuery} from "blitz"
import getUser from "app/users/queries/getUser"
import {Head, Link, useSession, useRouterQuery, useMutation, invoke} from "blitz"
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 +11,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 +49,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 +69,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 SessionFormProps = {
initialValues: any
onSubmit: React.FormEventHandler<HTMLFormElement>
}
const SessionForm = ({initialValues, onSubmit}: SessionFormProps) => {
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 SessionForm

View File

@@ -1,16 +0,0 @@
import {SessionContext} from "blitz"
import db, {SessionCreateArgs} from "db"
type CreateSessionInput = {
data: SessionCreateArgs["data"]
}
export default async function createSession(
{data}: CreateSessionInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const session = await db.session.create({data})
return session
}

View File

@@ -1,17 +0,0 @@
import {SessionContext} from "blitz"
import db, {SessionDeleteArgs} from "db"
type DeleteSessionInput = {
where: SessionDeleteArgs["where"]
}
export default async function deleteSession(
{where}: DeleteSessionInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const session = await db.session.delete({where})
return session
}

View File

@@ -1,18 +0,0 @@
import {SessionContext} from "blitz"
import db, {SessionUpdateArgs} from "db"
type UpdateSessionInput = {
where: SessionUpdateArgs["where"]
data: SessionUpdateArgs["data"]
}
export default async function updateSession(
{where, data}: UpdateSessionInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const session = await db.session.update({where, data})
return session
}

View File

@@ -1,60 +0,0 @@
import React, {Suspense} from "react"
import Layout from "app/layouts/Layout"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import getSession from "app/sessions/queries/getSession"
import deleteSession from "app/sessions/mutations/deleteSession"
export const Session = () => {
const router = useRouter()
const sessionId = useParam("sessionId", "number")
const [session] = useQuery(getSession, {where: {id: sessionId}})
return (
<div>
<h1>Session {session.id}</h1>
<pre>{JSON.stringify(session, null, 2)}</pre>
<Link href="/sessions/[sessionId]/edit" as={`/sessions/${session.id}/edit`}>
<a>Edit</a>
</Link>
<button
type="button"
onClick={async () => {
if (window.confirm("This will be deleted")) {
await deleteSession({where: {id: session.id}})
router.push("/sessions")
}
}}
>
Delete
</button>
</div>
)
}
const ShowSessionPage: BlitzPage = () => {
return (
<div>
<Head>
<title>Session</title>
</Head>
<main>
<p>
<Link href="/sessions">
<a>Sessions</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<Session />
</Suspense>
</main>
</div>
)
}
ShowSessionPage.getLayout = (page) => <Layout>{page}</Layout>
export default ShowSessionPage

View File

@@ -1,68 +0,0 @@
import React, {Suspense} from "react"
import Layout from "app/layouts/Layout"
import {Head, Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
import getSessions from "app/sessions/queries/getSessions"
const ITEMS_PER_PAGE = 100
export const SessionsList = () => {
const router = useRouter()
const page = Number(router.query.page) || 0
const [{sessions, hasMore}] = usePaginatedQuery(getSessions, {
orderBy: {id: "asc"},
skip: ITEMS_PER_PAGE * page,
take: ITEMS_PER_PAGE,
})
const goToPreviousPage = () => router.push({query: {page: page - 1}})
const goToNextPage = () => router.push({query: {page: page + 1}})
return (
<div>
<ul>
{sessions.map((session) => (
<li key={session.id}>
<Link href="/sessions/[sessionId]" as={`/sessions/${session.id}`}>
<a>{session.name}</a>
</Link>
</li>
))}
</ul>
<button disabled={page === 0} onClick={goToPreviousPage}>
Previous
</button>
<button disabled={!hasMore} onClick={goToNextPage}>
Next
</button>
</div>
)
}
const SessionsPage: BlitzPage = () => {
return (
<div>
<Head>
<title>Sessions</title>
</Head>
<main>
<h1>Sessions</h1>
<p>
<Link href="/sessions/new">
<a>Create Session</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<SessionsList />
</Suspense>
</main>
</div>
)
}
SessionsPage.getLayout = (page) => <Layout>{page}</Layout>
export default SessionsPage

View File

@@ -1,21 +0,0 @@
import {NotFoundError, SessionContext} from "blitz"
import db, {FindOneSessionArgs} from "db"
type GetSessionInput = {
where: FindOneSessionArgs["where"]
// Only available if a model relationship exists
// include?: FindOneSessionArgs['include']
}
export default async function getSession(
{where /* include */}: GetSessionInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const session = await db.session.findOne({where})
if (!session) throw new NotFoundError()
return session
}

View File

@@ -1,35 +0,0 @@
import {SessionContext} from "blitz"
import db, {FindManySessionArgs} from "db"
type GetSessionsInput = {
where?: FindManySessionArgs["where"]
orderBy?: FindManySessionArgs["orderBy"]
skip?: FindManySessionArgs["skip"]
take?: FindManySessionArgs["take"]
// Only available if a model relationship exists
// include?: FindManySessionArgs['include']
}
export default async function getSessions(
{where, orderBy, skip = 0, take}: GetSessionsInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const sessions = await db.session.findMany({
where,
orderBy,
take,
skip,
})
const count = await db.session.count()
const hasMore = typeof take === "number" ? skip + take < count : false
const nextPage = hasMore ? {take, skip: skip + take!} : null
return {
sessions,
nextPage,
hasMore,
}
}

View File

@@ -1,10 +1,16 @@
import db, {UserCreateArgs} from "db"
import {protect} from "blitz"
import db from "db"
import * as z from "zod"
type CreateUserInput = {
data: UserCreateArgs["data"]
}
export default async function createUser({data}: CreateUserInput, ctx: Record<any, any> = {}) {
const user = await db.user.create({data})
export default protect(
{
schema: z.object({
name: z.string(),
}),
},
async function createUser(input, {session}) {
const user = await db.user.create({data: input})
return user
}
return user
},
)

View File

@@ -1,11 +1,16 @@
import db, {UserDeleteArgs} from "db"
import {protect} from "blitz"
import db from "db"
import * as z from "zod"
type DeleteUserInput = {
where: UserDeleteArgs["where"]
}
export default protect(
{
schema: z.object({
id: z.number(),
}),
},
async function deleteUser({id}, {session}) {
const user = await db.user.delete({where: {id}})
export default async function deleteUser({where}: DeleteUserInput, ctx: Record<any, any> = {}) {
const user = await db.user.delete({where})
return user
}
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 +1,17 @@
import db, {UserUpdateArgs} from "db"
import {protect} from "blitz"
import db from "db"
import * as z from "zod"
type UpdateUserInput = {
where: UserUpdateArgs["where"]
data: UserUpdateArgs["data"]
}
export default protect(
{
schema: z.object({
id: z.number(),
name: z.string(),
}),
},
async function updateUser({id, ...data}, {session}) {
const user = await db.user.update({where: {id}, data})
export default async function updateUser(
{where, data}: UpdateUserInput,
ctx: Record<any, any> = {},
) {
const user = await db.user.update({where, data})
return user
}
return user
},
)

View File

@@ -1,29 +1,28 @@
import React, {Suspense} from "react"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import Layout from "app/layouts/Layout"
import {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}})
const [user] = useQuery(getUser, {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>
}
<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}})
await deleteUser({id: user.id})
router.push("/users")
}
}}
@@ -37,26 +36,19 @@ export const User = () => {
const ShowUserPage: BlitzPage = () => {
return (
<div>
<Head>
<title>User</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<p>
<Link href="/users">
<a>Users</a>
</Link>
</p>
<main>
<p>
{
<Link href="/users">
<a>Users</a>
</Link>
}
</p>
<Suspense fallback={<div>Loading...</div>}>
<User />
</Suspense>
</main>
<Suspense fallback={<div>Loading...</div>}>
<User />
</Suspense>
</div>
)
}
ShowUserPage.getLayout = (page) => <Layout title={"User"}>{page}</Layout>
export default ShowUserPage

View File

@@ -1,5 +1,6 @@
import React, {Suspense} from "react"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import Layout from "app/layouts/Layout"
import {Link, useRouter, useQuery, useMutation, 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"
@@ -7,7 +8,8 @@ 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}})
const [user, {mutate}] = useQuery(getUser, {id: userId})
const [updateUserMutation] = useMutation(updateUser)
return (
<div>
@@ -18,11 +20,11 @@ export const EditUser = () => {
initialValues={user}
onSubmit={async () => {
try {
const updated = await updateUser({
where: {id: user.id},
data: {name: "MyNewName"},
const updated = await updateUserMutation({
id: user.id,
name: "MyNewName",
})
mutate(updated)
await mutate(updated)
alert("Success!" + JSON.stringify(updated))
router.push("/users/[userId]", `/users/${updated.id}`)
} catch (error) {
@@ -38,26 +40,19 @@ export const EditUser = () => {
const EditUserPage: BlitzPage = () => {
return (
<div>
<Head>
<title>Edit User</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Suspense fallback={<div>Loading...</div>}>
<EditUser />
</Suspense>
<main>
<Suspense fallback={<div>Loading...</div>}>
<EditUser />
</Suspense>
<p>
{
<Link href="/users">
<a>Users</a>
</Link>
}
</p>
</main>
<p>
<Link href="/users">
<a>Users</a>
</Link>
</p>
</div>
)
}
EditUserPage.getLayout = (page) => <Layout title={"Edit User"}>{page}</Layout>
export default EditUserPage

View File

@@ -1,49 +1,60 @@
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"
import {Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
import getUsers from "app/users/queries/getUsers"
const ITEMS_PER_PAGE = 100
export const UsersList = () => {
const [users] = useQuery(getUsers, {orderBy: {id: "desc"}})
const router = useRouter()
const page = Number(router.query.page) || 0
const [{users, hasMore}] = usePaginatedQuery(getUsers, {
orderBy: {id: "asc"},
skip: ITEMS_PER_PAGE * page,
take: ITEMS_PER_PAGE,
})
const goToPreviousPage = () => router.push({query: {page: page - 1}})
const goToNextPage = () => router.push({query: {page: page + 1}})
return (
<ul>
{users?.map((user) => (
<li key={user.id}>
<Link href="/users/[userId]" as={`/users/${user.id}`}>
<a>{user.email}</a>
</Link>
</li>
))}
</ul>
<div>
<ul>
{users.map((user) => (
<li key={user.id}>
<Link href="/users/[userId]" as={`/users/${user.id}`}>
<a>{user.name}</a>
</Link>
</li>
))}
</ul>
<button disabled={page === 0} onClick={goToPreviousPage}>
Previous
</button>
<button disabled={!hasMore} onClick={goToNextPage}>
Next
</button>
</div>
)
}
const UsersPage: BlitzPage = () => {
return (
<Layout>
<Head>
<title>Users</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div>
<p>
<Link href="/users/new">
<a>Create User</a>
</Link>
</p>
<main>
<h1>Users</h1>
<p>
{
<Link href="/users/new">
<a>Create User</a>
</Link>
}
</p>
<Suspense fallback={<div>Loading...</div>}>
<UsersList />
</Suspense>
</main>
</Layout>
<Suspense fallback={<div>Loading...</div>}>
<UsersList />
</Suspense>
</div>
)
}
UsersPage.getLayout = (page) => <Layout title={"Users"}>{page}</Layout>
export default UsersPage

View File

@@ -1,44 +1,39 @@
import React from "react"
import {Head, Link, useRouter, BlitzPage} from "blitz"
import Layout from "app/layouts/Layout"
import {Link, useRouter, useMutation, BlitzPage} from "blitz"
import createUser from "app/users/mutations/createUser"
import UserForm from "app/users/components/UserForm"
const NewUserPage: BlitzPage = () => {
const router = useRouter()
const [createUserMutation] = useMutation(createUser)
return (
<div>
<Head>
<title>New User</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1>Create New User</h1>
<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>
<UserForm
initialValues={{}}
onSubmit={async () => {
try {
const user = await createUserMutation({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>
</main>
}}
/>
<p>
<Link href="/users">
<a>Users</a>
</Link>
</p>
</div>
)
}
NewUserPage.getLayout = (page) => <Layout title={"Create New User"}>{page}</Layout>
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,12 @@
import db, {FindOneUserArgs} from "db"
import {SessionContext, NotFoundError} from "blitz"
import {protect, NotFoundError} from "blitz"
import db, {FindFirstUserArgs} from "db"
type GetUserInput = {
where: FindOneUserArgs["where"]
select?: FindOneUserArgs["select"]
// Only available if a model relationship exists
// include?: FindOneUserArgs['include']
}
type GetUserInput = FindFirstUserArgs["where"]
export default async function getUser(
{where, select}: GetUserInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session?.authorize(["admin", "user"])
export default protect({}, async function getUser(input: GetUserInput, {session}) {
const user = await db.user.findFirst({where: input})
const user = await db.user.findOne({where, select})
if (!user) throw new NotFoundError(`User with id ${where.id} does not exist`)
if (!user) throw new NotFoundError()
return user
}
})

View File

@@ -1,29 +1,29 @@
import {protect} from "blitz"
import db, {FindManyUserArgs} from "db"
import {SessionContext} from "blitz"
type GetUsersInput = {
where?: FindManyUserArgs["where"]
orderBy?: FindManyUserArgs["orderBy"]
cursor?: FindManyUserArgs["cursor"]
take?: FindManyUserArgs["take"]
skip?: FindManyUserArgs["skip"]
// Only available if a model relationship exists
// include?: FindManyUserArgs['include']
}
type GetUsersInput = Pick<FindManyUserArgs, "orderBy" | "skip" | "take">
export default async function getUsers(
{where, orderBy, cursor, take, skip}: GetUsersInput,
ctx: {session?: SessionContext} = {},
export default protect({}, async function getUsers(
{orderBy, skip = 0, take}: GetUsersInput,
{session},
) {
ctx.session!.authorize(["admin"])
const users = await db.user.findMany({
where,
where: {
// add your selection criteria here
},
orderBy,
cursor,
take,
skip,
})
return users
}
const count = await db.user.count()
const hasMore = typeof take === "number" ? skip + take < count : false
const nextPage = hasMore ? {take, skip: skip + take!} : null
return {
users,
nextPage,
hasMore,
count,
}
})

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

@@ -43,3 +43,27 @@ model Session {
publicData String?
privateData String?
}
model User {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/auth",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.0",
"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.0",
"final-form": "4.20.1",
"passport-auth0": "1.3.3",
"passport-github2": "0.1.11",
@@ -45,7 +48,7 @@
"react-error-boundary": "2.3.1",
"react-final-form": "6.5.1",
"secure-password": "4.0.0",
"zod": "1.10.0"
"zod": "1.11.9"
},
"devDependencies": {
"@cypress/skip-test": "2.5.0",

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.0",
"scripts": {
"start": "blitz start",
"build": "blitz build",
@@ -26,7 +26,7 @@
]
},
"dependencies": {
"blitz": "0.23.1-canary.0",
"blitz": "0.24.0-canary.0",
"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.0",
"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.0",
"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

@@ -1,6 +1,6 @@
{
"name": "@examples/store",
"version": "0.23.1-canary.0",
"version": "0.24.0-canary.0",
"private": true,
"scripts": {
"build": "blitz db migrate && blitz build",
@@ -18,7 +18,7 @@
"dependencies": {
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"blitz": "0.24.0-canary.0",
"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.0",
"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.0",
"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.0",
"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,8 +133,9 @@
"ts-jest": "24.3.0",
"tsdx": "0.13.3",
"tslib": "1.11.1",
"typescript": "3.8.3",
"wait-on": "4.0.2"
"typescript": "4.0.3",
"wait-on": "4.0.2",
"zod": "1.11.9"
},
"husky": {
"hooks": {

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.0",
"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.0",
"@blitzjs/core": "0.24.0-canary.0",
"@blitzjs/generator": "0.24.0-canary.0",
"@blitzjs/installer": "0.24.0-canary.0",
"@blitzjs/server": "0.24.0-canary.0",
"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.0",
"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.0",
"@blitzjs/repl": "0.24.0-canary.0",
"@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.0",
"@blitzjs/installer": "0.24.0-canary.0",
"@blitzjs/server": "0.24.0-canary.0",
"@oclif/dev-cli": "1.22.2",
"@oclif/test": "1.2.5",
"@prisma/cli": "2.4.1",

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.0",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",

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.0",
"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.0",
"@blitzjs/display": "0.24.0-canary.0",
"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

@@ -0,0 +1,28 @@
import {Ctx} from "./middleware"
import {AuthenticatedSessionContext} from "./supertokens"
import {ZodSchema, infer as zInfer} from "zod"
export type ProtectArgs<T> = {schema?: T; authorize?: boolean | unknown}
interface AuthenticatedCtx extends Ctx {
session: AuthenticatedSessionContext
}
export const protect = <T extends ZodSchema<any, any>, U = zInfer<T>>(
{schema, authorize = true}: ProtectArgs<T>,
resolver: (args: U, ctx: AuthenticatedCtx) => any,
) => {
return (rawInput: U, ctx: Ctx) => {
if (authorize) {
const authorizeInput: any[] = ["superadmin", "SUPERADMIN"]
if (Array.isArray(authorize)) {
authorizeInput.push(...authorize)
} else if (typeof authorize !== "boolean") {
authorizeInput.push(authorize)
}
;(ctx as any).session.authorize(authorizeInput)
}
const input = schema ? schema.parse(rawInput) : rawInput
return resolver(input, ctx as AuthenticatedCtx)
}
}

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,11 +1,12 @@
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-query-hooks"
export {useMutation} from "./use-mutation"
export {invoke, invokeWithMiddleware} from "./invoke"
export {getQueryKey, invalidateQuery} from "./utils/react-query-utils"
export {protect} from "./authorization"
export * from "./use-params"
export * from "./use-infinite-query"
export * from "./ssr-query"
export * from "./rpc"
export * from "./with-router"
export * from "./use-router"

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,88 @@
import {
QueryFn,
FirstParam,
PromiseReturnType,
Resolver,
EnhancedResolver,
EnhancedResolverRpcClient,
} from "./types"
import {isClient} from "./utils"
import {IncomingMessage, ServerResponse} from "http"
import {baseLogger, log as displayLog, chalk} from "@blitzjs/display"
import prettyMs from "pretty-ms"
import {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
MiddlewareResponse,
Middleware,
} 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 type InvokeWithMiddlewareConfig = {
req: IncomingMessage
res: ServerResponse
middleware?: Middleware[]
[prop: string]: any
}
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

@@ -6,6 +6,8 @@ import {apiResolver} from "next/dist/next-server/server/api-utils"
import {BlitzApiRequest, BlitzApiResponse} from "."
import {Middleware, handleRequestWithMiddleware} from "./middleware"
const testIfNotWindows = process.platform === "win32" ? it.skip : it
describe("handleRequestWithMiddleware", () => {
it("works without await", async () => {
const middleware: Middleware[] = [
@@ -21,7 +23,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 +42,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,13 +61,14 @@ 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")
})
})
it("middleware can throw", async () => {
// Failing on windows for unknown reason
testIfNotWindows("middleware can throw", async () => {
console.log = jest.fn()
console.error = jest.fn()
const forbiddenMiddleware = jest.fn()
@@ -77,13 +80,14 @@ 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)
})
})
it("middleware can return error", async () => {
// Failing on windows for unknown reason
testIfNotWindows("middleware can return error", async () => {
console.log = jest.fn()
const forbiddenMiddleware = jest.fn()
const middleware: Middleware[] = [
@@ -94,7 +98,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)
})

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 {EnhancedResolver} from "./types"
export interface DefaultCtx {}
export interface Ctx extends DefaultCtx {}
export interface MiddlewareRequest extends BlitzApiRequest {
protocol?: string
@@ -37,12 +39,9 @@ export type ConnectMiddleware = (
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 +63,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 +89,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

@@ -76,7 +76,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 +113,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 +129,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) {
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,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,7 @@
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"
export const TOKEN_SEPARATOR = ";"
export const HANDLE_SEPARATOR = ":"
@@ -27,14 +27,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 +49,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 +70,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 +112,7 @@ export const useSession = () => {
return subscription.unsubscribe
}, [])
return {...publicData, isLoading}
return {...publicData, isLoading} as PublicData & {isLoading: boolean}
}
/*

View File

@@ -1,7 +1,9 @@
import {Middleware} from "./middleware"
/**
* 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 +15,73 @@ 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>
// 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
}
}

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,47 @@
import {
useMutation as useReactQueryMutation,
MutationResult,
MutationConfig,
MutateConfig,
} from "react-query"
import {validateQueryFn} from "./utils/react-query-utils"
/*
* 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 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>
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,6 +1,7 @@
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
@@ -48,39 +49,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,128 @@
import {queryCache, QueryKey} from "react-query"
import {serialize} from "superjson"
import {Resolver, EnhancedResolverRpcClient, QueryFn} from "../types"
import {isServer, isClient} from "."
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
window.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,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 +1,21 @@
import {getQueryCacheFunctions} from "../src/utils/query-cache"
import {getQueryCacheFunctions} from "../src/utils/react-query-utils"
import {queryCache} from "react-query"
jest.mock("react-query")
describe("getQueryCacheFunctions", () => {
it("returns a mutate function with working options", () => {
it("returns a mutate function with working options", async () => {
window.requestIdleCallback = jest.fn((fn) => {
fn({} as any)
})
const spyRefetchQueries = jest.spyOn(queryCache, "invalidateQueries")
const {mutate} = getQueryCacheFunctions("testQueryKey")
expect(mutate).toBeTruthy()
mutate({newData: true})
await mutate({newData: true})
expect(spyRefetchQueries).toBeCalledTimes(1)
mutate({newData: true}, {refetch: false})
await mutate({newData: true}, {refetch: false})
expect(spyRefetchQueries).toBeCalledTimes(1)
mutate({newData: true}, {refetch: true})
await mutate({newData: true}, {refetch: true})
expect(spyRefetchQueries).toBeCalledTimes(2)
})
})

View File

@@ -1,6 +1,4 @@
import {executeRpcCall, getIsomorphicRpcHandler} from "@blitzjs/core"
global.fetch = jest.fn(() => Promise.resolve({json: () => ({result: null, error: null})}))
import {executeRpcCall, getIsomorphicEnhancedResolver} from "@blitzjs/core"
declare global {
namespace NodeJS {
@@ -10,11 +8,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 +32,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 +60,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.0",
"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: "black"},
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.0",
"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.0",
"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.0",
"@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")) {

View File

@@ -1,15 +1,15 @@
import { SessionContext } from "blitz"
import { protect } from "blitz"
import { authenticateUser } from "app/auth/auth-utils"
import { LoginInput, LoginInputType } from "../validations"
export default async function login(input: LoginInputType, ctx: { session?: SessionContext } = {}) {
// This throws an error if input is invalid
const { email, password } = LoginInput.parse(input)
import { LoginInput } from "../validations"
export default protect({ schema: LoginInput, authorize: false }, async function login(
{ email, password },
{ session }
) {
// 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
}
})

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