Compare commits
163 Commits
authorize-
...
v0.24.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99bf898cdc | ||
|
|
4ef7d81018 | ||
|
|
37ce99a37a | ||
|
|
2f3be902e4 | ||
|
|
968f1d0cb9 | ||
|
|
0b103bccca | ||
|
|
406b643f94 | ||
|
|
33c7bec41f | ||
|
|
3d827c8506 | ||
|
|
b04c6d7469 | ||
|
|
242e2eadee | ||
|
|
08303d337b | ||
|
|
be861c7919 | ||
|
|
092045e807 | ||
|
|
1e0f17c93a | ||
|
|
310079b3bc | ||
|
|
a81252aeb4 | ||
|
|
cc58c72d94 | ||
|
|
2162e1c6b4 | ||
|
|
d46d860338 | ||
|
|
c65360de09 | ||
|
|
9873fc22de | ||
|
|
e6954fbca8 | ||
|
|
048ba5f5cb | ||
|
|
bd1063a965 | ||
|
|
ae6b22f4f0 | ||
|
|
f45d2d5ee3 | ||
|
|
7bc8a249b4 | ||
|
|
742ff71a97 | ||
|
|
9291ae3b38 | ||
|
|
5a5656078b | ||
|
|
ea815e83fa | ||
|
|
869c00c950 | ||
|
|
a670693e9d | ||
|
|
5de91ad57b | ||
|
|
31899458de | ||
|
|
dce462ba53 | ||
|
|
5ebed4b05d | ||
|
|
13353793af | ||
|
|
3583a59aa8 | ||
|
|
1c5aee7c67 | ||
|
|
c87883dbe8 | ||
|
|
7d84561690 | ||
|
|
3f43ffd4fe | ||
|
|
1ac2092129 | ||
|
|
579807ff20 | ||
|
|
566e8be3c3 | ||
|
|
1b9eb77964 | ||
|
|
9f24ba10b2 | ||
|
|
f5237c31c4 | ||
|
|
3ddb3870b9 | ||
|
|
763252a5ed | ||
|
|
6a37f32322 | ||
|
|
48e27be1a7 | ||
|
|
90df4e8409 | ||
|
|
3b46d96ec8 | ||
|
|
13c5a9b802 | ||
|
|
e6ddebadf5 | ||
|
|
1bb4cf33ff | ||
|
|
36dfbe42f5 | ||
|
|
6c06f0b62c | ||
|
|
a83536be21 | ||
|
|
07f9e26827 | ||
|
|
c5e6221ebb | ||
|
|
2b0fe98cf5 | ||
|
|
58386ffe2c | ||
|
|
23fc27027a | ||
|
|
e4c00094e5 | ||
|
|
a357fd0445 | ||
|
|
c43967984b | ||
|
|
e47d947dc0 | ||
|
|
ffb54ec064 | ||
|
|
08abc33494 | ||
|
|
34722f952c | ||
|
|
4003b8ac01 | ||
|
|
160b5fc062 | ||
|
|
b722c39f79 | ||
|
|
8da7bd7cd4 | ||
|
|
712cb172eb | ||
|
|
d623d8cc33 | ||
|
|
5ca38ca6d8 | ||
|
|
db0128c971 | ||
|
|
856aa53843 | ||
|
|
f9c3f82b27 | ||
|
|
97d5f396de | ||
|
|
760fba77ab | ||
|
|
f83ce0bd72 | ||
|
|
7425952616 | ||
|
|
7fac0134b3 | ||
|
|
df46939fe3 | ||
|
|
9e88b11496 | ||
|
|
c427ae23d4 | ||
|
|
cb849c5ba9 | ||
|
|
6e777dd72a | ||
|
|
80ab4876f2 | ||
|
|
cd0bf1f970 | ||
|
|
5f5a5c8ef7 | ||
|
|
9a1e0d0de7 | ||
|
|
fb44203510 | ||
|
|
010057b34c | ||
|
|
575e862ae3 | ||
|
|
aed6b8875a | ||
|
|
10b6f859fd | ||
|
|
9ac856c0ee | ||
|
|
1ff7f36482 | ||
|
|
df150da37e | ||
|
|
eb7409c0b3 | ||
|
|
1411a1d366 | ||
|
|
ec95cb40de | ||
|
|
b502d7ea1e | ||
|
|
74bf2a9e4c | ||
|
|
1ee637c367 | ||
|
|
c08771b57e | ||
|
|
8a468e4f79 | ||
|
|
23a33f1c3d | ||
|
|
b3767861a2 | ||
|
|
65acfff0ff | ||
|
|
ab3fc26409 | ||
|
|
7b7039e0e3 | ||
|
|
e93f24452c | ||
|
|
63c9375331 | ||
|
|
adfb486004 | ||
|
|
5828736369 | ||
|
|
40a93ee62d | ||
|
|
8fa82c7661 | ||
|
|
141003df89 | ||
|
|
6ef7b8a2de | ||
|
|
6ade33b849 | ||
|
|
02d3aa8259 | ||
|
|
1ae2bb3ee3 | ||
|
|
d84c73d2bb | ||
|
|
89b55971f1 | ||
|
|
fd1856bc7b | ||
|
|
e6dbbababb | ||
|
|
3e2b5ddc8e | ||
|
|
1b974a0371 | ||
|
|
8bf9667a15 | ||
|
|
94bd4c166c | ||
|
|
4ad9f9bdc9 | ||
|
|
13e1526ef5 | ||
|
|
ab3021a371 | ||
|
|
3e506c1dce | ||
|
|
85356ca8e8 | ||
|
|
530ce851e4 | ||
|
|
d953ef795a | ||
|
|
7fcb0945a2 | ||
|
|
ab4670c21b | ||
|
|
1ae7bf77b2 | ||
|
|
63e3fe1ccb | ||
|
|
280a2b5c4f | ||
|
|
bf2734d907 | ||
|
|
07341c14d3 | ||
|
|
e150b67cf4 | ||
|
|
1a1722168c | ||
|
|
7f266b0c98 | ||
|
|
6e92a2dfde | ||
|
|
66cd1ec650 | ||
|
|
7c4916324e | ||
|
|
d747e34853 | ||
|
|
3afab440c8 | ||
|
|
e576e6332c | ||
|
|
60d0c9d0bf | ||
|
|
8f800d388b |
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -19,3 +19,4 @@ dist
|
||||
**/.env.*.local
|
||||
**/.envrc
|
||||
.blitz-*
|
||||
.blitz-cli-cache
|
||||
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
12.16.1
|
||||
105
MAINTAINERS.md
105
MAINTAINERS.md
@@ -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 can’t do a full review right away, leave a comment thanking them and saying we’ll 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
|
||||
|
||||
86
README.md
86
README.md
@@ -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!
|
||||
|
||||
@@ -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})
|
||||
},
|
||||
),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react"
|
||||
import {Head, useRouter, BlitzPage} from "blitz"
|
||||
import {LoginForm} from "app/auth/components/LoginForm"
|
||||
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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("/")
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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},
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = withBundleAnalyzer({
|
||||
middleware: [
|
||||
sessionMiddleware({
|
||||
unstable_isAuthorized: unstable_simpleRolesIsAuthorized,
|
||||
// sessionExpiryMinutes: 1,
|
||||
sessionExpiryMinutes: 4,
|
||||
}),
|
||||
],
|
||||
/*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
12
examples/auth/types.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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()
|
||||
|
||||
30
examples/store/db/seeds.ts
Normal file
30
examples/store/db/seeds.ts
Normal 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
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.21.2-canary.1",
|
||||
"version": "0.24.0",
|
||||
"packages": ["packages/*"],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
3
packages/cli/test/__fixtures__/db/index.ts
Normal file
3
packages/cli/test/__fixtures__/db/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
disconnect: () => Promise.resolve(),
|
||||
}
|
||||
3
packages/cli/test/__fixtures__/db/seeds.ts
Normal file
3
packages/cli/test/__fixtures__/db/seeds.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default async () => {
|
||||
await Promise.resolve(10)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"sourceMap": false,
|
||||
"esModuleInterop": true,
|
||||
"types": [],
|
||||
"noEmit": false,
|
||||
"lib": ["dom", "dom.iterable", "ES2018"]
|
||||
},
|
||||
"include": ["src/**/*", "types"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
16
packages/core/.eslintrc.js
Normal file
16
packages/core/.eslintrc.js
Normal 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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
80
packages/core/src/invoke.ts
Normal file
80
packages/core/src/invoke.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
95
packages/core/src/public-data-store.test.ts
Normal file
95
packages/core/src/public-data-store.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
55
packages/core/src/public-data-store.ts
Normal file
55
packages/core/src/public-data-store.ts
Normal 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()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
54
packages/core/src/secure-proxy-middleware.test.ts
Normal file
54
packages/core/src/secure-proxy-middleware.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
24
packages/core/src/secure-proxy-middleware.ts
Normal file
24
packages/core/src/secure-proxy-middleware.ts
Normal 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"
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
31
packages/core/src/supertokens.test.ts
Normal file
31
packages/core/src/supertokens.test.ts
Normal 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",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>]
|
||||
}
|
||||
24
packages/core/src/use-mutation.ts
Normal file
24
packages/core/src/use-mutation.ts
Normal 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>
|
||||
}
|
||||
@@ -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>]
|
||||
}
|
||||
@@ -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[]
|
||||
|
||||
145
packages/core/src/use-query-hooks.ts
Normal file
145
packages/core/src/use-query-hooks.ts
Normal 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>]
|
||||
}
|
||||
@@ -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>]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
18
packages/core/src/utils/cookie.ts
Normal file
18
packages/core/src/utils/cookie.ts
Normal 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")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
129
packages/core/src/utils/react-query-utils.ts
Normal file
129
packages/core/src/utils/react-query-utils.ts
Normal 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,
|
||||
}
|
||||
18
packages/core/src/utils/request-idle-callback.ts
Normal file
18
packages/core/src/utils/request-idle-callback.ts
Normal 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
|
||||
3
packages/core/test/__snapshots__/use-query.test.tsx.snap
Normal file
3
packages/core/test/__snapshots__/use-query.test.tsx.snap
Normal 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
Reference in New Issue
Block a user