1
0
mirror of synced 2026-02-03 18:01:02 -05:00

Compare commits

...

163 Commits

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

* fix the bug

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

* fix test

* add comment (patch)
2020-09-28 16:54:27 -04:00
Brandon Bayer
c43967984b Add @Kamshak as a contributor 2020-09-28 11:28:52 -04:00
Rudi Yardley
e47d947dc0 Fix bug in dev where files were not being removed correctly when deleted (#1161)
(patch)
2020-09-28 11:06:05 -04:00
Satoshi Nitawaki
ffb54ec064 Add .node-version (#1148)
(meta)
2020-09-28 11:04:46 -04:00
Brandon Bayer
08abc33494 v0.23.2-canary.0 2020-09-26 22:21:14 -04:00
Brandon Bayer
34722f952c 🔥 Add useMutation() and strong typing for ctx and ctx.session (#1160)
(major)
2020-09-26 22:12:29 -04:00
Brandon Bayer
4003b8ac01 Big refactor of internal types for react-query, RPC, and resolvers (#1172)
(meta)
2020-09-26 21:19:19 -04:00
allcontributors[bot]
160b5fc062 docs: add brunocrosier as a contributor (#1159)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-24 20:29:01 -04:00
Bruno Crosier
b722c39f79 (newapp) Add "restart server" to instruction to index page (#1140)
Co-authored-by: Brandon Bayer <b@bayer.ws>
2020-09-24 20:28:14 -04:00
allcontributors[bot]
8da7bd7cd4 docs: add jorisre as a contributor (#1158)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Co-authored-by: Brandon Bayer <b@bayer.ws> (meta)
2020-09-24 20:26:00 -04:00
Joris
712cb172eb Fix: pass undefined through the RPC layer (#1156) 2020-09-24 20:24:57 -04:00
Brandon Bayer
d623d8cc33 v0.23.1-canary.0 2020-09-23 22:26:52 -04:00
Brandon Bayer
5ca38ca6d8 Fix ES5 compatibility: upgrade superjson to 1.2.2 (#1147)
(patch)
2020-09-23 22:22:08 -04:00
Brandon Bayer
db0128c971 Upgrade react-query to 2.23.0 (#945)
(patch)
2020-09-23 22:14:29 -04:00
Mohamed Shaban
856aa53843 Fix blitz console not closing on first Ctrl+D (#1142)
Co-authored-by: Fran Zekan <zekan.fran369@gmail.com>
Co-authored-by: Brandon Bayer <b@bayer.ws> (patch)
2020-09-23 21:57:13 -04:00
Fran Zekan
f9c3f82b27 Add blitz db seed for seeding data in your database (#678)
Co-authored-by: Brandon Bayer <b@bayer.ws> (minor)
2020-09-23 21:42:32 -04:00
allcontributors[bot]
97d5f396de docs: add drmas as a contributor (#1146)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-23 21:38:33 -04:00
Rudi Yardley
760fba77ab Speed up blitz start by 40% by adding incremental dev cache (#1137)
(patch)
2020-09-23 21:35:18 -04:00
Brandon Bayer
f83ce0bd72 Redirect MAINTAINERS.md to the new page in the docs 2020-09-23 21:07:15 -04:00
Brandon Bayer
7425952616 (newapp) Improve Form typing: remove need to pass schema type (#1144) 2020-09-23 19:55:27 -04:00
Brandon Bayer
7fac0134b3 Add @kitze as a contributor 2020-09-23 10:24:38 -04:00
Brandon Bayer
df46939fe3 back out no-for-of eslint rule
(meta)
2020-09-22 19:45:11 -04:00
Brandon Bayer
9e88b11496 v0.23.0 2020-09-22 19:28:46 -04:00
Ricardo Trejos
c427ae23d4 Add a timeout to the "retrieving freshest dependencies" step for blitz new(#1132)
(patch)
2020-09-21 20:58:32 -04:00
Enrico Schaaf
cb849c5ba9 Fix blitz autocomplete adding alpha warning message to the shell config (#1122)
(patch)
2020-09-21 19:35:34 -04:00
अभिनाश (Avinash)
6e777dd72a add lint rule to prevent Object.fromEntries in core package (#1129)
(meta)
2020-09-21 19:34:16 -04:00
Brandon Bayer
80ab4876f2 Improve error message for missing SESSION_SECRET_KEY (#1136)
(patch)
2020-09-21 16:00:59 -04:00
Brandon Bayer
cd0bf1f970 (newapp) Change signup & login to lowerCase emails before saving and verifying (#1135) 2020-09-21 14:56:38 -04:00
allcontributors[bot]
5f5a5c8ef7 docs: add enricoschaaf as a contributor (#1128)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-19 22:27:51 -04:00
Enrico Schaaf
9a1e0d0de7 Update Taiwind recipe to opt in to future Tailwind CSS changes (#1118)
(recipe)
2020-09-19 22:27:34 -04:00
Brandon Bayer
fb44203510 v0.22.3-canary.0 2020-09-19 22:01:38 -04:00
Brandon Bayer
010057b34c Fix ES5 compatibility (#1124)
(patch)
2020-09-19 21:10:50 -04:00
Brandon Bayer
575e862ae3 Remove prisma client generation from blitz in favor of new package.json schema config (Requires Prisma 2.7.0+ & new field in pkg.json)) (#1121)
(major)
2020-09-19 19:30:03 -04:00
Satoshi Nitawaki
aed6b8875a Fix slack channel name in MAINTAINERS.md (#1117)
(meta)
2020-09-19 10:59:40 -04:00
Satoshi Nitawaki
10b6f859fd Fix blitz new spewing out all the database migration logs (#1116)
(patch)
2020-09-19 10:54:26 -04:00
Brandon Bayer
9ac856c0ee Update @cardotrejos as a contributor 2020-09-19 10:50:09 -04:00
अभिनाश (Avinash)
1ff7f36482 Enable automatic network request cancelation for queries/mutations (#1092)
Co-authored-by: Brandon Bayer <b@bayer.ws> (minor)
2020-09-19 10:47:58 -04:00
Brandon Bayer
df150da37e Fix cli cache file to be in temp directory instead of project directory (#1087)
(patch)
2020-09-19 10:45:37 -04:00
Bhanu Teja Pachipulusu
eb7409c0b3 Add --force flag for blitz db reset command (#1107)
(minor)
2020-09-19 10:44:22 -04:00
Brandon Bayer
1411a1d366 🎉 Add Satoshi Nitawaki as L1 Maintainer (#1119)
(meta)
2020-09-19 10:35:59 -04:00
allcontributors[bot]
ec95cb40de docs: add hardfire as a contributor (#1113)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-18 18:15:07 -04:00
Brandon Bayer
b502d7ea1e Add dotenv-flow to jest config for new apps (#1112)
(newapp)
2020-09-18 18:08:03 -04:00
allcontributors[bot]
74bf2a9e4c docs: add doeixd as a contributor (#1109)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-18 15:36:03 -04:00
Brandon Bayer
1ee637c367 Revert "add dotenv-flow to jest setup file for new apps"
This reverts commit 23a33f1c3d.
2020-09-18 15:15:41 -04:00
Brandon Bayer
c08771b57e Revert "move it to jest config"
This reverts commit 8a468e4f79.
2020-09-18 15:15:01 -04:00
Brandon Bayer
8a468e4f79 move it to jest config 2020-09-18 15:14:31 -04:00
Brandon Bayer
23a33f1c3d add dotenv-flow to jest setup file for new apps 2020-09-18 15:12:37 -04:00
Brandon Bayer
b3767861a2 Fix inability to have a /test page (#1111)
(patch)
2020-09-18 13:46:56 -04:00
Patrick G
65acfff0ff Add test, tests, spec, specs to build hash ignore list (#1102)
Co-authored-by: Brandon Bayer <b@bayer.ws> (patch)
2020-09-18 11:23:45 -04:00
Brandon Bayer
ab3fc26409 Add @sergiodxa as a contributor 2020-09-18 11:11:05 -04:00
Brandon Bayer
7b7039e0e3 bump CI cache key
(ignore)
2020-09-18 11:05:04 -04:00
engelkes-finstreet
e93f24452c Fix blitz generate pages to use kebab-case for all url paths (#1089)
(patch)
2020-09-18 10:21:00 -04:00
Brandon Bayer
63c9375331 add "recipe" and "newapp" type to release patch
(meta)
2020-09-18 10:01:57 -04:00
Brandon Bayer
adfb486004 Tweaks to new app template pkg scripts: add tsc to pre-commit and lint to pre-push (#1104)
(minor)
2020-09-18 09:58:04 -04:00
Brandon Bayer
5828736369 Improve Recipe for Render.com (#1108)
(recipe)
2020-09-18 09:53:25 -04:00
Brandon Bayer
40a93ee62d Add @tsriram as a contributor 2020-09-18 09:52:42 -04:00
Brandon Bayer
8fa82c7661 Revert "Tweaks to new app template pkg scripts: add tsc to pre-commit and lint to pre-push"
This reverts commit 141003df89.
2020-09-17 16:46:01 -04:00
Brandon Bayer
141003df89 Tweaks to new app template pkg scripts: add tsc to pre-commit and lint to pre-push 2020-09-17 16:44:10 -04:00
Brandon Bayer
6ef7b8a2de Improve rendering of errors in generated Final Form <LabeledTextField> (#1095)
(patch)
2020-09-16 17:09:06 -04:00
allcontributors[bot]
6ade33b849 docs: add taylorcjohnson as a contributor (#1093)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-16 09:23:56 -04:00
Taylor Johnson
02d3aa8259 Change the new app schema provider to be just sqlite instead of an array (#1091)
(patch)
2020-09-16 09:23:11 -04:00
Brandon Bayer
1ae2bb3ee3 v0.22.2-canary.0 2020-09-14 22:07:30 -04:00
Brandon Bayer
d84c73d2bb Add @ditorojuan as a contributor 2020-09-14 22:00:56 -04:00
Brandon Bayer
89b55971f1 Add @johnletey as a contributor 2020-09-14 22:00:37 -04:00
Brandon Bayer
fd1856bc7b Add @cktang88 as a contributor 2020-09-14 21:59:28 -04:00
Brandon Bayer
e6dbbababb Add @johncantrell97 as a contributor 2020-09-14 21:59:19 -04:00
Brandon Bayer
3e2b5ddc8e Add @JoseRFelix as a contributor 2020-09-14 21:59:03 -04:00
Brandon Bayer
1b974a0371 Add @obii-bit as a contributor 2020-09-14 21:58:20 -04:00
engelkes-finstreet
8bf9667a15 Fix blitz generate query does not preserve plurality (#1083)
(patch)
2020-09-14 21:50:03 -04:00
yuta0801
94bd4c166c Fix an incorrect error message in blitz start (#1064) 2020-09-14 20:26:55 -04:00
allcontributors[bot]
4ad9f9bdc9 docs: add yuta0801 as a contributor (#1086)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-14 20:25:47 -04:00
yuta0801
13e1526ef5 Add --inspect flag to enable Node.js inspector (#1063)
(minor)
2020-09-14 20:25:36 -04:00
Brandon Bayer
ab3021a371 Significantly improve CLI performance: reduce execute time from 1700ms -> 900ms (#1057)
(patch)
2020-09-14 20:15:50 -04:00
allcontributors[bot]
3e506c1dce docs: add xiaoyu-tamu as a contributor (#1085)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (patch)
2020-09-14 20:13:27 -04:00
Michael Li
85356ca8e8 Fix type error for useQuery/usePaginatedQuery options object (#1059)
(patch)
2020-09-14 20:13:00 -04:00
Brandon Bayer
530ce851e4 Add React Bricks as Seedling Sponsor!
(meta)
2020-09-14 12:13:07 -04:00
Brandon Bayer
d953ef795a v0.22.1 2020-09-12 14:03:09 -04:00
Brandon Bayer
7fcb0945a2 Fix bug in default _app.tsx where Links inside ErrorBoundary don't work (#1054)
(patch)
2020-09-12 13:54:49 -04:00
Brandon Bayer
ab4670c21b v0.22.0 2020-09-11 17:48:30 -04:00
Brandon Bayer
1ae7bf77b2 Tiny tweak to Layout component in new apps (#1051)
(minor)
2020-09-11 17:46:35 -04:00
Brandon Bayer
63e3fe1ccb Add link to signup page to login form for new apps (#1050)
(minor)
2020-09-11 17:03:14 -04:00
Jirka Svoboda
280a2b5c4f refactor(server): Added isTypescript explicit flag, config refactored (#1014)
Co-authored-by: Rudi Yardley <contact@rudiyardley.com> (meta)
2020-09-11 11:16:45 -04:00
Brandon Bayer
bf2734d907 v0.21.2-canary.2 2020-09-11 11:10:32 -04:00
Sigurd Moland Wahl
07341c14d3 Fix bug where test files would cause blitz build to fail (#1045)
(patch)
2020-09-11 11:07:46 -04:00
allcontributors[bot]
e150b67cf4 docs: add PixelsCommander as a contributor (#1048)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-11 11:04:47 -04:00
Denis Radin
1a1722168c Add secureProxy option the the Passport.js adapter (#1033)
(minor)
2020-09-11 11:04:33 -04:00
dependabot[bot]
7f266b0c98 Bump node-fetch from 2.6.0 to 2.6.1 in /packages/generator (#1042)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (meta)
2020-09-11 09:35:50 -04:00
engelkes-finstreet
6e92a2dfde Update dependencies in new app template (#1036)
(patch)
2020-09-10 22:17:43 -04:00
Ante Primorac
66cd1ec650 Fix: export AppProps as a generic type (#1034)
(patch)
2020-09-10 22:16:51 -04:00
allcontributors[bot]
7c4916324e docs: add engelkes-finstreet as a contributor (#1041)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-10 22:12:32 -04:00
engelkes-finstreet
d747e34853 Add ability to set custom authenticateOptions for the Passport.js adapter (#1024)
(minor)
2020-09-10 22:10:37 -04:00
Brandon Bayer
3afab440c8 Add @sirmyron as a contributor 2020-09-10 22:05:04 -04:00
allcontributors[bot]
e576e6332c docs: add nitaking as a contributor (#1040)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-10 22:03:49 -04:00
Satoshi Nitawaki
60d0c9d0bf Enable passing extra prisma args to blitz db migrate(#1012)
(minor)
2020-09-10 22:03:34 -04:00
Brandon Bayer
8f800d388b Fix UnhandledPromiseRejectionWarning from useQuery during static pre-rendering (#1038)
(patch)
2020-09-10 09:59:38 -04:00
196 changed files with 3751 additions and 2190 deletions

View File

@@ -353,11 +353,12 @@
"profile": "https://github.com/ntgussoni",
"contributions": [
"test",
"code"
"code",
"review"
]
},
{
"login": "skn0tt",
"login": "Skn0tt",
"name": "Simon Knott",
"avatar_url": "https://avatars1.githubusercontent.com/u/14912729?v=4",
"profile": "http://simonknott.de",
@@ -565,15 +566,6 @@
"code"
]
},
{
"login": "jletey",
"name": "John Letey",
"avatar_url": "https://avatars1.githubusercontent.com/u/62398724?v=4",
"profile": "https://github.com/jletey",
"contributions": [
"code"
]
},
{
"login": "pixelmord",
"name": "Andreas Adam",
@@ -896,7 +888,8 @@
"avatar_url": "https://avatars1.githubusercontent.com/u/8602086?v=4",
"profile": "http://ricardotrejos.tech",
"contributions": [
"code"
"code",
"doc"
]
},
{
@@ -979,7 +972,8 @@
"avatar_url": "https://avatars2.githubusercontent.com/u/37571416?v=4",
"profile": "https://github.com/clgeoio",
"contributions": [
"code"
"code",
"test"
]
},
{
@@ -990,6 +984,293 @@
"contributions": [
"doc"
]
},
{
"login": "nitaking",
"name": "Satoshi Nitawaki",
"avatar_url": "https://avatars2.githubusercontent.com/u/10850034?v=4",
"profile": "https://twitter.com/nitaking_",
"contributions": [
"code",
"maintenance",
"question"
]
},
{
"login": "sirmyron",
"name": "sirmyron",
"avatar_url": "https://avatars2.githubusercontent.com/u/1430136?v=4",
"profile": "https://github.com/sirmyron",
"contributions": [
"doc"
]
},
{
"login": "engelkes-finstreet",
"name": "engelkes-finstreet",
"avatar_url": "https://avatars1.githubusercontent.com/u/36962022?v=4",
"profile": "https://github.com/engelkes-finstreet",
"contributions": [
"doc",
"code"
]
},
{
"login": "PixelsCommander",
"name": "Denis Radin",
"avatar_url": "https://avatars2.githubusercontent.com/u/810671?v=4",
"profile": "http://twitter.com/pixelscommander",
"contributions": [
"review",
"code",
"doc"
]
},
{
"login": "xiaoyu-tamu",
"name": "Michael Li",
"avatar_url": "https://avatars3.githubusercontent.com/u/33362998?v=4",
"profile": "https://github.com/xiaoyu-tamu",
"contributions": [
"code"
]
},
{
"login": "yuta0801",
"name": "yuta0801",
"avatar_url": "https://avatars2.githubusercontent.com/u/21266306?v=4",
"profile": "https://github.com/yuta0801",
"contributions": [
"code"
]
},
{
"login": "Obii-bit",
"name": "Obadja Ris",
"avatar_url": "https://avatars2.githubusercontent.com/u/67339820?v=4",
"profile": "https://github.com/Obii-bit",
"contributions": [
"doc"
]
},
{
"login": "JoseRFelix",
"name": "Jose Felix ",
"avatar_url": "https://avatars2.githubusercontent.com/u/21092519?v=4",
"profile": "http://jfelix.info",
"contributions": [
"code"
]
},
{
"login": "johncantrell97",
"name": "John Cantrell",
"avatar_url": "https://avatars3.githubusercontent.com/u/41305919?v=4",
"profile": "https://github.com/johncantrell97",
"contributions": [
"code"
]
},
{
"login": "cktang88",
"name": "Kwuang Tang",
"avatar_url": "https://avatars1.githubusercontent.com/u/10319942?v=4",
"profile": "http://kwuang.me",
"contributions": [
"code"
]
},
{
"login": "johnletey",
"name": "John Letey",
"avatar_url": "https://avatars1.githubusercontent.com/u/62398724?v=4",
"profile": "https://github.com/johnletey",
"contributions": [
"code"
]
},
{
"login": "ditorojuan",
"name": "Juan Di Toro",
"avatar_url": "https://avatars0.githubusercontent.com/u/22530892?v=4",
"profile": "https://github.com/ditorojuan",
"contributions": [
"code"
]
},
{
"login": "taylorcjohnson",
"name": "Taylor Johnson",
"avatar_url": "https://avatars0.githubusercontent.com/u/10552296?v=4",
"profile": "https://github.com/taylorcjohnson",
"contributions": [
"code",
"doc"
]
},
{
"login": "tsriram",
"name": "Sriram Thiagarajan",
"avatar_url": "https://avatars3.githubusercontent.com/u/450559?v=4",
"profile": "https://twitter.com/tsriram",
"contributions": [
"doc"
]
},
{
"login": "sergiodxa",
"name": "Sergio Xalambrí",
"avatar_url": "https://avatars2.githubusercontent.com/u/1312018?v=4",
"profile": "https://sergiodxa.com",
"contributions": [
"doc"
]
},
{
"login": "doeixd",
"name": "Patrick G",
"avatar_url": "https://avatars3.githubusercontent.com/u/13461122?v=4",
"profile": "https://github.com/doeixd",
"contributions": [
"code"
]
},
{
"login": "hardfire",
"name": "अभिनाश (Avinash)",
"avatar_url": "https://avatars3.githubusercontent.com/u/513457?v=4",
"profile": "http://avinash.com.np",
"contributions": [
"code"
]
},
{
"login": "enricoschaaf",
"name": "Enrico Schaaf",
"avatar_url": "https://avatars1.githubusercontent.com/u/54645197?v=4",
"profile": "http://enricoschaaf.com",
"contributions": [
"code"
]
},
{
"login": "kitze",
"name": "Kitze",
"avatar_url": "https://avatars0.githubusercontent.com/u/1160594?v=4",
"profile": "http://kitze.io",
"contributions": [
"ideas"
]
},
{
"login": "drmas",
"name": "Mohamed Shaban",
"avatar_url": "https://avatars3.githubusercontent.com/u/644440?v=4",
"profile": "https://github.com/drmas",
"contributions": [
"code"
]
},
{
"login": "jorisre",
"name": "Joris",
"avatar_url": "https://avatars1.githubusercontent.com/u/7545547?v=4",
"profile": "https://github.com/jorisre",
"contributions": [
"code"
]
},
{
"login": "Kamshak",
"name": "Valentin Funk",
"avatar_url": "https://avatars3.githubusercontent.com/u/337968?v=4",
"profile": "https://github.com/Kamshak",
"contributions": [
"doc"
]
},
{
"login": "lukebennett",
"name": "Luke Bennett",
"avatar_url": "https://avatars1.githubusercontent.com/u/135390?v=4",
"profile": "https://lukebennett.com",
"contributions": [
"code"
]
},
{
"login": "hmajid2301",
"name": "Haseeb Majid",
"avatar_url": "https://avatars0.githubusercontent.com/u/998807?v=4",
"profile": "https://haseebmajid.dev",
"contributions": [
"code"
]
},
{
"login": "phillippschmedt",
"name": "Phillipp Schmedt",
"avatar_url": "https://avatars0.githubusercontent.com/u/16028406?v=4",
"profile": "https://github.com/phillippschmedt",
"contributions": [
"code"
]
},
{
"login": "hasparus",
"name": "Piotr Monwid-Olechnowicz",
"avatar_url": "https://avatars0.githubusercontent.com/u/15332326?v=4",
"profile": "https://haspar.us",
"contributions": [
"code"
]
},
{
"login": "mizchi",
"name": "Kotaro Chikuba",
"avatar_url": "https://avatars2.githubusercontent.com/u/73962?v=4",
"profile": "https://mizchi.dev",
"contributions": [
"code",
"test"
]
},
{
"login": "konradkalemba",
"name": "Konrad Kalemba",
"avatar_url": "https://avatars0.githubusercontent.com/u/8682104?v=4",
"profile": "https://github.com/konradkalemba",
"contributions": [
"code"
]
},
{
"login": "Alucard17",
"name": "Alucard17",
"avatar_url": "https://avatars1.githubusercontent.com/u/26205172?v=4",
"profile": "https://github.com/Alucard17",
"contributions": [
"code"
]
},
{
"login": "Dohxis",
"name": "Domantas Mauruča",
"avatar_url": "https://avatars2.githubusercontent.com/u/8768909?v=4",
"profile": "https://github.com/Dohxis",
"contributions": [
"test",
"code"
]
},
{
"login": "sandulat",
"name": "Stratulat Alexandru",
"avatar_url": "https://avatars0.githubusercontent.com/u/7345874?v=4",
"profile": "https://sandulat.com/",
"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: [

View File

@@ -32,9 +32,9 @@ jobs:
**/node_modules
/home/runner/.cache/Cypress
C:\Users\runneradmin\AppData\Local\Cypress\Cache
key: ${{ runner.os }}-yarn-v2-${{ hashFiles('yarn.lock') }}
key: ${{ runner.os }}-yarn-v3-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-v2-
${{ runner.os }}-yarn-v3-
- name: Install dependencies
run: yarn install --frozen-lockfile --silent
env:

1
.gitignore vendored
View File

@@ -19,3 +19,4 @@ dist
**/.env.*.local
**/.envrc
.blitz-*
.blitz-cli-cache

1
.node-version Normal file
View File

@@ -0,0 +1 @@
12.16.1

View File

@@ -1,104 +1 @@
# ❤️ Blitz Maintainers
Aside from the core team, there are two levels of maintainers, described below.
## Becoming a Maintainer
We always need more level 1 maintainers! The main requirement is that you can show empathy when communicating online. We'll train you as needed on the other specifics. **This is a great role if you have limited time, because you can spend just as much time as you have without any ongoing responsibilities (unlike level 2)**
Level 2 maintainers have a much higher responsibility, so usually you will spend time as a level 1 maintainer before moving to level 2.
Please DM a core team member (Brandon Bayer, Rudi Yardley, or Dylan Brookes) in Slack if you're interested in becoming an official maintainer!
## Level 1 Maintainers
Level 1 maintainers are critical for a healthy Blitz community and project. They take a lot of burden off the core team and level 2 maintainers so they can focus on higher level things with longer term impact.
The primary responsibilities of level 1 maintainers are:
- Being a friendly, welcoming voice for the Blitz community
- Issue triage
- Pull request triage
- Monitor and answer the `#-help` slack channel
- Community encouragement
- Community moderation
- Tracking and ensuring progress of key issues
## Level 2 Maintainers
Level 2 maintainers are the backbone of the project. They are watchdogs over the code, ensuring code quality, correctness, and security. They also facilitate a rapid pace of progress.
The primary responsibilities of level 2 maintainers are:
- Code ownership over specific parts of the project
- Maintaining and improving the architecture of what they own
- Final pull request reviews
- Merging pull requests
- Tracking and ensuring progress of open pull requests
## ⚠️ Fundamentals
Maintainers are the face of the project and the front-line touch point for the community. Therefore maintainers have the very important responsibility of making people feel welcome, valued, understood, and appreciated.
**Please take time to read and understand everything outlined in this [guide on building welcoming communities](https://opensource.guide/building-community)**
Some especially important points:
- **Gratitude:** immediately express gratitude when someone opens an issue or PR. This takes effort/time and we appreciate it
- **Responsiveness:** during issue/PR triage, even if we cant do a full review right away, leave a comment thanking them and saying well review it soon
- **Understanding:** it's critical to ensure you understand exactly what someone is saying before you respond. Ask plenty of questions if needed. It's very bad if someone has to reply to your response and say "actually I was asking about X"
- In fact, at least one question is almost always required before you can respond appropriately  — whether in Github or in Slack
## Slack
- All `#-*` channels are for Blitz users
- All `#dev-*` channels are for Blitz internal development
If someone that's not a maintainer post in the wrong area, that's fine. Don't tell them they posted in the wrong place. But as a maintainer, you should for sure post in the right channel :)
## Issue Triage
#### If a bug report:
- Does it have enough information? Versions? Logs? Some way to reproduce?
- Has this already been fixed in a previous release?
- Is there already an existing issue for this?
### If a feature/change request:
- Is it clear what the request is and what the benefit will be?
- Is this an obvious win for Blitz? Then accept it
- If not obvious, then pull in a core team member or level 2 maintainer for more review
### Actions
1. Add tags:
- Add a `kind/*` tag
- Add a `scope/*` tag
- Add a `status/*` tag
- Add a good first/second issue tag if appropriate
## Pull Request Triage
- Are the changes covered by tests?
- Do the changes look ok? Make sure there's no obvious issues
### Actions
1. Kindly request any changes if needed
2. Else add a Github approval so that level 2 maintainers know it's already had an initial review
## Final PR Review & Merging (Level 2 maintainers)
As a level 2 maintainer, it is your responsibility to make sure broken code and regressions never reach the canary branch.
1. Ensure the PR'ed code fully works as intended and that there are no regressions in related code
1. If not fully covered by automated tests, you need to pull down the code locally and manually verify everything (the Github CLI helps with this!)
2. During squash & merge:
1. Change the commit title to be public friendly - this exact text will go in the release notes
2. Add the commit type in the description, in parenthesis like `(patch)`. Commit types:
- `major` - major breaking change
- `minor` - minor feature addition
- `patch` - patches, bug fixes, perf improvements, etc
- `example` - change to an example app
- `meta` - internal meta change related to the Blitz repo/project
This document has moved here: https://blitzjs.com/docs/maintainers

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-103-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-133-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">
@@ -116,23 +116,35 @@ Your financial contributions help ensure Blitz continues to be developed and mai
👉 View options and contribute at [GitHub Sponsors](https://github.com/sponsors/blitz-js), [PayPal](https://paypal.me/thebayers), or [Open Collective](https://opencollective.com/blitzjs)
### 🌱 Seedling Sponsors
<a aria-label="React Bricks" href="https://reactbricks.com/?utm_source=blitzjs&utm_medium=sponsorship&utm_campaign=blitzjs_sponsorship">
<img alt="" src="https://reactbricks.com/reactbricks_icon.svg" width="30px">
</a>
### 🥉 Bronze Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="100px">
</a>
### 🥈 Silver Sponsors
<a aria-label="Fauna" href="https://dashboard.fauna.com/accounts/register?utm_source=BlitzJS&utm_medium=sponsorship&utm_campaign=BlitzJS_Sponsorship_2020">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/Fauna_Logo_Blue.png" width="175px">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/Fauna_Logo_Blue.png" width="200px">
</a>
### 🏆 Gold Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="300px">
</a>
### 💎 Diamond Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="400px">
</a>
<br>
@@ -162,6 +174,7 @@ _Code ownership, pull request approvals and merging, etc_ (see [MAINTAINERS.md](
<tr>
<td align="center"><a href="https://github.com/aem"><img src="https://avatars0.githubusercontent.com/u/1909883?v=4" width="100px;" alt=""/><br /><sub><b>Adam Markon</b></sub></a><br />CLI</td>
<td align="center"><a href="http://robdrosenberg.com"><img src="https://avatars0.githubusercontent.com/u/20813991?v=4" width="100px;" alt=""/><br /><sub><b>Robert Rosenberg</b></sub></a><br />Website/Docs</td>
<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 />SuperJSON</td>
</tr>
</table>
<!-- markdownlint-enable -->
@@ -180,7 +193,6 @@ _Issue triage, pull request triage, community encouragement and moderation, etc_
<tr>
<td align="center"><a href="https://github.com/LoriKarikari"><img src="https://avatars1.githubusercontent.com/u/7902980?v=4" width="100px;" alt=""/><br /><sub><b>Lori Karikari</b></sub></a></td>
<td align="center"><a href="https://corey-brown.com"><img src="https://avatars1.githubusercontent.com/u/12791148?v=4" width="100px;" alt=""/><br /><sub><b>Corey Brown</b></sub></a></td>
<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></td>
<td align="center"><a href="http://jeremyliberman.com/"><img src="https://avatars3.githubusercontent.com/u/2754163?v=4" width="100px;" alt=""/><br /><sub><b>Jeremy Liberman</b></td>
<td align="center"><a href="http://jagascript.com"><img src="https://avatars0.githubusercontent.com/u/4562878?v=4" width="100px;" alt=""/><br /><sub><b>Jaga Santagostino</b></sub></a></td>
<td align="center"><a href="https://simonpeterdebbarma.com"><img src="https://avatars3.githubusercontent.com/u/31207418?v=4" width="100px;" alt=""/><br /><sub><b>Simon Debbarma</b></sub></a></td>
@@ -188,6 +200,7 @@ _Issue triage, pull request triage, community encouragement and moderation, etc_
</tr>
<tr>
<td align="center"><a href="https://twitter.com/ivandevp"><img src="https://avatars3.githubusercontent.com/u/9284690?v=4" width="100px;" alt=""/><br /><sub><b>Ivan Medina</b></sub></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></td>
</tr>
</table>
<!-- markdownlint-enable -->
@@ -247,10 +260,10 @@ 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>
<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>
<td align="center"><a href="http://jagascript.com"><img src="https://avatars0.githubusercontent.com/u/4562878?v=4" width="100px;" alt=""/><br /><sub><b>Jaga Santagostino</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kandros" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=kandros" title="Documentation">📖</a> <a href="#maintenance-kandros" title="Maintenance">🚧</a></td>
<td align="center"><a href="http://www.joaoportela.com"><img src="https://avatars0.githubusercontent.com/u/1010018?v=4" width="100px;" alt=""/><br /><sub><b>João Portela</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jportela" title="Code">💻</a></td>
<td align="center"><a href="http://dajin.dev"><img src="https://avatars0.githubusercontent.com/u/7122182?v=4" width="100px;" alt=""/><br /><sub><b>Da-Jin Chu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dajinchu" title="Code">💻</a></td>
@@ -278,70 +291,107 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
</tr>
<tr>
<td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4" width="100px;" alt=""/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=pgrimaud" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jletey"><img src="https://avatars1.githubusercontent.com/u/62398724?v=4" width="100px;" alt=""/><br /><sub><b>John Letey</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jletey" title="Code">💻</a></td>
<td align="center"><a href="https://pixelmord.github.io"><img src="https://avatars2.githubusercontent.com/u/224168?v=4" width="100px;" alt=""/><br /><sub><b>Andreas Adam</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=pixelmord" title="Code">💻</a></td>
<td align="center"><a href="https://kevo.dev"><img src="https://avatars3.githubusercontent.com/u/15717067?v=4" width="100px;" alt=""/><br /><sub><b>Kevin Tovar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kevotovar" title="Code">💻</a></td>
<td align="center"><a href="http://anteprimorac.com.hr"><img src="https://avatars0.githubusercontent.com/u/972083?v=4" width="100px;" alt=""/><br /><sub><b>Ante Primorac</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=anteprimorac" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=anteprimorac" title="Documentation">📖</a></td>
<td align="center"><a href="http://mykalmachon.dev"><img src="https://avatars1.githubusercontent.com/u/7844994?v=4" width="100px;" alt=""/><br /><sub><b>Mykal Machon</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=MykalMachon" title="Code">💻</a></td>
<td align="center"><a href="https://jamiedavenport.dev"><img src="https://avatars2.githubusercontent.com/u/1329874?v=4" width="100px;" alt=""/><br /><sub><b>Jamie Davenport</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jamiedavenport" title="Code">💻</a></td>
<td align="center"><a href="https://cloudnweb.dev/"><img src="https://avatars0.githubusercontent.com/u/17050715?v=4" width="100px;" alt=""/><br /><sub><b>GaneshMani</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ganeshmani" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://cloudnweb.dev/"><img src="https://avatars0.githubusercontent.com/u/17050715?v=4" width="100px;" alt=""/><br /><sub><b>GaneshMani</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ganeshmani" title="Code">💻</a></td>
<td align="center"><a href="http://ramonmorcillo.com"><img src="https://avatars3.githubusercontent.com/u/31936665?v=4" width="100px;" alt=""/><br /><sub><b>reymon359</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=reymon359" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/gregory-vasquez-96413b184/"><img src="https://avatars1.githubusercontent.com/u/36422346?v=4" width="100px;" alt=""/><br /><sub><b>gvasquez11</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=gvasquez11" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/josemiguelo"><img src="https://avatars1.githubusercontent.com/u/15330034?v=4" width="100px;" alt=""/><br /><sub><b> José Miguel Ochoa</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=josemiguelo" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/osirvent"><img src="https://avatars2.githubusercontent.com/u/5927133?v=4" width="100px;" alt=""/><br /><sub><b>Oscar Sirvent</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=osirvent" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=osirvent" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/donni106"><img src="https://avatars0.githubusercontent.com/u/1942953?v=4" width="100px;" alt=""/><br /><sub><b>Daniel Molnar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=donni106" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=donni106" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/exclipy"><img src="https://avatars1.githubusercontent.com/u/508799?v=4" width="100px;" alt=""/><br /><sub><b>Kevin Wu Won</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=exclipy" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/tehnuge"><img src="https://avatars1.githubusercontent.com/u/1928236?v=4" width="100px;" alt=""/><br /><sub><b>John Duong</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tehnuge" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/tehnuge"><img src="https://avatars1.githubusercontent.com/u/1928236?v=4" width="100px;" alt=""/><br /><sub><b>John Duong</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tehnuge" title="Code">💻</a></td>
<td align="center"><a href="https://noahfleischmann.com"><img src="https://avatars0.githubusercontent.com/u/23707137?v=4" width="100px;" alt=""/><br /><sub><b>Noah Fleischmann</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=fnoah" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/toshi1127"><img src="https://avatars3.githubusercontent.com/u/32378535?v=4" width="100px;" alt=""/><br /><sub><b>Matsumoto Toshi</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=toshi1127" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=toshi1127" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/simonedelmann"><img src="https://avatars2.githubusercontent.com/u/2821076?v=4" width="100px;" alt=""/><br /><sub><b>Simon Edelmann</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=simonedelmann" title="Code">💻</a></td>
<td align="center"><a href="https://shaun.church"><img src="https://avatars3.githubusercontent.com/u/571764?v=4" width="100px;" alt=""/><br /><sub><b>Shaun Church</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=shaunchurch" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=shaunchurch" title="Code">💻</a></td>
<td align="center"><a href="https://styfle.dev"><img src="https://avatars1.githubusercontent.com/u/229881?v=4" width="100px;" alt=""/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=styfle" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/SigurdMW"><img src="https://avatars3.githubusercontent.com/u/6359003?v=4" width="100px;" alt=""/><br /><sub><b>Sigurd Moland Wahl</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=SigurdMW" title="Code">💻</a></td>
<td align="center"><a href="https://brianandrews.dev/"><img src="https://avatars1.githubusercontent.com/u/6384100?v=4" width="100px;" alt=""/><br /><sub><b>Brian Andrews</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sbardian" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://brianandrews.dev/"><img src="https://avatars1.githubusercontent.com/u/6384100?v=4" width="100px;" alt=""/><br /><sub><b>Brian Andrews</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sbardian" title="Documentation">📖</a></td>
<td align="center"><a href="http://garrisonsnelling.com"><img src="https://avatars0.githubusercontent.com/u/5100597?v=4" width="100px;" alt=""/><br /><sub><b>Garrison Snelling</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=garrisons" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/tylangesmith"><img src="https://avatars1.githubusercontent.com/u/22609577?v=4" width="100px;" alt=""/><br /><sub><b>Ty Lange-Smith</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tylangesmith" title="Code">💻</a></td>
<td align="center"><a href="https://rubenmoya.dev"><img src="https://avatars3.githubusercontent.com/u/905225?v=4" width="100px;" alt=""/><br /><sub><b>Rubén Moya</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=rubenmoya" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=rubenmoya" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/robertgrzonka"><img src="https://avatars0.githubusercontent.com/u/35585466?v=4" width="100px;" alt=""/><br /><sub><b>robertgrzonka</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=robertgrzonka" title="Code">💻</a> <a href="#infra-robertgrzonka" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/agoxlea"><img src="https://avatars3.githubusercontent.com/u/1240841?v=4" width="100px;" alt=""/><br /><sub><b>Alex Orr</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=agoxlea" title="Code">💻</a></td>
<td align="center"><a href="https://christse.io"><img src="https://avatars1.githubusercontent.com/u/250450?v=4" width="100px;" alt=""/><br /><sub><b>Chris Tse</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=chris-tse" title="Code">💻</a></td>
<td align="center"><a href="http://twitter.com/nettofarah"><img src="https://avatars1.githubusercontent.com/u/270688?v=4" width="100px;" alt=""/><br /><sub><b>Netto Farah</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nettofarah" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/nettofarah"><img src="https://avatars1.githubusercontent.com/u/270688?v=4" width="100px;" alt=""/><br /><sub><b>Netto Farah</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nettofarah" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/rohanjulka19"><img src="https://avatars0.githubusercontent.com/u/19673968?v=4" width="100px;" alt=""/><br /><sub><b>Rohan Julka</b></sub></a><br /><a href="#infra-rohanjulka19" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://www.ivansantos.me"><img src="https://avatars3.githubusercontent.com/u/301291?v=4" width="100px;" alt=""/><br /><sub><b>Ivan Santos</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=pragmaticivan" title="Code">💻</a></td>
<td align="center"><a href="https://able.bio"><img src="https://avatars0.githubusercontent.com/u/12991390?v=4" width="100px;" alt=""/><br /><sub><b>Soumyajit Pathak</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=drenther" title="Code">💻</a></td>
<td align="center"><a href="http://www.sebastiankurpiel.com"><img src="https://avatars2.githubusercontent.com/u/16307737?v=4" width="100px;" alt=""/><br /><sub><b>Sebastian Kurpiel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=SebastianKurp" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/scisteffan"><img src="https://avatars2.githubusercontent.com/u/2676185?v=4" width="100px;" alt=""/><br /><sub><b>Steffan</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=scisteffan" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=scisteffan" title="Documentation">📖</a> <a href="#financial-scisteffan" title="Financial">💵</a></td>
<td align="center"><a href="https://github.com/kripod"><img src="https://avatars3.githubusercontent.com/u/14854048?v=4" width="100px;" alt=""/><br /><sub><b>Kristóf Poduszló</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kripod" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Weilbyte"><img src="https://avatars1.githubusercontent.com/u/43392677?v=4" width="100px;" alt=""/><br /><sub><b>Weilbyte</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Weilbyte" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Weilbyte" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Weilbyte"><img src="https://avatars1.githubusercontent.com/u/43392677?v=4" width="100px;" alt=""/><br /><sub><b>Weilbyte</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Weilbyte" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Weilbyte" title="Documentation">📖</a></td>
<td align="center"><a href="http://ricardotrejos.tech"><img src="https://avatars1.githubusercontent.com/u/8602086?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Trejos</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cardotrejos" title="Code">💻</a></td>
<td align="center"><a href="http://ricardotrejos.tech"><img src="https://avatars1.githubusercontent.com/u/8602086?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Trejos</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cardotrejos" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=cardotrejos" title="Documentation">📖</a></td>
<td align="center"><a href="https://gkaragkiaouris.tech/"><img src="https://avatars0.githubusercontent.com/u/8822835?v=4" width="100px;" alt=""/><br /><sub><b>George Karagkiaouris</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=karaggeorge" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=karaggeorge" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/brady-pascoe-3bba6b13a/"><img src="https://avatars0.githubusercontent.com/u/18705892?v=4" width="100px;" alt=""/><br /><sub><b>Brady Pascoe</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=bpas247" title="Code">💻</a></td>
<td align="center"><a href="https://www.yeahcoach.com"><img src="https://avatars1.githubusercontent.com/u/761766?v=4" width="100px;" alt=""/><br /><sub><b>Jirka Svoboda</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=svobik7" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/alan2207"><img src="https://avatars3.githubusercontent.com/u/12713315?v=4" width="100px;" alt=""/><br /><sub><b>Alan Alickovic</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=alan2207" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=alan2207" title="Documentation">📖</a></td>
<td align="center"><a href="https://yngve.hoiseth.net"><img src="https://avatars0.githubusercontent.com/u/8469540?v=4" width="100px;" alt=""/><br /><sub><b>Yngve Høiseth</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=yhoiseth" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/bruno_crosier"><img src="https://avatars1.githubusercontent.com/u/18399089?v=4" width="100px;" alt=""/><br /><sub><b>Bruno Crosier</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=brunocrosier" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://twitter.com/bruno_crosier"><img src="https://avatars1.githubusercontent.com/u/18399089?v=4" width="100px;" alt=""/><br /><sub><b>Bruno Crosier</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=brunocrosier" title="Documentation">📖</a></td>
<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="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>
<td align="center"><a href="https://github.com/xiaoyu-tamu"><img src="https://avatars3.githubusercontent.com/u/33362998?v=4" width="100px;" alt=""/><br /><sub><b>Michael Li</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=xiaoyu-tamu" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/yuta0801"><img src="https://avatars2.githubusercontent.com/u/21266306?v=4" width="100px;" alt=""/><br /><sub><b>yuta0801</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=yuta0801" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Obii-bit"><img src="https://avatars2.githubusercontent.com/u/67339820?v=4" width="100px;" alt=""/><br /><sub><b>Obadja Ris</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Obii-bit" title="Documentation">📖</a></td>
<td align="center"><a href="http://jfelix.info"><img src="https://avatars2.githubusercontent.com/u/21092519?v=4" width="100px;" alt=""/><br /><sub><b>Jose Felix </b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=JoseRFelix" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/johncantrell97"><img src="https://avatars3.githubusercontent.com/u/41305919?v=4" width="100px;" alt=""/><br /><sub><b>John Cantrell</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=johncantrell97" title="Code">💻</a></td>
<td align="center"><a href="http://kwuang.me"><img src="https://avatars1.githubusercontent.com/u/10319942?v=4" width="100px;" alt=""/><br /><sub><b>Kwuang Tang</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cktang88" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/johnletey"><img src="https://avatars1.githubusercontent.com/u/62398724?v=4" width="100px;" alt=""/><br /><sub><b>John Letey</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=johnletey" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ditorojuan"><img src="https://avatars0.githubusercontent.com/u/22530892?v=4" width="100px;" alt=""/><br /><sub><b>Juan Di Toro</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ditorojuan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/taylorcjohnson"><img src="https://avatars0.githubusercontent.com/u/10552296?v=4" width="100px;" alt=""/><br /><sub><b>Taylor Johnson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=taylorcjohnson" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=taylorcjohnson" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/tsriram"><img src="https://avatars3.githubusercontent.com/u/450559?v=4" width="100px;" alt=""/><br /><sub><b>Sriram Thiagarajan</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tsriram" title="Documentation">📖</a></td>
<td align="center"><a href="https://sergiodxa.com"><img src="https://avatars2.githubusercontent.com/u/1312018?v=4" width="100px;" alt=""/><br /><sub><b>Sergio Xalambrí</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sergiodxa" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/doeixd"><img src="https://avatars3.githubusercontent.com/u/13461122?v=4" width="100px;" alt=""/><br /><sub><b>Patrick G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=doeixd" title="Code">💻</a></td>
<td align="center"><a href="http://avinash.com.np"><img src="https://avatars3.githubusercontent.com/u/513457?v=4" width="100px;" alt=""/><br /><sub><b>अभिनाश (Avinash)</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hardfire" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://enricoschaaf.com"><img src="https://avatars1.githubusercontent.com/u/54645197?v=4" width="100px;" alt=""/><br /><sub><b>Enrico Schaaf</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=enricoschaaf" title="Code">💻</a></td>
<td align="center"><a href="http://kitze.io"><img src="https://avatars0.githubusercontent.com/u/1160594?v=4" width="100px;" alt=""/><br /><sub><b>Kitze</b></sub></a><br /><a href="#ideas-kitze" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/drmas"><img src="https://avatars3.githubusercontent.com/u/644440?v=4" width="100px;" alt=""/><br /><sub><b>Mohamed Shaban</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=drmas" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jorisre"><img src="https://avatars1.githubusercontent.com/u/7545547?v=4" width="100px;" alt=""/><br /><sub><b>Joris</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jorisre" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Kamshak"><img src="https://avatars3.githubusercontent.com/u/337968?v=4" width="100px;" alt=""/><br /><sub><b>Valentin Funk</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Kamshak" title="Documentation">📖</a></td>
<td align="center"><a href="https://lukebennett.com"><img src="https://avatars1.githubusercontent.com/u/135390?v=4" width="100px;" alt=""/><br /><sub><b>Luke Bennett</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=lukebennett" title="Code">💻</a></td>
<td align="center"><a href="https://haseebmajid.dev"><img src="https://avatars0.githubusercontent.com/u/998807?v=4" width="100px;" alt=""/><br /><sub><b>Haseeb Majid</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hmajid2301" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/phillippschmedt"><img src="https://avatars0.githubusercontent.com/u/16028406?v=4" width="100px;" alt=""/><br /><sub><b>Phillipp Schmedt</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=phillippschmedt" title="Code">💻</a></td>
<td align="center"><a href="https://haspar.us"><img src="https://avatars0.githubusercontent.com/u/15332326?v=4" width="100px;" alt=""/><br /><sub><b>Piotr Monwid-Olechnowicz</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hasparus" title="Code">💻</a></td>
<td align="center"><a href="https://mizchi.dev"><img src="https://avatars2.githubusercontent.com/u/73962?v=4" width="100px;" alt=""/><br /><sub><b>Kotaro Chikuba</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mizchi" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=mizchi" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/konradkalemba"><img src="https://avatars0.githubusercontent.com/u/8682104?v=4" width="100px;" alt=""/><br /><sub><b>Konrad Kalemba</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=konradkalemba" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Alucard17"><img src="https://avatars1.githubusercontent.com/u/26205172?v=4" width="100px;" alt=""/><br /><sub><b>Alucard17</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Alucard17" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Dohxis"><img src="https://avatars2.githubusercontent.com/u/8768909?v=4" width="100px;" alt=""/><br /><sub><b>Domantas Mauruča</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Dohxis" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=Dohxis" title="Code">💻</a></td>
<td align="center"><a href="https://sandulat.com/"><img src="https://avatars0.githubusercontent.com/u/7345874?v=4" width="100px;" alt=""/><br /><sub><b>Stratulat Alexandru</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sandulat" 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

@@ -2,6 +2,7 @@ import {passportAuth} from "blitz"
import db from "db"
import {Strategy as TwitterStrategy} from "passport-twitter"
import {Strategy as GitHubStrategy} from "passport-github2"
import {Strategy as Auth0Strategy} from "passport-auth0"
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
@@ -16,8 +17,13 @@ assert(
assert(process.env.GITHUB_CLIENT_ID, "You must provide the GITHUB_CLIENT_ID env variable")
assert(process.env.GITHUB_CLIENT_SECRET, "You must provide the GITHUB_CLIENT_SECRET env variable")
assert(process.env.AUTH0_DOMAIN, "You must provide the AUTH0_DOMAIN env variable")
assert(process.env.AUTH0_CLIENT_ID, "You must provide the AUTH0_CLIENT_ID env variable")
assert(process.env.AUTH0_CLIENT_SECRET, "You must provide the AUTH0_CLIENT_SECRET env variable")
export default passportAuth({
successRedirectUrl: "/",
authenticateOptions: {scope: "openid email profile"}, //used for Auth0Strategy - without an empty profile is returned
strategies: [
new TwitterStrategy(
{
@@ -85,5 +91,41 @@ export default passportAuth({
done(null, {publicData})
},
),
new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL:
process.env.NODE_ENV === "production"
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/auth0/callback"
: "http://localhost:3000/api/auth/auth0/callback",
},
async function (_token, _tokenSecret, extraParams, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
// This can happen if you haven't enabled email access in your twitter app permissions
return done(new Error("GitHub OAuth response doesn't have email."))
}
const user = await db.user.upsert({
where: {email},
create: {
email,
name: profile.displayName,
},
update: {email},
})
const publicData = {
userId: user.id,
roles: [user.role],
source: "auth0",
githubUsername: profile.username,
}
done(undefined, {publicData})
},
),
],
})

View File

@@ -1,25 +1,25 @@
import React from "react"
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"
import {LoginInput, LoginInputType} from "app/auth/validations"
import {LoginInput} from "app/auth/validations"
type LoginFormProps = {
onSuccess?: () => void
}
export const LoginForm = (props: LoginFormProps) => {
const [loginMutation] = useMutation(login)
return (
<div>
<h1>Login</h1>
<Form<LoginInputType>
<Form
submitText="Log In"
schema={LoginInput}
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") {
@@ -36,6 +36,9 @@ export const LoginForm = (props: LoginFormProps) => {
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
</Form>
<div style={{marginTop: "1rem"}}>
Or <Link href="/signup">Sign Up</Link>
</div>
</div>
)
}

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,12 +1,12 @@
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"
import {SignupInput, SignupInputType} from "app/auth/validations"
import {SignupInput} from "app/auth/validations"
const SignupPage: BlitzPage = () => {
const router = useRouter()
const [signupMutation] = useMutation(signup)
return (
<>
@@ -18,12 +18,12 @@ const SignupPage: BlitzPage = () => {
<div>
<h1>Create an Account</h1>
<Form<SignupInputType>
<Form
submitText="Create Account"
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,28 +1,28 @@
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"
type FormProps<FormValues> = {
type FormProps<S extends z.ZodType<any, any>> = {
/** All your form fields */
children: ReactNode
/** Text to display in the submit button */
submitText: string
onSubmit: FinalFormProps<FormValues>["onSubmit"]
initialValues?: FinalFormProps<FormValues>["initialValues"]
schema?: z.ZodType<any, any>
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
schema?: S
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
export function Form<FormValues extends Record<string, unknown>>({
export function Form<S extends z.ZodType<any, any>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<FormValues>) {
}: FormProps<S>) {
return (
<FinalForm<FormValues>
<FinalForm
initialValues={initialValues}
validate={(values) => {
if (!schema) return

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

@@ -1,12 +1,18 @@
import {AppProps, ErrorComponent} from "blitz"
import {AppProps, ErrorComponent, useRouter} from "blitz"
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 (
<ErrorBoundary
FallbackComponent={RootErrorFallback}
resetKeys={[router.asPath]}
onReset={() => {
// This ensures the Blitz useQuery hooks will automatically refetch
// data any time you reset the error boundary

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/auth",
"version": "0.21.2-canary.1",
"version": "0.24.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,10 +36,11 @@
]
},
"dependencies": {
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.21.2-canary.1",
"@prisma/cli": "2.8.0",
"@prisma/client": "2.8.0",
"blitz": "0.24.0",
"final-form": "4.20.1",
"passport-auth0": "1.3.3",
"passport-github2": "0.1.11",
"passport-twitter": "1.0.4",
"react": "0.0.0-experimental-7f28234f8",
@@ -49,6 +53,7 @@
"devDependencies": {
"@cypress/skip-test": "2.5.0",
"@next/bundle-analyzer": "latest",
"@types/passport-auth0": "1.0.4",
"@types/passport-github2": "1.2.4",
"@types/passport-twitter": "1.0.36",
"@types/react": "16.9.38",

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.21.2-canary.1",
"version": "0.24.0",
"scripts": {
"start": "blitz start",
"build": "blitz build",
@@ -26,7 +26,7 @@
]
},
"dependencies": {
"blitz": "0.21.2-canary.1",
"blitz": "0.24.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.21.2-canary.1",
"version": "0.24.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.21.2-canary.1",
"blitz": "0.24.0",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8"
},

View File

@@ -6,14 +6,13 @@ import ProductForm from "app/products/components/ProductForm"
function Product() {
const router = useRouter()
const id = useParam("id", "number")
const [product, {mutate}] = useQuery(getProduct, {where: {id}})
const [product] = useQuery(getProduct, {where: {id}})
return (
<ProductForm
product={product}
onSuccess={(updatedProduct) => {
mutate(updatedProduct)
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

@@ -0,0 +1,30 @@
import db from "./index"
const randomString = (len: number, offset = 3) => {
let output = ""
for (let i = 0; i < len + Math.ceil((Math.random() - 0.5) * offset); i++) {
const ascii = Math.floor(Math.random() * 26) + (i % 2 === 0 ? 97 : 65)
output += String.fromCharCode(ascii)
}
return output
}
const randomProduct = () => {
return {
name: randomString(10),
handle: randomString(6, 0),
description: Array.from(new Array(10), () => randomString(10)).join(" "),
price: Math.floor(Math.random() * 10000),
}
}
const seed = async () => {
for (let i = 0; i < 5; i++) {
await db.product.create({data: randomProduct()})
}
await db.user.create({data: {email: "foo@bar.com", name: "Foobar"}})
}
export default seed

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "tailwind",
"version": "0.21.2-canary.1",
"version": "0.24.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.21.2-canary.1",
"blitz": "0.24.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.21.2-canary.1",
"version": "0.24.0",
"packages": ["packages/*"],
"npmClient": "yarn",
"useWorkspaces": true,

View File

@@ -81,12 +81,13 @@
"@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",
"babel-jest": "26.3.0",
"concurrently": "5.3.0",
"cpy-cli": "3.1.1",
"cross-env": "7.0.2",
"debug": "4.1.1",
@@ -94,6 +95,8 @@
"directory-tree": "2.2.4",
"eslint": "7.7.0",
"eslint-config-react-app": "5.2.1",
"eslint-plugin-es": "mysticatea/eslint-plugin-es",
"eslint-plugin-es5": "1.5.0",
"eslint-plugin-flowtype": "5.2.0",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-jsx-a11y": "6.3.1",
@@ -130,7 +133,7 @@
"ts-jest": "24.3.0",
"tsdx": "0.13.3",
"tslib": "1.11.1",
"typescript": "3.8.3",
"typescript": "4.0.3",
"wait-on": "4.0.2"
},
"husky": {

View File

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

View File

@@ -5,19 +5,24 @@ import chalk from "chalk"
import {parseSemver} from "../utils/parse-semver"
async function main() {
console.log(
chalk.yellow(
`You are using alpha software - if you have any problems, please open an issue here:
https://github.com/blitz-js/blitz/issues/new/choose\n`,
),
)
const options = require("minimist")(process.argv.slice(2))
if (options._[0] !== "autocomplete:script" || Object.keys(options).length > 1) {
console.log(
chalk.yellow(
`You are using alpha software - if you have any problems, please open an issue here:
https://github.com/blitz-js/blitz/issues/new/choose\n`,
),
)
}
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")
@@ -37,7 +42,6 @@ async function main() {
const cli = require(cliPkgPath)
const options = require("minimist")(process.argv.slice(2))
const hasVersionFlag = options._.length === 0 && (options.v || options.version)
const hasVerboseFlag = options._.length === 0 && (options.V || options.verbose)

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/cli",
"description": "Blitz.js CLI",
"version": "0.21.2-canary.1",
"version": "0.24.0",
"license": "MIT",
"scripts": {
"b": "./bin/run",
@@ -30,14 +30,15 @@
"/lib"
],
"dependencies": {
"@blitzjs/display": "0.21.2-canary.1",
"@blitzjs/repl": "0.21.2-canary.1",
"@blitzjs/display": "0.24.0",
"@blitzjs/repl": "0.24.0",
"@oclif/command": "1.5.20",
"@oclif/config": "1.15.1",
"@oclif/plugin-autocomplete": "0.2.0",
"@oclif/plugin-help": "2.2.3",
"@oclif/plugin-not-found": "1.2.3",
"@prisma/sdk": "2.6.0",
"@salesforce/lazy-require": "0.3.2",
"camelcase": "6.0.0",
"chalk": "4.0.0",
"cross-spawn": "7.0.3",
@@ -54,12 +55,13 @@
"rimraf": "3.0.2",
"tar": "6.0.2",
"ts-node": "8.9.0",
"tsconfig-paths": "3.9.0"
"tsconfig-paths": "3.9.0",
"v8-compile-cache": "2.1.1"
},
"devDependencies": {
"@blitzjs/generator": "0.21.2-canary.1",
"@blitzjs/installer": "0.21.2-canary.1",
"@blitzjs/server": "0.21.2-canary.1",
"@blitzjs/generator": "0.24.0",
"@blitzjs/installer": "0.24.0",
"@blitzjs/server": "0.24.0",
"@oclif/dev-cli": "1.22.2",
"@oclif/test": "1.2.5",
"@prisma/cli": "2.4.1",

View File

@@ -1,6 +1,5 @@
import {build as ServerBuild} from "@blitzjs/server"
import {Command} from "@oclif/command"
import {build} from "@blitzjs/server"
import {runPrismaGeneration} from "./db"
export class Build extends Command {
static description = "Create a production build"
@@ -12,7 +11,8 @@ export class Build extends Command {
}
try {
await build(config, runPrismaGeneration({silent: true, failSilently: true}))
const build: typeof ServerBuild = require("@blitzjs/server").build
await build(config)
} catch (err) {
console.error(err)
process.exit(1) // clean up?

View File

@@ -1,16 +1,7 @@
import {runRepl} from "@blitzjs/repl"
import {Command} from "@oclif/command"
import path from "path"
import fs from "fs"
import pkgDir from "pkg-dir"
import {log} from "@blitzjs/display"
import chalk from "chalk"
import {setupTsnode} from "../utils/setup-ts-node"
import {runPrismaGeneration} from "./db"
const projectRoot = pkgDir.sync() || process.cwd()
const isTypescript = fs.existsSync(path.join(projectRoot, "tsconfig.json"))
const projectRoot = require("pkg-dir").sync() || process.cwd()
const isTypescript = require("fs").existsSync(require("path").join(projectRoot, "tsconfig.json"))
export class Console extends Command {
static description = "Run the Blitz console REPL"
@@ -22,19 +13,17 @@ export class Console extends Command {
}
async run() {
const {log} = require("@blitzjs/display")
const chalk = require("chalk")
log.branded("You have entered the Blitz console")
console.log(chalk.yellow("Tips: - Exit by typing .exit or pressing Ctrl-D"))
console.log(chalk.yellow(" - Use your db like this: await db.project.findMany()"))
console.log(chalk.yellow(" - Use your queries/mutations like this: await getProjects({})"))
const spinner = log.spinner("Loading your code").start()
if (isTypescript) {
setupTsnode()
require("../utils/setup-ts-node").setupTsnode()
}
await runPrismaGeneration({silent: true, failSilently: true})
spinner.succeed()
runRepl(Console.replOptions)
require("@blitzjs/repl").runRepl(Console.replOptions)
}
}

View File

@@ -1,17 +1,7 @@
import {resolveBinAsync} from "@blitzjs/server"
import {log} from "@blitzjs/display"
import {Command, flags} from "@oclif/command"
import chalk from "chalk"
import {spawn} from "cross-spawn"
import {prompt} from "enquirer"
import * as fs from "fs"
import * as path from "path"
import {promisify} from "util"
import {projectRoot} from "../utils/get-project-root"
import pEvent from "p-event"
import {getConfig, getSchema} from "@prisma/sdk"
import {log} from "@blitzjs/display"
const getPrismaBin = () => resolveBinAsync("@prisma/cli", "prisma")
const getPrismaBin = () => require("@blitzjs/server").resolveBinAsync("@prisma/cli", "prisma")
let prismaBin: string
let schemaArg: string
@@ -26,11 +16,11 @@ const runPrisma = async (args: string[], silent = false) => {
}
}
const cp = spawn(prismaBin, args, {
const cp = require("cross-spawn").spawn(prismaBin, args, {
stdio: silent ? "ignore" : "inherit",
env: process.env,
})
const code = await pEvent(cp, "exit", {rejectionEvents: []})
const code = await require("p-event")(cp, "exit", {rejectionEvents: []})
return code === 0
}
@@ -53,8 +43,8 @@ export const runPrismaGeneration = async ({silent = false, failSilently = false}
}
}
const runMigrateUp = async ({silent = false} = {}) => {
const args = ["migrate", "up", schemaArg, "--create-db", "--experimental"]
const runMigrateUp = async ({silent = false} = {}, schemaArgLocal = schemaArg) => {
const args = ["migrate", "up", schemaArgLocal, "--create-db", "--experimental"]
if (process.env.NODE_ENV === "production" || silent) {
args.push("--auto-approve")
@@ -69,16 +59,17 @@ const runMigrateUp = async ({silent = false} = {}) => {
return runPrismaGeneration({silent})
}
export const runMigrate = async (name?: string) => {
export const runMigrate = async (flags: object = {}, schemaArgLocal = schemaArg) => {
if (process.env.NODE_ENV === "production") {
return runMigrateUp()
return runMigrateUp({}, schemaArgLocal)
}
// @ts-ignore escape:TS7053
const nestedFlags = Object.keys(flags).map((key) => [`--${key}`, flags[key]])
const options = ([] as string[]).concat(...nestedFlags)
const silent = Boolean(name)
const args = ["migrate", "save", schemaArg, "--create-db", "--experimental"]
if (name) {
args.push("--name", name)
}
const silent = options.includes("--name")
const args = ["migrate", "save", schemaArgLocal, "--create-db", "--experimental", ...options]
const success = await runPrisma(args, silent)
@@ -86,7 +77,7 @@ export const runMigrate = async (name?: string) => {
throw new Error("Migration failed")
}
return runMigrateUp({silent})
return runMigrateUp({silent}, schemaArgLocal)
}
export async function resetPostgres(connectionString: string, db: any): Promise<void> {
@@ -131,8 +122,12 @@ export async function resetMysql(connectionString: string, db: any): Promise<voi
export async function resetSqlite(connectionString: string): Promise<void> {
const relativePath = connectionString.replace(/^file:/, "").replace(/^(?:\.\.[\\/])+/, "")
const dbPath = path.join(projectRoot, "db", relativePath)
const unlink = promisify(fs.unlink)
const dbPath = require("path").join(
require("../utils/get-project-root").projectRoot,
"db",
relativePath,
)
const unlink = require("util").promisify(require("fs").unlink)
try {
// delete database from folder
await unlink(dbPath)
@@ -153,20 +148,66 @@ export function getDbName(connectionString: string): string {
return dbName
}
async function runSeed() {
const projectRoot = require("../utils/get-project-root").projectRoot
const seedPath = require("path").join(projectRoot, "db/seeds")
const dbPath = require("path").join(projectRoot, "db/index")
log.branded("Seeding database")
let spinner = log.spinner("Loading seeds\n").start()
let seeds: Function | undefined
try {
seeds = require(seedPath).default
if (seeds === undefined) {
throw new Error(`Cant find default export from db/seeds`)
}
} catch (err) {
log.error(`Couldn't import default from db/seeds.ts or db/seeds/index.ts file`)
throw err
}
spinner.succeed()
spinner = log.spinner("Checking for database migrations\n").start()
await runMigrate({}, `--schema=${require("path").join(process.cwd(), "db", "schema.prisma")}`)
spinner.succeed()
try {
console.log(log.withCaret("Seeding..."))
seeds && await seeds()
} catch (err) {
log.error(err)
log.error(`Couldn't run imported function, are you sure it's a function?`)
throw err
}
const db = require(dbPath).default
await db.$disconnect()
log.success("Done seeding")
}
export class Db extends Command {
static description = `Run database commands
${chalk.bold("migrate")} Run any needed migrations via Prisma 2 and generate Prisma Client.
${require("chalk").bold(
"migrate",
)} Run any needed migrations via Prisma 2 and generate Prisma Client.
${chalk.bold(
${require("chalk").bold(
"introspect",
)} Will introspect the database defined in db/schema.prisma and automatically generate a complete schema.prisma file for you. Lastly, it'll generate Prisma Client.
${chalk.bold(
${require("chalk").bold(
"studio",
)} Open the Prisma Studio UI at http://localhost:5555 so you can easily see and change data in your database.
${chalk.bold("reset")} Reset the database and run a fresh migration via Prisma 2.
${require("chalk").bold(
"reset",
)} Reset the database and run a fresh migration via Prisma 2. You can also pass --force to skip all the user prompts.
${require("chalk").bold(
"seed",
)} Generates seeded data in database via Prisma 2. You need db/seeds.ts or db/seeds/index.ts.
`
static args = [
@@ -182,21 +223,25 @@ ${chalk.bold("reset")} Reset the database and run a fresh migration via Prisma
help: flags.help({char: "h"}),
// Used by `new` command to perform the initial migration
name: flags.string({hidden: true}),
// Used by `reset` command to skip the confirmation prompt
force: flags.boolean({char: "f", hidden: true}),
}
static strict = false
async run() {
const {args, flags} = this.parse(Db)
const command = args["command"]
// Needs to happen at run-time since the `new` command needs to change the cwd before running
const schemaPath = path.join(process.cwd(), "db", "schema.prisma")
const schemaPath = require("path").join(process.cwd(), "db", "schema.prisma")
schemaArg = `--schema=${schemaPath}`
if (command === "migrate" || command === "m") {
try {
return await runMigrate(flags.name)
return await runMigrate(flags)
} catch (error) {
if (flags.name) {
if (Object.keys(flags).length > 0) {
throw error
} else {
process.exit(1)
@@ -214,47 +259,59 @@ ${chalk.bold("reset")} Reset the database and run a fresh migration via Prisma
}
if (command === "reset") {
const spinner = log.spinner("Loading your database").start()
await runPrismaGeneration({silent: true, failSilently: true})
spinner.succeed()
const forceSkipConfirmation = flags.force
const {confirm} = await prompt<{confirm: string}>({
type: "confirm",
name: "confirm",
message: "Are you sure you want to reset your database and erase ALL data?",
})
if (!forceSkipConfirmation) {
const {confirm} = await require("enquirer").prompt({
type: "confirm",
name: "confirm",
message: "Are you sure you want to reset your database and erase ALL data?",
})
if (!confirm) {
return
if (!confirm) {
return
}
}
log.progress("Resetting your database...")
const {projectRoot} = require("../utils/get-project-root")
const prismaClientPath = require.resolve("@prisma/client", {paths: [projectRoot]})
const {PrismaClient} = require(prismaClientPath)
const db = new PrismaClient()
const schemaPath = path.join(projectRoot, "db/schema.prisma")
const datamodel = await getSchema(schemaPath)
const config = await getConfig({datamodel})
const schemaPath = require("path").join(projectRoot, "db/schema.prisma")
const datamodel = await require("@prisma/sdk").getSchema(schemaPath)
const config = await require("@prisma/sdk").getConfig({datamodel})
const dataSource = config.datasources[0]
const providerType = dataSource.activeProvider
const connectionString = dataSource.url.value
if (providerType === "postgresql") {
resetPostgres(connectionString, db)
await resetPostgres(connectionString, db)
return
} else if (providerType === "mysql") {
resetMysql(connectionString, db)
await resetMysql(connectionString, db)
return
} else if (providerType === "sqlite") {
resetSqlite(connectionString)
await resetSqlite(connectionString)
return
} else {
this.log("Could not find a valid database configuration")
log.error("Could not find a valid database configuration")
return
}
return
}
if (command === "help") {
return Db.run(["--help"])
}
if (command === "seed") {
try {
return await runSeed()
} catch {
process.exit(1)
}
}
this.log("\nUh oh, Blitz does not support that command.")
this.log("You can try running a prisma command directly with:")
this.log("\n `npm run prisma COMMAND` or `yarn prisma COMMAND`\n")

View File

@@ -1,9 +1,6 @@
import {Command} from "../command"
import {flags} from "@oclif/command"
import * as fs from "fs"
import * as path from "path"
import enquirer from "enquirer"
import _pluralize from "pluralize"
import {log} from "@blitzjs/display"
import {
PageGenerator,
MutationGenerator,
@@ -13,15 +10,13 @@ import {
QueryGenerator,
} from "@blitzjs/generator"
import {PromptAbortedError} from "../errors/prompt-aborted"
import {log} from "@blitzjs/display"
import camelCase from "camelcase"
import pkgDir from "pkg-dir"
const debug = require("debug")("blitz:generate")
const pascalCase = (str: string) => camelCase(str, {pascalCase: true})
const projectRoot = pkgDir.sync() || process.cwd()
const isTypescript = fs.existsSync(path.join(projectRoot, "tsconfig.json"))
const pascalCase = (str: string) => require("camelcase")(str, {pascalCase: true})
const getIsTypescript = () =>
require("fs").existsSync(
require("path").join(require("../utils/get-project-root").projectRoot, "tsconfig.json"),
)
enum ResourceType {
All = "all",
@@ -46,18 +41,18 @@ interface Args {
}
function pluralize(input: string): string {
return _pluralize.isPlural(input) ? input : _pluralize.plural(input)
return require("pluralize").isPlural(input) ? input : require("pluralize").plural(input)
}
function singular(input: string): string {
return _pluralize.isSingular(input) ? input : _pluralize.singular(input)
return require("pluralize").isSingular(input) ? input : require("pluralize").singular(input)
}
function modelName(input: string = "") {
return camelCase(singular(input))
return require("camelcase")(singular(input))
}
function modelNames(input: string = "") {
return camelCase(pluralize(input))
return require("camelcase")(pluralize(input))
}
function ModelName(input: string = "") {
return pascalCase(singular(input))
@@ -157,31 +152,33 @@ export class Generate extends Command {
]
async promptForTargetDirectory(paths: string[]): Promise<string> {
return enquirer
.prompt<{directory: string}>({
return require("enquirer")
.prompt({
name: "directory",
type: "select",
message: "Please select a target directory:",
choices: paths,
})
.then((resp) => resp.directory)
.then((resp: any) => resp.directory)
}
async genericConfirmPrompt(message: string): Promise<boolean> {
return enquirer
.prompt<{continue: string}>({
return require("enquirer")
.prompt({
name: "continue",
type: "select",
message: message,
choices: ["Yes", "No"],
})
.then((resp) => resp.continue === "Yes")
.then((resp: any) => resp.continue === "Yes")
}
async handleNoContext(message: string): Promise<void> {
const shouldCreateNewRoot = await this.genericConfirmPrompt(message)
if (!shouldCreateNewRoot) {
log.error("Could not determine proper location for files. Aborting.")
require("@blitzjs/display").log.error(
"Could not determine proper location for files. Aborting.",
)
this.exit(0)
}
}
@@ -192,7 +189,7 @@ export class Generate extends Command {
if (modelSegments.length > 1) {
return {
model: modelSegments[modelSegments.length - 1],
context: path.join(...modelSegments.slice(0, modelSegments.length - 1)),
context: require("path").join(...modelSegments.slice(0, modelSegments.length - 1)),
}
}
@@ -201,7 +198,7 @@ export class Generate extends Command {
return {
model: modelName,
context: path.join(...contextSegments),
context: require("path").join(...contextSegments),
}
}
@@ -222,7 +219,7 @@ export class Generate extends Command {
const generators = generatorMap[args.type]
for (const GeneratorClass of generators) {
const generator = new GeneratorClass({
destinationRoot: path.resolve(),
destinationRoot: require("path").resolve(),
extraArgs: argv.slice(2).filter((arg) => !arg.startsWith("-")),
modelName: singularRootContext,
modelNames: modelNames(singularRootContext),
@@ -232,9 +229,10 @@ export class Generate extends Command {
parentModels: modelNames(flags.parent),
ParentModel: ModelName(flags.parent),
ParentModels: ModelNames(flags.parent),
rawInput: model,
dryRun: flags["dry-run"],
context: context,
useTs: isTypescript,
useTs: getIsTypescript(),
})
await generator.run()
}

View File

@@ -1,22 +1,14 @@
import {Command} from "../command"
import * as path from "path"
import {RecipeExecutor} from "@blitzjs/installer"
import _got from "got"
import type {RecipeExecutor} from "@blitzjs/installer"
import {log} from "@blitzjs/display"
import {dedent} from "../utils/dedent"
import {Stream} from "stream"
import {promisify} from "util"
import tar from "tar"
import {mkdirSync, readFileSync, existsSync} from "fs-extra"
import rimraf from "rimraf"
import spawn from "cross-spawn"
import * as os from "os"
import {setupTsnode} from "../utils/setup-ts-node"
const pipeline = promisify(Stream.pipeline)
async function got(url: string) {
return _got(url).catch((e) => Boolean(console.error(e)) || e)
return require("got")(url).catch((e: any) => Boolean(console.error(e)) || e)
}
async function gotJSON(url: string) {
@@ -28,7 +20,7 @@ async function isUrlValid(url: string) {
}
function requireJSON(file: string) {
return JSON.parse(readFileSync(file).toString("utf-8"))
return JSON.parse(require("fs-extra").readFileSync(file).toString("utf-8"))
}
const GH_ROOT = "https://github.com/"
@@ -115,10 +107,13 @@ export class Install extends Command {
defaultBranch: string,
subdirectory?: string,
): Promise<string> {
const recipeDir = path.join(os.tmpdir(), `blitz-recipe-${repoFullName.replace("/", "-")}`)
const recipeDir = require("path").join(
require("os").tmpdir(),
`blitz-recipe-${repoFullName.replace("/", "-")}`,
)
// clean up from previous run in case of error
rimraf.sync(recipeDir)
mkdirSync(recipeDir)
require("rimraf").sync(recipeDir)
require("fs-extra").mkdirSync(recipeDir)
process.chdir(recipeDir)
const repoName = repoFullName.split("/")[1]
@@ -127,8 +122,8 @@ export class Install extends Command {
const extractPath = subdirectory ? [`${repoName}-${defaultBranch}/${subdirectory}`] : undefined
const depth = subdirectory ? subdirectory.split("/").length + 1 : 1
await pipeline(
_got.stream(`${CODE_ROOT}${repoFullName}/tar.gz/${defaultBranch}`),
tar.extract({strip: depth}, extractPath),
require("got").stream(`${CODE_ROOT}${repoFullName}/tar.gz/${defaultBranch}`),
require("tar").extract({strip: depth}, extractPath),
)
return recipeDir
@@ -149,9 +144,11 @@ export class Install extends Command {
}
async run() {
setupTsnode()
require("../utils/setup-ts-node").setupTsnode()
const {args} = this.parse(Install)
const pkgManager = existsSync(path.resolve("yarn.lock")) ? "yarn" : "npm"
const pkgManager = require("fs-extra").existsSync(require("path").resolve("yarn.lock"))
? "yarn"
: "npm"
const originalCwd = process.cwd()
const recipeInfo = this.normalizeRecipePath(args.recipe)
@@ -178,21 +175,21 @@ export class Install extends Command {
spinner = log.spinner("Installing package.json dependencies").start()
await new Promise((resolve) => {
const installProcess = spawn(pkgManager, ["install"])
const installProcess = require("cross-spawn")(pkgManager, ["install"])
installProcess.on("exit", resolve)
})
spinner.stop()
const recipePackageMain = requireJSON("./package.json").main
const recipeEntry = path.resolve(recipePackageMain)
const recipeEntry = require("path").resolve(recipePackageMain)
process.chdir(originalCwd)
await this.installRecipeAtPath(recipeEntry)
rimraf.sync(recipeRepoPath)
require("rimraf").sync(recipeRepoPath)
}
} else {
await this.installRecipeAtPath(path.resolve(args.recipe))
await this.installRecipeAtPath(require("path").resolve(args.recipe))
}
}
}

View File

@@ -1,14 +1,11 @@
import * as path from "path"
import {flags} from "@oclif/command"
import {Command} from "../command"
import {AppGenerator, AppGeneratorOptions} from "@blitzjs/generator"
import type {AppGeneratorOptions} from "@blitzjs/generator"
import chalk from "chalk"
import hasbin from "hasbin"
import {log} from "@blitzjs/display"
const debug = require("debug")("blitz:new")
import {PromptAbortedError} from "../errors/prompt-aborted"
import {Db} from "./db"
export interface Flags {
ts: boolean
@@ -60,8 +57,8 @@ export class New extends Command {
debug("flags: ", flags)
try {
const destinationRoot = path.resolve(args.name)
const appName = path.basename(destinationRoot)
const destinationRoot = require("path").resolve(args.name)
const appName = require("path").basename(destinationRoot)
const formChoices: Array<{name: AppGeneratorOptions["form"]; message?: string}> = [
{name: "React Final Form", message: "React Final Form (recommended)"},
@@ -78,7 +75,7 @@ export class New extends Command {
const {"dry-run": dryRun, "skip-install": skipInstall, npm} = flags
const generator = new AppGenerator({
const generator = new (require("@blitzjs/generator").AppGenerator)({
destinationRoot,
appName,
dryRun,
@@ -105,7 +102,7 @@ export class New extends Command {
try {
// Required in order for DATABASE_URL to be available
require("dotenv-expand")(require("dotenv-flow").config({silent: true}))
await Db.run(["migrate", "--name", "Initial Migration"])
await require("./db").Db.run(["migrate", "--name", "Initial Migration"])
spinner.succeed()
} catch {
spinner.fail()

View File

@@ -1,12 +1,5 @@
import {dev, prod} from "@blitzjs/server"
import {dev as Dev, prod as Prod} from "@blitzjs/server"
import {Command, flags} from "@oclif/command"
import fs from "fs"
import path from "path"
import pkgDir from "pkg-dir"
import {runPrismaGeneration} from "./db"
const projectRoot = pkgDir.sync() || process.cwd()
const isTypescript = fs.existsSync(path.join(projectRoot, "tsconfig.json"))
export class Start extends Command {
static description = "Start a development server"
@@ -24,24 +17,33 @@ export class Start extends Command {
char: "H",
description: "Set server hostname",
}),
inspect: flags.boolean({
description: "Enable the Node.js inspector",
}),
["no-incremental-build"]: flags.boolean({
description:
"Disable incremental build and start from a fresh cache. Incremental build is automatically enabled for development mode and disabled during `blitz build` or when the `--production` flag is supplied.",
}),
}
async run() {
const {flags} = this.parse(Start)
const config = {
rootFolder: process.cwd(),
port: flags.port,
hostname: flags.hostname,
isTypescript,
inspect: flags.inspect,
clean: flags["no-incremental-build"],
}
try {
if (flags.production) {
await prod(config, runPrismaGeneration({silent: true, failSilently: true}))
const prod: typeof Prod = require("@blitzjs/server").prod
await prod(config)
} else {
await dev(config, runPrismaGeneration({silent: true, failSilently: true}))
const dev: typeof Dev = require("@blitzjs/server").dev
await dev(config)
}
} catch (err) {
console.error(err)

View File

@@ -1,6 +1,5 @@
import {spawn} from "cross-spawn"
import {Command} from "@oclif/command"
import hasYarn from "has-yarn"
export class Test extends Command {
static description = "Run project tests"
@@ -20,7 +19,7 @@ export class Test extends Command {
if (watch) {
watchMode = watch === "watch" || watch === "w"
}
const packageManager = hasYarn() ? "yarn" : "npm"
const packageManager = require("has-yarn")() ? "yarn" : "npm"
if (watchMode) spawn(packageManager, ["test:watch"], {stdio: "inherit"})
else spawn(packageManager, ["test"], {stdio: "inherit"})

View File

@@ -1,3 +1,7 @@
require("v8-compile-cache")
const cacheFile = require("path").join(__dirname, ".blitzjs-cli-cache")
const lazyLoad = require("@salesforce/lazy-require").default.create(cacheFile)
lazyLoad.start()
import {run as oclifRun} from "@oclif/command"
// Load the .env environment variable so it's available for all commands

View File

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

View File

@@ -0,0 +1,3 @@
export default {
disconnect: () => Promise.resolve(),
}

View File

@@ -0,0 +1,3 @@
export default async () => {
await Promise.resolve(10)
}

View File

@@ -37,30 +37,11 @@ jest.mock(
}),
)
jest.mock(
"../../src/commands/db",
jest.fn(() => {
return {
runPrismaGeneration: jest.fn(),
}
}),
)
describe("Console command", () => {
beforeEach(() => {
jest.resetAllMocks()
})
it("runs PrismaGeneration", async () => {
await Console.prototype.run()
expect(db.runPrismaGeneration).toHaveBeenCalled()
})
it("runs PrismaGeneration with silent allowed", async () => {
await Console.prototype.run()
expect(db.runPrismaGeneration).toHaveBeenCalledWith({silent: true, failSilently: true})
})
it("runs repl", async () => {
await Console.prototype.run()
expect(repl.runRepl).toHaveBeenCalled()

View File

@@ -1,15 +1,17 @@
import * as path from "path"
import {resolveBinAsync} from "@blitzjs/server"
import pkgDir from "pkg-dir"
import {join} from "path"
import {Db} from "../../src/commands/db"
let onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
callback(0)
})
const spawn = jest.fn(() => ({on: onSpy, off: jest.fn()}))
jest.doMock("cross-spawn", () => ({spawn}))
import {Db} from "../../src/commands/db"
pkgDir.sync = jest.fn(() => join(__dirname, "../__fixtures__/"))
let schemaArg: string
let prismaBin: string
@@ -17,6 +19,8 @@ let migrateSaveParams: any[]
let migrateUpDevParams: any[]
let migrateUpProdParams: any[]
let migrateSaveWithNameParams: any[]
let migrateSaveWithUnknownParams: any[]
beforeAll(async () => {
schemaArg = `--schema=${path.join(process.cwd(), "db", "schema.prisma")}`
prismaBin = await resolveBinAsync("@prisma/cli", "prisma")
@@ -41,6 +45,11 @@ beforeAll(async () => {
["migrate", "save", schemaArg, "--create-db", "--experimental", "--name", "name"],
{stdio: "ignore", env: process.env},
]
migrateSaveWithUnknownParams = [
prismaBin,
["migrate", "save", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit", env: process.env},
]
})
describe("Db command", () => {
@@ -76,6 +85,20 @@ describe("Db command", () => {
expect(onSpy).toHaveBeenCalledTimes(3)
}
function expectDbMigrateWithUnknownFlag() {
expect(spawn).toBeCalledWith(...migrateSaveWithUnknownParams)
expect(spawn).toHaveBeenCalledTimes(3)
expect(onSpy).toHaveBeenCalledTimes(3)
}
function expectDbSeedOutcome() {
expect(spawn).toBeCalledWith(...migrateSaveParams)
expect(spawn.mock.calls.length).toBe(3)
expect(onSpy).toHaveBeenCalledTimes(3)
expect(spawn).toBeCalledWith(...migrateUpDevParams)
}
it("runs db help when no command given", async () => {
// When running the help command oclif exits with code 0
// Unfortantely it treats this as an exception and throws accordingly
@@ -137,6 +160,18 @@ describe("Db command", () => {
expectProductionDbMigrateOutcome()
})
it("runs db migrate silently with the right args when name flag is used", async () => {
await Db.run(["migrate", "--name", "name"])
expectDbMigrateWithNameOutcome()
})
it("runs db migrate. (with unknown flags)", async () => {
await Db.run(["migrate", "--hoge", "aaa"])
expectDbMigrateWithUnknownFlag()
})
it("runs db introspect", async () => {
await Db.run(["introspect"])
@@ -165,9 +200,23 @@ describe("Db command", () => {
expect(spawn.mock.calls.length).toBe(0)
})
it("runs db migrate silently with the right args when name flag is used", async () => {
await Db.run(["migrate", "--name", "name"])
describe("runs db seed", () => {
let $disconnect: jest.Mock
beforeAll(() => {
jest.doMock("../__fixtures__/db", () => {
$disconnect = jest.fn()
return {default: {$disconnect}}
})
})
expectDbMigrateWithNameOutcome()
it("runs migrations and closes db at the end", async () => {
await Db.run(["seed"])
expectDbSeedOutcome()
})
it("closes connection at the end", async () => {
await Db.run(["seed"])
expect($disconnect).toBeCalled()
})
})
})

View File

@@ -10,6 +10,7 @@
"sourceMap": false,
"esModuleInterop": true,
"types": [],
"noEmit": false,
"lib": ["dom", "dom.iterable", "ES2018"]
},
"include": ["src/**/*", "types"],

View File

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

View File

@@ -0,0 +1,16 @@
module.exports = {
extends: ["../../.eslintrc.js"],
plugins: ["es5", "es"],
rules: {
"es/no-object-fromentries": "error",
"es5/no-generators": "error",
"es5/no-typeof-symbol": "error",
"es5/no-es6-methods": "error",
"es5/no-es6-static-methods": [
"error",
{
exceptMethods: ["Object.assign"],
},
],
},
}

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/core",
"description": "Blitz.js core functionality",
"version": "0.21.2-canary.1",
"version": "0.24.0",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
@@ -40,16 +40,18 @@
"url": "https://github.com/blitz-js/blitz"
},
"dependencies": {
"@blitzjs/config": "0.21.2-canary.1",
"@blitzjs/display": "0.21.2-canary.1",
"@blitzjs/config": "0.24.0",
"@blitzjs/display": "0.24.0",
"bad-behavior": "1.0.1",
"cookie-session": "1.4.0",
"deepmerge": "4.2.2",
"lodash": "^4.17.19",
"lodash-es": "^4.17.15",
"passport": "0.4.1",
"pretty-ms": "6.0.1",
"react-query": "2.5.11",
"react-query": "2.23.0",
"serialize-error": "6.0.0",
"superjson": "1.2.1",
"superjson": "1.2.3",
"url": "0.11.0"
},
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"

View File

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

View File

@@ -1,20 +1,50 @@
import {NextPage, NextComponentType} from "next"
import {NextPage, NextComponentType, NextPageContext} from "next"
import {AppProps as NextAppProps} from "next/app"
export * from "./use-query"
export * from "./use-paginated-query"
export * from "./use-params"
export * from "./use-infinite-query"
export * from "./ssr-query"
export * from "./rpc"
export * from "./with-router"
export * from "./use-router"
export * from "./use-router-query"
export * from "./middleware"
export * from "./types"
export * from "./supertokens"
export * from "./passport-adapter"
export * from "./errors"
export {useQuery, usePaginatedQuery, useInfiniteQuery} from "./use-query-hooks"
export {getQueryKey, invalidateQuery} from "./utils/react-query-utils"
export {useParam, useParams} from "./use-params"
export {withRouter, RouterContext, BlitzRouter} from "./with-router"
export {useRouter} from "./use-router"
export {useRouterQuery} from "./use-router-query"
export {passportAuth} from "./passport-adapter"
export {getIsomorphicEnhancedResolver} from "./rpc"
export {useMutation} from "./use-mutation"
export {invoke, invokeWithMiddleware} from "./invoke"
export {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
MiddlewareResponse,
MiddlewareRequest,
connectMiddleware,
Ctx,
DefaultCtx,
} from "./middleware"
export {
TOKEN_SEPARATOR,
HANDLE_SEPARATOR,
SESSION_TYPE_OPAQUE_TOKEN_SIMPLE,
SESSION_TYPE_ANONYMOUS_JWT,
SESSION_TOKEN_VERSION_0,
COOKIE_ANONYMOUS_SESSION_TOKEN,
COOKIE_SESSION_TOKEN,
COOKIE_REFRESH_TOKEN,
COOKIE_CSRF_TOKEN,
COOKIE_PUBLIC_DATA_TOKEN,
HEADER_CSRF,
HEADER_PUBLIC_DATA_TOKEN,
HEADER_SESSION_REVOKED,
HEADER_CSRF_ERROR,
LOCALSTORAGE_PREFIX,
getAntiCSRFToken,
useSession,
SessionConfig, // new
PublicData,
SessionContext,
DefaultPublicData,
} from "./supertokens"
// --------------------
// Exports from Next.js
@@ -49,10 +79,10 @@ export {default as dynamic} from "next/dynamic"
export {default as ErrorComponent} from "next/error"
export type BlitzComponentType = NextComponentType
export type BlitzComponentType<C = NextPageContext, IP = {}, P = {}> = NextComponentType<C, IP, P>
export interface AppProps extends NextAppProps {
Component: BlitzComponentType & {
export interface AppProps<P = {}> extends NextAppProps<P> {
Component: BlitzComponentType<NextPageContext, any, P> & {
getLayout?: (component: JSX.Element) => JSX.Element
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,18 @@
import {BlitzApiRequest, BlitzApiResponse} from "."
/* 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, ConnectMiddleware} from "."
import {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
connectMiddleware,
Middleware,
} from "./middleware"
import {SessionContext, PublicData} from "./supertokens"
import {SessionContext} from "./supertokens"
import {log} from "@blitzjs/display"
import passport, {Strategy} from "passport"
import passport from "passport"
import cookieSession from "cookie-session"
import {isLocalhost} from "./utils/index"
export type BlitzPassportConfig = {
successRedirectUrl?: string
errorRedirectUrl?: string
strategies: Required<Strategy>[]
}
export type VerifyCallbackResult = {
publicData: PublicData
privateData?: Record<string, any>
redirectUrl?: string
}
import {secureProxyMiddleware} from "./secure-proxy-middleware"
import {VerifyCallbackResult, BlitzPassportConfig, Middleware} from "./types"
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
@@ -34,19 +25,23 @@ const INTERNAL_REDIRECT_URL_KEY = "_redirectUrl"
export function passportAuth(config: BlitzPassportConfig) {
return async function authHandler(req: BlitzApiRequest, res: BlitzApiResponse) {
const cookieSessionMiddleware = cookieSession({
secret: process.env.SESSION_SECRET_KEY || "default-dev-secret",
secure: process.env.NODE_ENV === "production" && !isLocalhost(req),
})
const passportMiddleware = passport.initialize()
const middleware: Middleware[] = [
// TODO - fix TS type - shouldn't need `any` here
connectMiddleware(
cookieSession({
secret: process.env.SESSION_SECRET_KEY || "default-dev-secret",
secure: process.env.NODE_ENV === "production" && !isLocalhost(req),
}) as any,
),
// TODO - fix TS type - shouldn't need `any` here
connectMiddleware(passport.initialize() as any),
connectMiddleware(cookieSessionMiddleware as ConnectMiddleware),
connectMiddleware(passportMiddleware as ConnectMiddleware),
connectMiddleware(passport.session()),
]
if (config.secureProxy) {
middleware.push(secureProxyMiddleware)
}
if (!req.query.auth.length) {
return res.status(404).end()
}
@@ -67,11 +62,13 @@ 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()
})
}
middleware.push(connectMiddleware(passport.authenticate(strategy.name)))
middleware.push(
connectMiddleware(passport.authenticate(strategy.name, {...config.authenticateOptions})),
)
} else if (req.query.auth[1] === "callback") {
log.info(`Processing callback for ${strategy.name}...`)
middleware.push(
@@ -102,9 +99,9 @@ export function passportAuth(config: BlitzPassportConfig) {
const redirectUrlFromVerifyResult =
result && typeof result === "object" && (result as any).redirectUrl
let redirectUrl =
let redirectUrl: string =
redirectUrlFromVerifyResult ||
session.publicData[INTERNAL_REDIRECT_URL_KEY] ||
(session.publicData as any)[INTERNAL_REDIRECT_URL_KEY] ||
(error ? config.errorRedirectUrl : config.successRedirectUrl) ||
"/"
@@ -118,10 +115,9 @@ export function passportAuth(config: BlitzPassportConfig) {
assert(isVerifyCallbackResult(result), "Passport verify callback is invalid")
await session.create(
{...result.publicData, [INTERNAL_REDIRECT_URL_KEY]: undefined},
result.privateData,
)
delete (result.publicData as any)[INTERNAL_REDIRECT_URL_KEY]
await session.create(result.publicData, result.privateData)
res.setHeader("Location", redirectUrl)
res.statusCode = 302

View File

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

View File

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

View File

@@ -1,26 +1,41 @@
import {deserializeError} from "serialize-error"
import {queryCache} from "react-query"
import {getQueryKey} from "./utils"
import {ResolverModule, Middleware} from "./middleware"
import {isClient, isServer, clientDebug} from "./utils"
import {
getAntiCSRFToken,
publicDataStore,
HEADER_CSRF,
HEADER_SESSION_REVOKED,
HEADER_CSRF_ERROR,
HEADER_PUBLIC_DATA_TOKEN,
} from "./supertokens"
import {publicDataStore} from "./public-data-store"
import {CSRFTokenMismatchError} from "./errors"
import {serialize, deserialize} from "superjson"
import merge from "deepmerge"
import {
ResolverType,
ResolverModule,
EnhancedResolver,
EnhancedResolverRpcClient,
CancellablePromise,
ResolverRpc,
RpcOptions,
} from "./types"
import {SuperJSONResult} from "superjson/dist/types"
import {getQueryKeyFromUrlAndParams} from "./utils/react-query-utils"
type Options = {
fromQueryHook?: boolean
resultOfGetFetchMore?: any
}
export const executeRpcCall = <TInput, TResult>(
apiUrl: string,
params: TInput,
opts: RpcOptions = {},
) => {
if (!opts.fromQueryHook && !opts.fromInvoke) {
console.warn(
"[Deprecation] Directly calling queries/mutations is deprecated in favor of invoke(queryFn, params)",
)
}
export async 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,134 +43,178 @@ export async 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)
}
const result = await window.fetch(url, {
method: "POST",
headers,
credentials: "include",
redirect: "follow",
body: JSON.stringify({
// TODO remove `|| null` once superjson allows `undefined`
params: serialized.json || null,
meta: {
params: serialized.meta,
},
}),
})
// Create a new AbortController instance for this request
const controller = new AbortController()
if (result.headers) {
for (const [name] of result.headers.entries()) {
if (name.toLowerCase() === HEADER_PUBLIC_DATA_TOKEN) publicDataStore.updateState()
if (name.toLowerCase() === HEADER_SESSION_REVOKED) publicDataStore.clear()
if (name.toLowerCase() === HEADER_CSRF_ERROR) {
throw new CSRFTokenMismatchError()
const promise = window
.fetch(apiUrl, {
method: "POST",
headers,
credentials: "include",
redirect: "follow",
body: JSON.stringify({
params: serialized.json,
meta: {
params: serialized.meta,
},
}),
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)) {
const err = new CSRFTokenMismatchError()
delete err.stack
throw err
}
}
}
}
let payload
try {
payload = await result.json()
} catch (error) {
throw new Error(`Failed to parse json from request to ${url}`)
}
let payload
try {
payload = await result.json()
} catch (error) {
throw new Error(`Failed to parse json from request to ${apiUrl}`)
}
if (payload.error) {
const error = deserializeError(payload.error)
// We don't clear the publicDataStore for anonymous users
if (error.name === "AuthenticationError" && publicDataStore.getData().userId) {
publicDataStore.clear()
}
throw error
if (payload.error) {
let error = deserializeError(payload.error) as any
// We don't clear the publicDataStore for anonymous users
if (error.name === "AuthenticationError" && publicDataStore.getData().userId) {
publicDataStore.clear()
}
const prismaError = error.message.match(/invalid.*prisma.*invocation/i)
if (prismaError && !("code" in error)) {
error = new Error(prismaError[0])
error.statusCode = 500
}
// Prevent client-side error popop from showing
delete error.stack
throw error
} else {
const data =
payload.result === undefined
? undefined
: deserialize({json: payload.result, meta: payload.meta?.result})
if (!opts.fromQueryHook) {
const queryKey = getQueryKeyFromUrlAndParams(apiUrl, params)
queryCache.setQueryData(queryKey, data)
}
return data as TResult
}
}) as CancellablePromise<TResult>
// Disable react-query request cancellation for now
// Having too many weird bugs with it enabled
// promise.cancel = () => controller.abort()
return promise
}
executeRpcCall.warm = (apiUrl: string) => {
if (isClient) {
return window.fetch(apiUrl, {method: "HEAD"})
} else {
const data =
payload.result === undefined
? undefined
: deserialize({json: payload.result, meta: payload.meta?.result})
if (!opts.fromQueryHook) {
const queryKey = getQueryKey(url, params)
queryCache.setQueryData(queryKey, data)
}
return data
return
}
}
executeRpcCall.warm = (url: string) => {
if (typeof window !== "undefined") {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
window.fetch(url, {method: "HEAD"})
}
}
const getApiUrlFromResolverFilePath = (resolverFilePath: string) =>
resolverFilePath.replace(/^app\/_resolvers/, "/api")
interface ResolverEnhancement {
_meta: {
name: string
type: string
path: string
apiUrl: string
}
}
export interface RpcFunction {
(params: any, opts: any): Promise<any>
}
export interface EnhancedRpcFunction extends RpcFunction, ResolverEnhancement {}
export interface EnhancedResolverModule extends ResolverEnhancement {
(input: any, ctx: Record<string, any>): Promise<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

@@ -0,0 +1,54 @@
// @ts-ignore
import {Request} from "express"
import {secureProxyMiddleware} from "./secure-proxy-middleware"
import {Socket} from "net"
// @ts-ignore
let reqSecure: Request = {
connection: new Socket(),
method: "GET",
url: "/stuff?q=thing",
headers: {
"x-forwarded-proto": "https",
},
}
// @ts-ignore
let reqHttp: Request = {
connection: new Socket(),
method: "GET",
url: "/stuff?q=thing",
headers: {
"x-forwarded-proto": "http",
},
}
// @ts-ignore
let reqNoHeader: Request = {
connection: new Socket(),
method: "GET",
url: "/stuff?q=thing",
}
const res = {}
describe("secure proxy middleware", () => {
it("should set https protocol if X-Forwarded-Proto is https", () => {
// @ts-ignore
secureProxyMiddleware(reqSecure, res, () => null)
expect(reqSecure.protocol).toEqual("https")
})
it("should set http protocol if X-Forwarded-Proto is absent", () => {
// @ts-ignore
secureProxyMiddleware(reqNoHeader, res, () => null)
expect(reqNoHeader.protocol).toEqual("http")
})
it("should set http protocol if X-Forwarded-Proto is http", () => {
// @ts-ignore
secureProxyMiddleware(reqHttp, res, () => null)
expect(reqHttp.protocol).toEqual("http")
})
})

View File

@@ -0,0 +1,24 @@
import {MiddlewareRequest, MiddlewareResponse} from "middleware"
import {Middleware} from "types"
export const secureProxyMiddleware: Middleware = function (
req: MiddlewareRequest,
_res: MiddlewareResponse,
next: (error?: Error) => void,
) {
req.protocol = getProtocol(req)
next()
}
function getProtocol(req: MiddlewareRequest) {
// @ts-ignore
// For some reason there is no encrypted on socket while it is expected
if (req.connection.encrypted) {
return "https"
}
const forwardedProto = req.headers && (req.headers["x-forwarded-proto"] as string)
if (forwardedProto) {
return forwardedProto.split(/\s*,\s*/)[0]
}
return "http"
}

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

View File

@@ -1,55 +0,0 @@
import {
useInfiniteQuery as useInfiniteReactQuery,
InfiniteQueryResult,
InfiniteQueryOptions,
} from "react-query"
import {useIsDevPrerender, 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>[]>
export function useInfiniteQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options: InfiniteQueryOptions<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 = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getInfiniteQueryKey(queryFn, params)
const {data, ...queryRest} = useInfiniteReactQuery({
queryKey,
queryFn: (_infinite, _apiUrl, params, resultOfGetFetchMore?) =>
queryRpcFn(params, {fromQueryHook: true, resultOfGetFetchMore}),
config: {
suspense: true,
retry: retryFunction,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
}
return [data as PromiseReturnType<T>[], rest as RestQueryResult<T>]
}

View File

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

View File

@@ -1,54 +0,0 @@
import {
usePaginatedQuery as usePaginatedReactQuery,
PaginatedQueryResult,
QueryOptions,
} from "react-query"
import {useIsDevPrerender, 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>>
export function usePaginatedQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options?: QueryOptions<PaginatedQueryResult<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 = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getQueryKey(queryFn, params)
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
queryKey,
queryFn: (_apiUrl, params) => 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,7 +1,8 @@
import {useMemo} from "react"
import {fromPairs} from "lodash"
import {useRouter} from "next/router"
import {useRouterQuery} from "./use-router-query"
type ParsedUrlQueryValue = string | string[] | undefined
import {ParsedUrlQueryValue} from "./types"
export interface ParsedUrlQuery {
[key: string]: ParsedUrlQueryValue
@@ -31,7 +32,7 @@ function areQueryValuesEqual(value1: ParsedUrlQueryValue, value2: ParsedUrlQuery
}
export function extractRouterParams(routerQuery: ParsedUrlQuery, query: ParsedUrlQuery) {
return Object.fromEntries(
return fromPairs(
Object.entries(routerQuery).filter(
([key, value]) =>
typeof query[key] === "undefined" || !areQueryValuesEqual(value, query[key]),
@@ -47,39 +48,43 @@ export function useParams(returnType?: "string" | "number" | "array") {
const router = useRouter()
const query = useRouterQuery()
const rawParams = extractRouterParams(router.query, query)
const params = useMemo(() => {
const rawParams = extractRouterParams(router.query, query)
if (returnType === "string") {
const params: Record<string, string> = {}
for (const [key, value] of Object.entries(rawParams)) {
if (typeof value === "string") {
params[key] = value
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, value] of Object.entries(rawParams)) {
if (value) {
params[key] = Number(value)
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, value] of Object.entries(rawParams)) {
if (Array.isArray(value)) {
params[key] = value
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,79 +0,0 @@
import {useQuery as useReactQuery, QueryResult, QueryOptions} 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"
// NOTE - this is only for use inside useQuery
export const useIsDevPrerender = () => {
if (process.env.NODE_ENV === "production") {
return false
} else {
// useQuery is only for client-side data fetching, so if it's running on the
// server, it's for pre-render
return isServer
}
}
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?: QueryOptions<QueryResult<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 = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getQueryKey(queryFn, params)
const {data, ...queryRest} = useReactQuery({
queryKey,
queryFn: (_apiUrl, params) => 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 {QueryKeyPart} 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, ...QueryKeyPart[]] {
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<any>): QueryCacheFunctions<T> => ({
mutate: (newData, opts = {refetch: true}) => {
queryCache.setQueryData(queryKey, newData)
if (opts.refetch) {
return queryCache.invalidateQueries(queryKey, {refetchActive: true})
}
return null
},
})
export function getQueryKey<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
) {
if (typeof queryFn === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"getQueryKey is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryKey: [string, Record<string, any>] = [
((queryFn as unknown) as EnhancedRpcFunction)._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
]
return queryKey
}
export function getInfiniteQueryKey<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
) {
if (typeof queryFn === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"getQueryKey is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryKey: ["infinite", string, Record<string, any>] = [
// we need an extra cache key for infinite loading so that the cache for
// for this query is stored separately since the hook result is an array of results. Without this cache for usePaginatedQuery and this will conflict and break.
"infinite",
((queryFn as unknown) as EnhancedRpcFunction)._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
]
return queryKey
}

View File

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

View File

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

View File

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

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