Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8700453a4 | ||
|
|
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 | ||
|
|
0fb3163ed5 | ||
|
|
a64cf7d62a | ||
|
|
ac78d9a6e1 | ||
|
|
46035af2b3 | ||
|
|
3ee531f221 | ||
|
|
294be124f2 | ||
|
|
b1b8b5f15e | ||
|
|
4f1d80f970 | ||
|
|
622016f4b7 | ||
|
|
ceb0262540 | ||
|
|
8e9f2d097e | ||
|
|
155dfaa4da | ||
|
|
ec0273f09d | ||
|
|
7a80f2be2f | ||
|
|
0821c019bd | ||
|
|
f6cc8d152e | ||
|
|
2921cb5a85 | ||
|
|
017c1ff813 | ||
|
|
eabe54c8ec | ||
|
|
ba14973ad3 | ||
|
|
e29786ba24 | ||
|
|
71e3e48f69 | ||
|
|
c0ef559eae | ||
|
|
60a338b730 | ||
|
|
b4039ff9af | ||
|
|
f51a0de5dc | ||
|
|
5a006fa89f | ||
|
|
1ecaa5ea76 | ||
|
|
5d0eb4e44e | ||
|
|
1756d57a39 | ||
|
|
2cf1b63895 | ||
|
|
b0c2a58a83 | ||
|
|
188b8d8ae8 | ||
|
|
fbd5f2815f | ||
|
|
898f39bf8e | ||
|
|
91f5056b9d | ||
|
|
a00a529387 | ||
|
|
b5d564fab8 | ||
|
|
be19dbccf8 | ||
|
|
b21df113a8 | ||
|
|
4e0abb6ca6 | ||
|
|
9cbec551f8 | ||
|
|
7531cf66f7 | ||
|
|
cf0d77e010 | ||
|
|
713f20d494 | ||
|
|
19293c1efb | ||
|
|
2466a6b98c | ||
|
|
de8e0a6808 | ||
|
|
8b9252c697 | ||
|
|
12c0f72243 | ||
|
|
5e43599338 | ||
|
|
1bf5bf0492 | ||
|
|
ff9f70daa8 | ||
|
|
89676e4ba8 | ||
|
|
679efef951 | ||
|
|
bcd9e8dbc9 | ||
|
|
8b0fa91233 | ||
|
|
e55e90c345 | ||
|
|
4e81c97b2b | ||
|
|
16b414bd3d | ||
|
|
8798c9fb3d | ||
|
|
9f91767246 | ||
|
|
a6878b35d0 | ||
|
|
39c3b27038 | ||
|
|
5cc54ab981 | ||
|
|
91034216a5 | ||
|
|
2c78ead4cd | ||
|
|
a0cf98bc47 | ||
|
|
6898ead0a5 | ||
|
|
c7843e8077 | ||
|
|
370486291a | ||
|
|
e26e04c603 | ||
|
|
1fa7d17587 | ||
|
|
27bc2e49c4 | ||
|
|
18beb6fb1d | ||
|
|
5f68bda686 | ||
|
|
81ec5c8528 | ||
|
|
0d0c808cc3 | ||
|
|
097a2196f3 | ||
|
|
c592b04c18 |
@@ -357,7 +357,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "skn0tt",
|
||||
"login": "Skn0tt",
|
||||
"name": "Simon Knott",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/14912729?v=4",
|
||||
"profile": "http://simonknott.de",
|
||||
@@ -565,15 +565,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",
|
||||
@@ -879,6 +870,304 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Weilbyte",
|
||||
"name": "Weilbyte",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/43392677?v=4",
|
||||
"profile": "https://github.com/Weilbyte",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cardotrejos",
|
||||
"name": "Ricardo Trejos",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/8602086?v=4",
|
||||
"profile": "http://ricardotrejos.tech",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "karaggeorge",
|
||||
"name": "George Karagkiaouris",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/8822835?v=4",
|
||||
"profile": "https://gkaragkiaouris.tech/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bpas247",
|
||||
"name": "Brady Pascoe",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/18705892?v=4",
|
||||
"profile": "https://www.linkedin.com/in/brady-pascoe-3bba6b13a/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "svobik7",
|
||||
"name": "Jirka Svoboda",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/761766?v=4",
|
||||
"profile": "https://www.yeahcoach.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "alan2207",
|
||||
"name": "Alan Alickovic",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/12713315?v=4",
|
||||
"profile": "https://github.com/alan2207",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yhoiseth",
|
||||
"name": "Yngve Høiseth",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/8469540?v=4",
|
||||
"profile": "https://yngve.hoiseth.net",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "brunocrosier",
|
||||
"name": "Bruno Crosier",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/18399089?v=4",
|
||||
"profile": "https://twitter.com/bruno_crosier",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jschepmans",
|
||||
"name": "Johan Schepmans",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/5782977?v=4",
|
||||
"profile": "https://github.com/jschepmans",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dillonraphael",
|
||||
"name": "Dillon Raphael",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/3496193?v=4",
|
||||
"profile": "https://twitter.com/dillonraphael",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "clgeoio",
|
||||
"name": "Cody G",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/37571416?v=4",
|
||||
"profile": "https://github.com/clgeoio",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "madflow",
|
||||
"name": "madflow",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/183248?v=4",
|
||||
"profile": "https://github.com/madflow",
|
||||
"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": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -24,6 +24,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"no-use-before-define": ["error", {functions: false, classes: false}],
|
||||
},
|
||||
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:
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -15,10 +15,8 @@ tsconfig.tsbuildinfo
|
||||
dist
|
||||
.now
|
||||
# local env files
|
||||
**/.envrc
|
||||
**/.env
|
||||
**/.env.local
|
||||
**/.env.development.local
|
||||
**/.env.test.local
|
||||
**/.env.production.local
|
||||
**/.env.*.local
|
||||
**/.envrc
|
||||
.blitz-*
|
||||
.blitz-cli-cache
|
||||
|
||||
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
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
|
||||
# 2020-08-17 Blitz Contributor Call
|
||||
|
||||
- Attending: Brandon Bayer, Adam Markon, Kellen Mace, Myron Davis, Dwight Watson
|
||||
- Brandon:
|
||||
- Auth out, set up by default in canary release
|
||||
- Need to work on a potential CSRF bug
|
||||
- Next major release will include auth by default and allow you to choose your form library
|
||||
- Next auth features are email confirmation
|
||||
- After that, logging and plugins are next
|
||||
- Auth out, set up by default in canary release
|
||||
- Need to work on a potential CSRF bug
|
||||
- Next major release will include auth by default and allow you to choose your form library
|
||||
- Next auth features are email confirmation
|
||||
- After that, logging and plugins are next
|
||||
- Adam:
|
||||
- Overhauled the recipe infrastructure. Now using jscodeshift instead of recast
|
||||
- Added support for conditional JSX in templates
|
||||
- Going to work on custom templates next
|
||||
- Overhauled the recipe infrastructure. Now using jscodeshift instead of recast
|
||||
- Added support for conditional JSX in templates
|
||||
- Going to work on custom templates next
|
||||
- Dwight
|
||||
- Has been opening issues for problems
|
||||
- Made a few PRs for some issues
|
||||
|
||||
- Has been opening issues for problems
|
||||
- Made a few PRs for some issues
|
||||
|
||||
# 2020-07-07 Blitz Contributor Call
|
||||
|
||||
|
||||
87
README.md
87
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-91-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-122-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">
|
||||
@@ -65,12 +65,12 @@ Run `npm install -g blitz`
|
||||
⚡️ CLI with code scaffolding, Rails-style console REPL, etc<br>
|
||||
⚡️ GraphQL Ready<br>
|
||||
⚡️ Deploy serverless or serverful<br>
|
||||
|
||||
**Other key features coming:**<br>
|
||||
⚡️ Highly secure authentication <br>
|
||||
⚡️ Authorization you can use on both server and client<br>
|
||||
⚡️ Recipes for easily adding libraries like Tailwind, CSS-in-JS, etc.<br>
|
||||
|
||||
**Other key features coming:**<br>
|
||||
⚡️ Model validation you can use on both server and client<br>
|
||||
⚡️ Plugins for easily adding libraries like Tailwind, CSS-in-JS, etc.<br>
|
||||
⚡️ React native support<br>
|
||||
⚡️ GUI so you don't have to use the CLI<br>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -173,8 +185,6 @@ _Code ownership, pull request approvals and merging, etc_ (see [MAINTAINERS.md](
|
||||
|
||||
_Issue triage, pull request triage, community encouragement and moderation, etc_ (see [MAINTAINERS.md](./MAINTAINERS.md))
|
||||
|
||||
We need more woman & nonbinary level 1 maintainers. See [MAINTAINERS.md](./MAINTAINERS.md) for what this entails
|
||||
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
@@ -183,14 +193,14 @@ We need more woman & nonbinary level 1 maintainers. See [MAINTAINERS.md](./MAINT
|
||||
<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="https://twitter.com/GeggsElias"><img src="https://avatars3.githubusercontent.com/u/22719177?v=4" width="100px;" alt=""/><br /><sub><b>Elias Johansson</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>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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></td>
|
||||
<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 -->
|
||||
@@ -253,7 +263,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
|
||||
<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>
|
||||
</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>
|
||||
@@ -281,48 +291,89 @@ 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="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://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/madflow"><img src="https://avatars0.githubusercontent.com/u/183248?v=4" width="100px;" alt=""/><br /><sub><b>madflow</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=madflow" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://twitter.com/nitaking_"><img src="https://avatars2.githubusercontent.com/u/10850034?v=4" width="100px;" alt=""/><br /><sub><b>Satoshi Nitawaki</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nitaking" title="Code">💻</a> <a href="#maintenance-nitaking" title="Maintenance">🚧</a> <a href="#question-nitaking" title="Answering Questions">💬</a></td>
|
||||
<td align="center"><a href="https://github.com/sirmyron"><img src="https://avatars2.githubusercontent.com/u/1430136?v=4" width="100px;" alt=""/><br /><sub><b>sirmyron</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sirmyron" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a></td>
|
||||
</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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
import {AuthenticationError} from "blitz"
|
||||
import SecurePassword from "secure-password"
|
||||
import db, {User} from "db"
|
||||
import db from "db"
|
||||
|
||||
const SP = new SecurePassword()
|
||||
|
||||
@@ -9,7 +9,12 @@ export const hashPassword = async (password: string) => {
|
||||
return hashedBuffer.toString("base64")
|
||||
}
|
||||
export const verifyPassword = async (hashedPassword: string, password: string) => {
|
||||
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
|
||||
try {
|
||||
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const authenticateUser = async (email: string, password: string) => {
|
||||
@@ -29,6 +34,6 @@ export const authenticateUser = async (email: string, password: string) => {
|
||||
throw new AuthenticationError()
|
||||
}
|
||||
|
||||
delete user.hashedPassword
|
||||
return user as Omit<User, "hashedPassword">
|
||||
const {hashedPassword, ...rest} = user
|
||||
return rest
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from "react"
|
||||
import {Link} 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
|
||||
@@ -12,8 +13,7 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
|
||||
<Form<LoginInputType>
|
||||
<Form
|
||||
submitText="Log In"
|
||||
schema={LoginInput}
|
||||
initialValues={{email: undefined, password: undefined}}
|
||||
@@ -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,5 +1,5 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import {authenticateUser} from "app/auth"
|
||||
import {authenticateUser} from "app/auth/auth-utils"
|
||||
import {LoginInput, LoginInputType} from "../validations"
|
||||
|
||||
export default async function login(input: LoginInputType, ctx: {session?: SessionContext} = {}) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import db from "db"
|
||||
import {SessionContext} from "blitz"
|
||||
import {hashPassword} from "app/auth"
|
||||
import {hashPassword} from "app/auth/auth-utils"
|
||||
import {SignupInput, SignupInputType} from "app/auth/validations"
|
||||
|
||||
export default async function signup(input: SignupInputType, ctx: {session?: SessionContext} = {}) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Head, useRouter, BlitzPage} 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()
|
||||
@@ -18,7 +18,7 @@ const SignupPage: BlitzPage = () => {
|
||||
<div>
|
||||
<h1>Create an Account</h1>
|
||||
|
||||
<Form<SignupInputType>
|
||||
<Form
|
||||
submitText="Create Account"
|
||||
schema={SignupInput}
|
||||
onSubmit={async (values) => {
|
||||
@@ -30,7 +30,10 @@ const SignupPage: BlitzPage = () => {
|
||||
// This error comes from Prisma
|
||||
return {email: "This email is already being used"}
|
||||
} else {
|
||||
return {[FORM_ERROR]: error.toString()}
|
||||
return {
|
||||
[FORM_ERROR]:
|
||||
"Sorry, we had an unexpected error. Please try again. - " + error.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -3,26 +3,26 @@ 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
|
||||
|
||||
10
examples/auth/app/hooks/useCurrentUser.ts
Normal file
10
examples/auth/app/hooks/useCurrentUser.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {useQuery, useSession} from "blitz"
|
||||
import getCurrentUser from "app/users/queries/getCurrentUser"
|
||||
|
||||
export const useCurrentUser = () => {
|
||||
// We wouldn't have to useSession() here, but doing so improves perf on initial
|
||||
// load since we can skip the getCurrentUser() request.
|
||||
const session = useSession()
|
||||
const [user] = useQuery(getCurrentUser, null, {enabled: !!session.userId})
|
||||
return session.userId ? user : null
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
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"
|
||||
|
||||
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,11 +1,22 @@
|
||||
import {Suspense} from "react"
|
||||
import {Head, Link, useSession, useRouterQuery} 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"
|
||||
|
||||
const CurrentUserInfo = () => {
|
||||
const currentUser = useCurrentUser()
|
||||
|
||||
return <pre>{JSON.stringify(currentUser, null, 2)}</pre>
|
||||
}
|
||||
|
||||
const UserStuff = () => {
|
||||
const session = useSession()
|
||||
const query = useRouterQuery()
|
||||
|
||||
if (session.isLoading) return <div>Loading...</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!session.userId && (
|
||||
@@ -26,6 +37,9 @@ const UserStuff = () => {
|
||||
</>
|
||||
)}
|
||||
<pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
<Suspense fallback="Loading...">
|
||||
<CurrentUserInfo />
|
||||
</Suspense>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
|
||||
23
examples/auth/app/sessions/components/SessionForm.tsx
Normal file
23
examples/auth/app/sessions/components/SessionForm.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react"
|
||||
|
||||
type SessionFormProps = {
|
||||
initialValues: any
|
||||
onSubmit: React.FormEventHandler<HTMLFormElement>
|
||||
}
|
||||
|
||||
const SessionForm = ({initialValues, onSubmit}: SessionFormProps) => {
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
onSubmit(event)
|
||||
}}
|
||||
>
|
||||
<div>Put your form fields here. But for now, just click submit</div>
|
||||
<div>{JSON.stringify(initialValues)}</div>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionForm
|
||||
16
examples/auth/app/sessions/mutations/createSession.ts
Normal file
16
examples/auth/app/sessions/mutations/createSession.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import db, {SessionCreateArgs} from "db"
|
||||
|
||||
type CreateSessionInput = {
|
||||
data: SessionCreateArgs["data"]
|
||||
}
|
||||
export default async function createSession(
|
||||
{data}: CreateSessionInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
) {
|
||||
ctx.session!.authorize()
|
||||
|
||||
const session = await db.session.create({data})
|
||||
|
||||
return session
|
||||
}
|
||||
17
examples/auth/app/sessions/mutations/deleteSession.ts
Normal file
17
examples/auth/app/sessions/mutations/deleteSession.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import db, {SessionDeleteArgs} from "db"
|
||||
|
||||
type DeleteSessionInput = {
|
||||
where: SessionDeleteArgs["where"]
|
||||
}
|
||||
|
||||
export default async function deleteSession(
|
||||
{where}: DeleteSessionInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
) {
|
||||
ctx.session!.authorize()
|
||||
|
||||
const session = await db.session.delete({where})
|
||||
|
||||
return session
|
||||
}
|
||||
18
examples/auth/app/sessions/mutations/updateSession.ts
Normal file
18
examples/auth/app/sessions/mutations/updateSession.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import db, {SessionUpdateArgs} from "db"
|
||||
|
||||
type UpdateSessionInput = {
|
||||
where: SessionUpdateArgs["where"]
|
||||
data: SessionUpdateArgs["data"]
|
||||
}
|
||||
|
||||
export default async function updateSession(
|
||||
{where, data}: UpdateSessionInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
) {
|
||||
ctx.session!.authorize()
|
||||
|
||||
const session = await db.session.update({where, data})
|
||||
|
||||
return session
|
||||
}
|
||||
60
examples/auth/app/sessions/pages/sessions/[sessionId].tsx
Normal file
60
examples/auth/app/sessions/pages/sessions/[sessionId].tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, {Suspense} from "react"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
|
||||
import getSession from "app/sessions/queries/getSession"
|
||||
import deleteSession from "app/sessions/mutations/deleteSession"
|
||||
|
||||
export const Session = () => {
|
||||
const router = useRouter()
|
||||
const sessionId = useParam("sessionId", "number")
|
||||
const [session] = useQuery(getSession, {where: {id: sessionId}})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Session {session.id}</h1>
|
||||
<pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
|
||||
<Link href="/sessions/[sessionId]/edit" as={`/sessions/${session.id}/edit`}>
|
||||
<a>Edit</a>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (window.confirm("This will be deleted")) {
|
||||
await deleteSession({where: {id: session.id}})
|
||||
router.push("/sessions")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ShowSessionPage: BlitzPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Session</title>
|
||||
</Head>
|
||||
|
||||
<main>
|
||||
<p>
|
||||
<Link href="/sessions">
|
||||
<a>Sessions</a>
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Session />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ShowSessionPage.getLayout = (page) => <Layout>{page}</Layout>
|
||||
|
||||
export default ShowSessionPage
|
||||
68
examples/auth/app/sessions/pages/sessions/index.tsx
Normal file
68
examples/auth/app/sessions/pages/sessions/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, {Suspense} from "react"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import {Head, Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
|
||||
import getSessions from "app/sessions/queries/getSessions"
|
||||
|
||||
const ITEMS_PER_PAGE = 100
|
||||
|
||||
export const SessionsList = () => {
|
||||
const router = useRouter()
|
||||
const page = Number(router.query.page) || 0
|
||||
const [{sessions, hasMore}] = usePaginatedQuery(getSessions, {
|
||||
orderBy: {id: "asc"},
|
||||
skip: ITEMS_PER_PAGE * page,
|
||||
take: ITEMS_PER_PAGE,
|
||||
})
|
||||
|
||||
const goToPreviousPage = () => router.push({query: {page: page - 1}})
|
||||
const goToNextPage = () => router.push({query: {page: page + 1}})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
{sessions.map((session) => (
|
||||
<li key={session.id}>
|
||||
<Link href="/sessions/[sessionId]" as={`/sessions/${session.id}`}>
|
||||
<a>{session.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button disabled={page === 0} onClick={goToPreviousPage}>
|
||||
Previous
|
||||
</button>
|
||||
<button disabled={!hasMore} onClick={goToNextPage}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SessionsPage: BlitzPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Sessions</title>
|
||||
</Head>
|
||||
|
||||
<main>
|
||||
<h1>Sessions</h1>
|
||||
|
||||
<p>
|
||||
<Link href="/sessions/new">
|
||||
<a>Create Session</a>
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SessionsList />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SessionsPage.getLayout = (page) => <Layout>{page}</Layout>
|
||||
|
||||
export default SessionsPage
|
||||
21
examples/auth/app/sessions/queries/getSession.ts
Normal file
21
examples/auth/app/sessions/queries/getSession.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {NotFoundError, SessionContext} from "blitz"
|
||||
import db, {FindOneSessionArgs} from "db"
|
||||
|
||||
type GetSessionInput = {
|
||||
where: FindOneSessionArgs["where"]
|
||||
// Only available if a model relationship exists
|
||||
// include?: FindOneSessionArgs['include']
|
||||
}
|
||||
|
||||
export default async function getSession(
|
||||
{where /* include */}: GetSessionInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
) {
|
||||
ctx.session!.authorize()
|
||||
|
||||
const session = await db.session.findOne({where})
|
||||
|
||||
if (!session) throw new NotFoundError()
|
||||
|
||||
return session
|
||||
}
|
||||
35
examples/auth/app/sessions/queries/getSessions.ts
Normal file
35
examples/auth/app/sessions/queries/getSessions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import db, {FindManySessionArgs} from "db"
|
||||
|
||||
type GetSessionsInput = {
|
||||
where?: FindManySessionArgs["where"]
|
||||
orderBy?: FindManySessionArgs["orderBy"]
|
||||
skip?: FindManySessionArgs["skip"]
|
||||
take?: FindManySessionArgs["take"]
|
||||
// Only available if a model relationship exists
|
||||
// include?: FindManySessionArgs['include']
|
||||
}
|
||||
|
||||
export default async function getSessions(
|
||||
{where, orderBy, skip = 0, take}: GetSessionsInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
) {
|
||||
ctx.session!.authorize()
|
||||
|
||||
const sessions = await db.session.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
take,
|
||||
skip,
|
||||
})
|
||||
|
||||
const count = await db.session.count()
|
||||
const hasMore = typeof take === "number" ? skip + take < count : false
|
||||
const nextPage = hasMore ? {take, skip: skip + take!} : null
|
||||
|
||||
return {
|
||||
sessions,
|
||||
nextPage,
|
||||
hasMore,
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@ export default async function updateUser(
|
||||
{where, data}: UpdateUserInput,
|
||||
ctx: Record<any, any> = {},
|
||||
) {
|
||||
// Don't allow updating
|
||||
delete data.id
|
||||
|
||||
const user = await db.user.update({where, data})
|
||||
|
||||
return user
|
||||
|
||||
13
examples/auth/app/users/queries/getCurrentUser.ts
Normal file
13
examples/auth/app/users/queries/getCurrentUser.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import db from "db"
|
||||
import {SessionContext} from "blitz"
|
||||
|
||||
export default async function getCurrentUser(_ = null, ctx: {session?: SessionContext} = {}) {
|
||||
if (!ctx.session?.userId) return null
|
||||
|
||||
const user = await db.user.findOne({
|
||||
where: {id: ctx.session!.userId},
|
||||
select: {id: true, name: true, email: true, role: true},
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default async function getUsers(
|
||||
{where, orderBy, cursor, take, skip}: GetUsersInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
) {
|
||||
ctx.session?.authorize(["admin"])
|
||||
ctx.session!.authorize(["admin"])
|
||||
|
||||
const users = await db.user.findMany({
|
||||
where,
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = withBundleAnalyzer({
|
||||
middleware: [
|
||||
sessionMiddleware({
|
||||
unstable_isAuthorized: unstable_simpleRolesIsAuthorized,
|
||||
// sessionExpiryMinutes: 1,
|
||||
}),
|
||||
],
|
||||
/*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@examples/auth",
|
||||
"version": "0.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"studio": "blitz db studio",
|
||||
@@ -8,7 +8,7 @@
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
"analyze": "cross-env ANALYZE=true blitz build",
|
||||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run --browser chrome",
|
||||
"cy:run": "cypress run",
|
||||
"test:start": "blitz db migrate && blitz start --production -p 3099",
|
||||
"test": "cross-env NODE_ENV=test start-server-and-test test:start http://localhost:3099 cy:run"
|
||||
},
|
||||
@@ -33,14 +33,15 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.1.0",
|
||||
"@prisma/client": "2.1.0",
|
||||
"blitz": "0.17.1-canary.5",
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.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-33c3af284",
|
||||
"react-dom": "0.0.0-experimental-33c3af284",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
"react-error-boundary": "2.3.1",
|
||||
"react-final-form": "6.5.1",
|
||||
"secure-password": "4.0.0",
|
||||
@@ -49,6 +50,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",
|
||||
@@ -67,7 +69,7 @@
|
||||
"eslint-plugin-react": "7.20.5",
|
||||
"eslint-plugin-react-hooks": "4.0.8",
|
||||
"husky": "4.2.5",
|
||||
"lint-staged": "10.2.11",
|
||||
"lint-staged": "10.2.13",
|
||||
"prettier": "2.0.5",
|
||||
"pretty-quick": "2.0.1",
|
||||
"start-server-and-test": "1.11.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-prisma",
|
||||
"version": "0.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"build": "blitz build",
|
||||
@@ -26,10 +26,10 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"blitz": "0.17.1-canary.5",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"knex": "0.21.2",
|
||||
"react": "0.0.0-experimental-33c3af284",
|
||||
"react-dom": "0.0.0-experimental-33c3af284",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
"sqlite3": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45,7 +45,7 @@
|
||||
"eslint-plugin-react": "7.20.5",
|
||||
"eslint-plugin-react-hooks": "4.0.8",
|
||||
"husky": "4.2.5",
|
||||
"lint-staged": "10.2.11",
|
||||
"lint-staged": "10.2.13",
|
||||
"prettier": "2.0.5",
|
||||
"pretty-quick": "2.0.1",
|
||||
"typescript": "3.9.6"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@examples/plain-js",
|
||||
"version": "0.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"build": "blitz db migrate && blitz build",
|
||||
@@ -29,11 +29,11 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.0.0",
|
||||
"@prisma/client": "2.0.0",
|
||||
"blitz": "0.17.1-canary.5",
|
||||
"react": "0.0.0-experimental-33c3af284",
|
||||
"react-dom": "0.0.0-experimental-33c3af284"
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "16.9.35",
|
||||
|
||||
@@ -2,17 +2,18 @@ import {Suspense} from "react"
|
||||
import {Link, useRouter, useQuery, useParam} from "blitz"
|
||||
import getProduct from "app/products/queries/getProduct"
|
||||
import ProductForm from "app/products/components/ProductForm"
|
||||
import {queryCache} from "react-query"
|
||||
|
||||
function Product() {
|
||||
const router = useRouter()
|
||||
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)
|
||||
onSuccess={() => {
|
||||
queryCache.invalidateQueries("/api/products/queries/getProducts")
|
||||
router.push("/admin/products")
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import getProduct from "app/products/queries/getProduct"
|
||||
function ProductsList() {
|
||||
const {orderby = "id", order = "desc"} = useRouterQuery()
|
||||
|
||||
const [products] = useQuery(getProducts, {
|
||||
const [{products}] = useQuery(getProducts, {
|
||||
orderBy: {
|
||||
[Array.isArray(orderby) ? orderby[0] : orderby]: order,
|
||||
},
|
||||
|
||||
@@ -3,10 +3,10 @@ import {Product, ProductCreateInput, ProductUpdateInput} from "db"
|
||||
import createProduct from "../mutations/createProduct"
|
||||
import updateProduct from "../mutations/updateProduct"
|
||||
|
||||
type ProductInput = ProductCreateInput | ProductUpdateInput
|
||||
type ProductInput = ProductCreateInput | Product
|
||||
|
||||
function isNew(product: ProductInput): product is ProductCreateInput {
|
||||
return (product as ProductUpdateInput).id === undefined
|
||||
return (product as any).id === undefined
|
||||
}
|
||||
|
||||
type ProductFormProps = {
|
||||
@@ -19,7 +19,7 @@ function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
|
||||
return (
|
||||
<Form
|
||||
initialValues={product || {name: null, handle: null, description: null, price: null}}
|
||||
onSubmit={async (data: ProductInput) => {
|
||||
onSubmit={async (data: any) => {
|
||||
if (isNew(data)) {
|
||||
try {
|
||||
const product = await createProduct({data})
|
||||
@@ -29,7 +29,10 @@ function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const product = await updateProduct({where: {id: data.id}, data})
|
||||
// Can't update id
|
||||
const id = data.id
|
||||
delete data.id
|
||||
const product = await updateProduct({where: {id}, data})
|
||||
onSuccess(product)
|
||||
} catch (error) {
|
||||
alert("Error updating product " + JSON.stringify(error, null, 2))
|
||||
|
||||
@@ -6,9 +6,6 @@ type UpdateProductInput = {
|
||||
}
|
||||
|
||||
export default async function updateProduct({where, data}: UpdateProductInput) {
|
||||
// Don't allow updating
|
||||
delete data.id
|
||||
|
||||
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}})
|
||||
const dataString = superjson.stringify(product)
|
||||
return {
|
||||
props: {dataString},
|
||||
@@ -18,7 +18,7 @@ export const getStaticProps: GetStaticProps<StaticProps> = async (ctx) => {
|
||||
}
|
||||
}
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const paths = (await getProducts({orderBy: {id: "desc"}})).map(({handle}) => ({
|
||||
const paths = (await getProducts({orderBy: {id: "desc"}})).products.map(({handle}) => ({
|
||||
params: {handle},
|
||||
}))
|
||||
return {
|
||||
|
||||
@@ -9,7 +9,7 @@ type StaticProps = {
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<StaticProps> = async () => {
|
||||
const products = await getProducts({orderBy: {id: "desc"}})
|
||||
const {products} = await getProducts({orderBy: {id: "desc"}})
|
||||
const dataString = superjson.stringify(products)
|
||||
return {
|
||||
props: {dataString},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {Suspense, Fragment} from "react"
|
||||
import {BlitzPage, useInfiniteQuery} from "blitz"
|
||||
import getProductsInfinite from "app/products/queries/getProductsInfinite"
|
||||
import {BlitzPage, useInfiniteQuery, Link} from "blitz"
|
||||
import getProducts from "app/products/queries/getProducts"
|
||||
|
||||
const Products = () => {
|
||||
const [groupedProducts, {isFetching, isFetchingMore, fetchMore, canFetchMore}] = useInfiniteQuery(
|
||||
getProductsInfinite,
|
||||
getProducts,
|
||||
(page = {take: 3, skip: 0}) => page,
|
||||
{
|
||||
getFetchMore: (lastGroup) => lastGroup.nextPage,
|
||||
@@ -36,6 +36,9 @@ const Page: BlitzPage = function () {
|
||||
return (
|
||||
<div>
|
||||
<h1>Products - Infinite</h1>
|
||||
<Link href="/products/paginated">
|
||||
<a>Go to Paginated Product List</a>
|
||||
</Link>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Products />
|
||||
</Suspense>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import {Suspense, useState} from "react"
|
||||
import {Link, BlitzPage, usePaginatedQuery} from "blitz"
|
||||
import {Suspense} from "react"
|
||||
import {Link, BlitzPage, usePaginatedQuery, useRouter} from "blitz"
|
||||
import getProducts from "app/products/queries/getProducts"
|
||||
|
||||
const ITEMS_PER_PAGE = 3
|
||||
|
||||
const Products = () => {
|
||||
const [page, setPage] = useState(0)
|
||||
const [products] = usePaginatedQuery(getProducts, {
|
||||
const router = useRouter()
|
||||
const page = Number(router.query.page) || 0
|
||||
const [{products, hasMore}] = usePaginatedQuery(getProducts, {
|
||||
skip: ITEMS_PER_PAGE * page,
|
||||
take: ITEMS_PER_PAGE,
|
||||
})
|
||||
|
||||
const goToPreviousPage = () => router.push({query: {page: page - 1}})
|
||||
const goToNextPage = () => router.push({query: {page: page + 1}})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{products.map((product) => (
|
||||
@@ -20,10 +24,10 @@ const Products = () => {
|
||||
</Link>
|
||||
</p>
|
||||
))}
|
||||
<button disabled={page === 0} onClick={() => setPage(page - 1)}>
|
||||
<button disabled={page === 0} onClick={goToPreviousPage}>
|
||||
Previous
|
||||
</button>
|
||||
<button disabled={products.length !== ITEMS_PER_PAGE} onClick={() => setPage(page + 1)}>
|
||||
<button disabled={!hasMore} onClick={goToNextPage}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
@@ -34,6 +38,9 @@ const Page: BlitzPage = function () {
|
||||
return (
|
||||
<div>
|
||||
<h1>Products - Paginated</h1>
|
||||
<Link href="/products/infinite">
|
||||
<a>Go to Infinite Product List</a>
|
||||
</Link>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Products />
|
||||
</Suspense>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const getServerSideProps: GetServerSideProps = async ({req, res}) => {
|
||||
}
|
||||
|
||||
const Page: BlitzPage<PageProps> = function ({dataString}) {
|
||||
const products = useMemo(() => superjson.parse(dataString), [dataString]) as Products
|
||||
const {products} = useMemo(() => superjson.parse(dataString), [dataString]) as Products
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -12,7 +12,7 @@ type GetProductsInput = {
|
||||
}
|
||||
|
||||
export default async function getProducts(
|
||||
{where, orderBy, skip, cursor, take}: GetProductsInput,
|
||||
{where, orderBy, skip = 0, cursor, take}: GetProductsInput,
|
||||
ctx: Record<any, unknown> = {},
|
||||
) {
|
||||
if (ctx.referer) {
|
||||
@@ -27,7 +27,15 @@ export default async function getProducts(
|
||||
take,
|
||||
})
|
||||
|
||||
return products
|
||||
const count = await db.product.count()
|
||||
const hasMore = typeof take === "number" ? skip + take < count : false
|
||||
const nextPage = hasMore ? {take, skip: skip + take!} : null
|
||||
|
||||
return {
|
||||
products,
|
||||
nextPage,
|
||||
hasMore,
|
||||
}
|
||||
}
|
||||
|
||||
export const middleware: Middleware[] = [
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import db, {FindManyProductArgs} from "db"
|
||||
|
||||
type GetProductsInput = {
|
||||
where?: FindManyProductArgs["where"]
|
||||
orderBy?: FindManyProductArgs["orderBy"]
|
||||
skip?: FindManyProductArgs["skip"]
|
||||
cursor?: FindManyProductArgs["cursor"]
|
||||
take?: FindManyProductArgs["take"]
|
||||
// Only available if a model relationship exists
|
||||
// include?: FindManyProductArgs['include']
|
||||
}
|
||||
|
||||
export default async function getProducts({where, orderBy, take, skip}: GetProductsInput) {
|
||||
const products = await db.product.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
take,
|
||||
skip,
|
||||
})
|
||||
|
||||
const count = await db.product.count()
|
||||
const hasMore = skip + take < count
|
||||
const nextPage = hasMore ? {take, skip: skip + take} : null
|
||||
|
||||
return {
|
||||
products,
|
||||
nextPage,
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,8 @@ describe("admin/products/[handle] page", () => {
|
||||
cy.get("button").click()
|
||||
|
||||
cy.location("pathname").should("equal", "/admin/products")
|
||||
cy.get("ul > li:last-child").contains(data[0] + random)
|
||||
// Todo - make test work for this
|
||||
// cy.get("ul > li:last-child").contains(data[0] + random)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
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.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "blitz db migrate && blitz build",
|
||||
@@ -16,14 +16,14 @@
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.0.0",
|
||||
"@prisma/client": "2.0.0",
|
||||
"blitz": "0.17.1-canary.5",
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"final-form": "4.19.1",
|
||||
"react": "0.0.0-experimental-33c3af284",
|
||||
"react-dom": "0.0.0-experimental-33c3af284",
|
||||
"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": {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind",
|
||||
"version": "0.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"scripts": {
|
||||
"build": "blitz db migrate && blitz build",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
@@ -28,11 +28,11 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.0.0",
|
||||
"@prisma/client": "2.0.0",
|
||||
"blitz": "0.17.1-canary.5",
|
||||
"react": "0.0.0-experimental-33c3af284",
|
||||
"react-dom": "0.0.0-experimental-33c3af284",
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
"typescript": "3.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,7 +48,7 @@
|
||||
"eslint-plugin-react": "7.20.5",
|
||||
"eslint-plugin-react-hooks": "4.0.8",
|
||||
"husky": "4.2.5",
|
||||
"lint-staged": "10.1.7",
|
||||
"lint-staged": "10.2.13",
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"prettier": "2.0.5",
|
||||
"pretty-quick": "2.0.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"packages": ["packages/*"],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
|
||||
55
package.json
55
package.json
@@ -45,10 +45,12 @@
|
||||
"@testing-library/react-hooks": "3.3.0",
|
||||
"@types/b64-lite": "1.3.0",
|
||||
"@types/cookie": "0.4.0",
|
||||
"@types/cross-spawn": "6.0.1",
|
||||
"@types/cookie-session": "2.0.41",
|
||||
"@types/cross-spawn": "6.0.2",
|
||||
"@types/debug": "4.1.5",
|
||||
"@types/detect-port": "1.3.0",
|
||||
"@types/diff": "4.0.2",
|
||||
"@types/dotenv-flow": "3.0.1",
|
||||
"@types/flush-write-stream": "1.0.0",
|
||||
"@types/from2": "2.3.0",
|
||||
"@types/fs-extra": "8.1.0",
|
||||
@@ -65,6 +67,7 @@
|
||||
"@types/node": "13.13.2",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"@types/parallel-transform": "1.1.0",
|
||||
"@types/passport": "1.0.4",
|
||||
"@types/pluralize": "0.0.29",
|
||||
"@types/prettier": "2.0.0",
|
||||
"@types/pump": "1.1.0",
|
||||
@@ -78,54 +81,57 @@
|
||||
"@types/vinyl": "2.0.4",
|
||||
"@types/vinyl-fs": "2.4.11",
|
||||
"@types/webpack": "4.41.13",
|
||||
"@types/passport": "1.0.4",
|
||||
"@types/cookie-session": "2.0.41",
|
||||
"@typescript-eslint/eslint-plugin": "2.x",
|
||||
"@typescript-eslint/parser": "2.x",
|
||||
"@wessberg/cjs-to-esm-transformer": "0.0.19",
|
||||
"@wessberg/rollup-plugin-ts": "1.2.24",
|
||||
"react-test-renderer": "16.13.1",
|
||||
"@wessberg/cjs-to-esm-transformer": "0.0.22",
|
||||
"@wessberg/rollup-plugin-ts": "1.3.3",
|
||||
"babel-eslint": "10.x",
|
||||
"cpy-cli": "3.1.0",
|
||||
"cross-env": "7.0.0",
|
||||
"babel-jest": "26.3.0",
|
||||
"concurrently": "5.3.0",
|
||||
"cpy-cli": "3.1.1",
|
||||
"cross-env": "7.0.2",
|
||||
"debug": "4.1.1",
|
||||
"delay": "4.3.0",
|
||||
"directory-tree": "2.2.4",
|
||||
"eslint": "7.x",
|
||||
"eslint": "7.7.0",
|
||||
"eslint-config-react-app": "5.2.1",
|
||||
"eslint-plugin-flowtype": "4.x",
|
||||
"eslint-plugin-import": "2.x",
|
||||
"eslint-plugin-jsx-a11y": "6.x",
|
||||
"eslint-plugin-prettier": "3.1.3",
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "2.x",
|
||||
"eslint-plugin-unicorn": "19.0.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",
|
||||
"eslint-plugin-prettier": "3.1.4",
|
||||
"eslint-plugin-react": "7.20.6",
|
||||
"eslint-plugin-react-hooks": "4.1.0",
|
||||
"eslint-plugin-unicorn": "21.0.0",
|
||||
"husky": "4.2.5",
|
||||
"isomorphic-unfetch": "3.0.0",
|
||||
"jest": "24.9.0",
|
||||
"jest-environment-jsdom-sixteen": "1.0.3",
|
||||
"lerna": "3.20.2",
|
||||
"lint-staged": "10.0.8",
|
||||
"mock-fs": "4.12.0",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "10.2.13",
|
||||
"mock-fs": "4.13.0",
|
||||
"nock": "12.0.3",
|
||||
"npm-run-all": "4.1.5",
|
||||
"patch-package": "6.2.2",
|
||||
"postinstall-postinstall": "2.1.0",
|
||||
"prettier": "2.0.4",
|
||||
"prettier": "2.1.1",
|
||||
"pretty-quick": "2.0.1",
|
||||
"prompt": "1.0.0",
|
||||
"react-test-renderer": "16.13.1",
|
||||
"release": "6.1.0",
|
||||
"rimraf": "3.0.2",
|
||||
"rollup": "2.7.2",
|
||||
"rollup": "2.26.8",
|
||||
"rollup-plugin-commonjs": "10.1.0",
|
||||
"rollup-plugin-json": "4.0.0",
|
||||
"rollup-plugin-node-polyfills": "0.2.1",
|
||||
"rollup-plugin-node-resolve": "5.2.0",
|
||||
"rollup-plugin-peer-deps-external": "2.2.2",
|
||||
"rollup-plugin-peer-deps-external": "2.2.3",
|
||||
"semver": "7.3.2",
|
||||
"stdout-stderr": "0.1.13",
|
||||
"test-listen": "1.1.0",
|
||||
"ts-jest": "24.3.0",
|
||||
"tsdx": "0.13.1",
|
||||
"tsdx": "0.13.3",
|
||||
"tslib": "1.11.1",
|
||||
"typescript": "3.8.3",
|
||||
"wait-on": "4.0.2"
|
||||
@@ -147,5 +153,6 @@
|
||||
"@types/react": "16.9.34",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"ts-jest": "24.3.0"
|
||||
}
|
||||
},
|
||||
"version": "0.0.0"
|
||||
}
|
||||
|
||||
@@ -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.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
@@ -39,11 +39,11 @@
|
||||
"url": "https://github.com/blitz-js/blitz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blitzjs/cli": "0.17.1-canary.5",
|
||||
"@blitzjs/core": "0.17.1-canary.5",
|
||||
"@blitzjs/generator": "0.17.1-canary.5",
|
||||
"@blitzjs/installer": "0.17.1-canary.5",
|
||||
"@blitzjs/server": "0.17.1-canary.5",
|
||||
"@blitzjs/cli": "0.23.1-canary.0",
|
||||
"@blitzjs/core": "0.23.1-canary.0",
|
||||
"@blitzjs/generator": "0.23.1-canary.0",
|
||||
"@blitzjs/installer": "0.23.1-canary.0",
|
||||
"@blitzjs/server": "0.23.1-canary.0",
|
||||
"envinfo": "7.7.2",
|
||||
"os-name": "3.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
|
||||
@@ -5,12 +5,16 @@ 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(
|
||||
@@ -37,7 +41,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.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"b": "./bin/run",
|
||||
@@ -30,43 +30,51 @@
|
||||
"/lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@blitzjs/display": "0.17.1-canary.5",
|
||||
"@blitzjs/repl": "0.17.1-canary.5",
|
||||
"@blitzjs/display": "0.23.1-canary.0",
|
||||
"@blitzjs/repl": "0.23.1-canary.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.2",
|
||||
"dotenv": "8.2.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"dotenv-expand": "5.1.0",
|
||||
"dotenv-flow": "3.2.0",
|
||||
"enquirer": "2.3.4",
|
||||
"got": "11.1.3",
|
||||
"has-yarn": "2.1.0",
|
||||
"hasbin": "1.2.3",
|
||||
"minimist": "1.2.5",
|
||||
"p-event": "4.2.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"pluralize": "8.0.0",
|
||||
"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.17.1-canary.5",
|
||||
"@blitzjs/installer": "0.17.1-canary.5",
|
||||
"@blitzjs/server": "0.17.1-canary.5",
|
||||
"@blitzjs/generator": "0.23.1-canary.0",
|
||||
"@blitzjs/installer": "0.23.1-canary.0",
|
||||
"@blitzjs/server": "0.23.1-canary.0",
|
||||
"@oclif/dev-cli": "1.22.2",
|
||||
"@oclif/test": "1.2.5",
|
||||
"@prisma/cli": "2.0.0-beta.3",
|
||||
"nock": "13.0.0-beta.3"
|
||||
"@prisma/cli": "2.4.1",
|
||||
"nock": "13.0.0-beta.3",
|
||||
"stdout-stderr": "0.1.13"
|
||||
},
|
||||
"oclif": {
|
||||
"commands": "./lib/src/commands",
|
||||
"bin": "blitz",
|
||||
"plugins": [
|
||||
"@oclif/plugin-help",
|
||||
"@oclif/plugin-not-found"
|
||||
"@oclif/plugin-not-found",
|
||||
"@oclif/plugin-autocomplete"
|
||||
],
|
||||
"hooks": {
|
||||
"init": [
|
||||
|
||||
@@ -3,7 +3,7 @@ import chalk from "chalk"
|
||||
|
||||
import {isBlitzRoot, IsBlitzRootError} from "./utils/is-blitz-root"
|
||||
|
||||
const whitelistGlobal = ["-h", "--help", "help", "new"]
|
||||
const whitelistGlobal = ["-h", "--help", "help", "new", "autocomplete", "autocomplete:script"]
|
||||
|
||||
export const hook: Hook<"init"> = async function (options) {
|
||||
const {id} = options
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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 +10,7 @@ export class Build extends Command {
|
||||
}
|
||||
|
||||
try {
|
||||
await build(config, runPrismaGeneration({silent: true}))
|
||||
await require("@blitzjs/server").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})
|
||||
spinner.succeed()
|
||||
|
||||
runRepl(Console.replOptions)
|
||||
require("@blitzjs/repl").runRepl(Console.replOptions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,98 @@
|
||||
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 {log} from "@blitzjs/display"
|
||||
|
||||
const schemaPath = path.join(process.cwd(), "db", "schema.prisma")
|
||||
const schemaArg = `--schema=${schemaPath}`
|
||||
const getPrismaBin = () => resolveBinAsync("@prisma/cli", "prisma")
|
||||
const getPrismaBin = () => require("@blitzjs/server").resolveBinAsync("@prisma/cli", "prisma")
|
||||
let prismaBin: string
|
||||
let schemaArg: string
|
||||
|
||||
const runPrisma = async (args: string[], silent = false) => {
|
||||
if (!prismaBin) {
|
||||
try {
|
||||
prismaBin = await getPrismaBin()
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Oops, we can't find Prisma Client. Please make sure it's installed in your project",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const cp = require("cross-spawn").spawn(prismaBin, args, {
|
||||
stdio: silent ? "ignore" : "inherit",
|
||||
env: process.env,
|
||||
})
|
||||
const code = await require("p-event")(cp, "exit", {rejectionEvents: []})
|
||||
|
||||
return code === 0
|
||||
}
|
||||
|
||||
const runPrismaExitOnError = async (...args: Parameters<typeof runPrisma>) => {
|
||||
const success = await runPrisma(...args)
|
||||
|
||||
if (!success) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Prisma client generation will fail if no model is defined in the schema.
|
||||
// So the silent option is here to ignore that failure
|
||||
export const runPrismaGeneration = async ({silent = false} = {}) => {
|
||||
try {
|
||||
const prismaBin = await getPrismaBin()
|
||||
export const runPrismaGeneration = async ({silent = false, failSilently = false} = {}) => {
|
||||
const success = await runPrisma(["generate", schemaArg], silent)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
spawn(prismaBin, ["generate", schemaArg], {stdio: silent ? "ignore" : "inherit"}).on(
|
||||
"exit",
|
||||
(code) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else if (silent) {
|
||||
resolve()
|
||||
} else {
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
} catch (error) {
|
||||
if (silent) return
|
||||
throw new Error(
|
||||
"Oops, we can't find Prisma Client. Please make sure it's installed in your project",
|
||||
)
|
||||
if (!success && !failSilently) {
|
||||
throw new Error("Prisma Client generation failed")
|
||||
}
|
||||
}
|
||||
|
||||
const runMigrateUp = (prismaBin: string, resolve: (value?: unknown) => void) => {
|
||||
const args = ["migrate", "up", schemaArg, "--create-db", "--experimental"]
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
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")
|
||||
}
|
||||
const cp = spawn(prismaBin, args, {stdio: "inherit"})
|
||||
cp.on("exit", async (code) => {
|
||||
if (code === 0) {
|
||||
await runPrismaGeneration()
|
||||
resolve()
|
||||
} else {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
const success = await runPrisma(args, silent)
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Migration failed")
|
||||
}
|
||||
|
||||
return runPrismaGeneration({silent})
|
||||
}
|
||||
|
||||
export const runMigrate = async () => {
|
||||
const prismaBin = await getPrismaBin()
|
||||
return new Promise((resolve) => {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
runMigrateUp(prismaBin, resolve)
|
||||
} else {
|
||||
const cp = spawn(prismaBin, ["migrate", "save", schemaArg, "--create-db", "--experimental"], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
cp.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
runMigrateUp(prismaBin, resolve)
|
||||
} else {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
export const runMigrate = async (flags: object = {}, schemaArgLocal = schemaArg) => {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
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 = options.includes("--name")
|
||||
|
||||
const args = ["migrate", "save", schemaArgLocal, "--create-db", "--experimental", ...options]
|
||||
|
||||
const success = await runPrisma(args, silent)
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Migration failed")
|
||||
}
|
||||
|
||||
return runMigrateUp({silent}, schemaArgLocal)
|
||||
}
|
||||
|
||||
export async function resetPostgres(connectionString: string, db: any): Promise<void> {
|
||||
const dbName: string = getDbName(connectionString)
|
||||
try {
|
||||
// close all other connections
|
||||
await db.queryRaw(
|
||||
await db.$queryRaw(
|
||||
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND datname='${dbName};'`,
|
||||
)
|
||||
// currently assuming the public schema is being used
|
||||
// delete schema and recreate with the appropriate privileges
|
||||
await db.executeRaw("DROP SCHEMA public cascade;")
|
||||
await db.executeRaw("CREATE SCHEMA public;")
|
||||
await db.executeRaw("GRANT ALL ON schema public TO postgres;")
|
||||
await db.executeRaw("GRANT ALL ON schema public TO public;")
|
||||
await db.$executeRaw("DROP SCHEMA public cascade;")
|
||||
await db.$executeRaw("CREATE SCHEMA public;")
|
||||
await db.$executeRaw("GRANT ALL ON schema public TO postgres;")
|
||||
await db.$executeRaw("GRANT ALL ON schema public TO public;")
|
||||
// run migration
|
||||
await runMigrate()
|
||||
log.success("Your database has been reset.")
|
||||
@@ -105,7 +108,7 @@ export async function resetMysql(connectionString: string, db: any): Promise<voi
|
||||
const dbName: string = getDbName(connectionString)
|
||||
try {
|
||||
// delete database
|
||||
await db.executeRaw(`DROP DATABASE \`${dbName}\``)
|
||||
await db.$executeRaw(`DROP DATABASE \`${dbName}\``)
|
||||
// run migration
|
||||
await runMigrate()
|
||||
log.success("Your database has been reset.")
|
||||
@@ -118,8 +121,13 @@ export async function resetMysql(connectionString: string, db: any): Promise<voi
|
||||
}
|
||||
|
||||
export async function resetSqlite(connectionString: string): Promise<void> {
|
||||
const dbPath: string = connectionString.replace(/^file:/, "").replace(/^(?:\.\.[\\/])+/, "")
|
||||
const unlink = promisify(fs.unlink)
|
||||
const relativePath = connectionString.replace(/^file:/, "").replace(/^(?:\.\.[\\/])+/, "")
|
||||
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)
|
||||
@@ -143,17 +151,21 @@ export function getDbName(connectionString: string): string {
|
||||
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.
|
||||
`
|
||||
|
||||
static args = [
|
||||
@@ -167,79 +179,93 @@ ${chalk.bold("reset")} Reset the database and run a fresh migration via Prisma
|
||||
|
||||
static flags = {
|
||||
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} = this.parse(Db)
|
||||
const {args, flags} = this.parse(Db)
|
||||
const command = args["command"]
|
||||
|
||||
const prismaBin = await getPrismaBin()
|
||||
// Needs to happen at run-time since the `new` command needs to change the cwd before running
|
||||
const schemaPath = require("path").join(process.cwd(), "db", "schema.prisma")
|
||||
schemaArg = `--schema=${schemaPath}`
|
||||
|
||||
if (command === "migrate" || command === "m") {
|
||||
await runMigrate()
|
||||
} else if (command === "introspect") {
|
||||
const cp = spawn(prismaBin, ["introspect", schemaArg], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
cp.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
spawn(prismaBin, ["generate", schemaArg], {stdio: "inherit"}).on(
|
||||
"exit",
|
||||
(code: number) => {
|
||||
if (code !== 0) {
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
)
|
||||
try {
|
||||
return await runMigrate(flags)
|
||||
} catch (error) {
|
||||
if (Object.keys(flags).length > 0) {
|
||||
throw error
|
||||
} else {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
} else if (command === "studio") {
|
||||
const cp = spawn(prismaBin, ["studio", schemaArg, "--experimental"], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
cp.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
} else {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
} else if (command === "reset") {
|
||||
const spinner = log.spinner("Loading your database").start()
|
||||
await runPrismaGeneration({silent: true})
|
||||
spinner.succeed()
|
||||
await prompt<{confirm: string}>({
|
||||
type: "confirm",
|
||||
name: "confirm",
|
||||
message: "Are you sure you want to reset your database and erase ALL data?",
|
||||
}).then((res) => {
|
||||
if (res.confirm) {
|
||||
const prismaClientPath = require.resolve("@prisma/client", {paths: [projectRoot]})
|
||||
const {PrismaClient} = require(prismaClientPath)
|
||||
const db = new PrismaClient()
|
||||
const dataSource: any = db.engine.datasources[0]
|
||||
const providerType: string = dataSource.name
|
||||
const connectionString: string = dataSource.url
|
||||
if (providerType === "postgresql") {
|
||||
resetPostgres(connectionString, db)
|
||||
} else if (providerType === "mysql") {
|
||||
resetMysql(connectionString, db)
|
||||
} else if (providerType === "sqlite") {
|
||||
resetSqlite(connectionString)
|
||||
} else {
|
||||
this.log("Could not find a valid database configuration")
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (command === "help") {
|
||||
await Db.run(["--help"])
|
||||
} else {
|
||||
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")
|
||||
this.log("Or you can list available db commands with with:")
|
||||
this.log("\n `npm run blitz db --help` or `yarn blitz db --help`\n")
|
||||
}
|
||||
}
|
||||
|
||||
if (command === "introspect") {
|
||||
await runPrismaExitOnError(["introspect", schemaArg])
|
||||
return runPrismaExitOnError(["generate", schemaArg])
|
||||
}
|
||||
|
||||
if (command === "studio") {
|
||||
return runPrismaExitOnError(["studio", schemaArg, "--experimental"])
|
||||
}
|
||||
|
||||
if (command === "reset") {
|
||||
const forceSkipConfirmation = flags.force
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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") {
|
||||
await resetPostgres(connectionString, db)
|
||||
return
|
||||
} else if (providerType === "mysql") {
|
||||
await resetMysql(connectionString, db)
|
||||
return
|
||||
} else if (providerType === "sqlite") {
|
||||
await resetSqlite(connectionString)
|
||||
return
|
||||
} else {
|
||||
log.error("Could not find a valid database configuration")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (command === "help") {
|
||||
return Db.run(["--help"])
|
||||
}
|
||||
|
||||
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")
|
||||
this.log("Or you can list available db commands with with:")
|
||||
this.log("\n `npm run blitz db --help` or `yarn blitz db --help`\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,12 +1,10 @@
|
||||
import * as path from "path"
|
||||
import {flags} from "@oclif/command"
|
||||
import {Command} from "../command"
|
||||
import {AppGenerator} 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"
|
||||
|
||||
export interface Flags {
|
||||
@@ -47,6 +45,10 @@ export class New extends Command {
|
||||
"dry-run": flags.boolean({
|
||||
description: "show what files will be created without writing them to disk",
|
||||
}),
|
||||
"no-git": flags.boolean({
|
||||
description: "Skip git repository creation",
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
async run() {
|
||||
@@ -54,26 +56,70 @@ export class New extends Command {
|
||||
debug("args: ", args)
|
||||
debug("flags: ", flags)
|
||||
|
||||
const destinationRoot = path.resolve(args.name)
|
||||
const appName = path.basename(destinationRoot)
|
||||
|
||||
const generator = new AppGenerator({
|
||||
destinationRoot,
|
||||
appName,
|
||||
dryRun: flags["dry-run"],
|
||||
useTs: !flags.js,
|
||||
yarn: !flags.npm,
|
||||
version: this.config.version,
|
||||
skipInstall: flags["skip-install"],
|
||||
})
|
||||
|
||||
try {
|
||||
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)"},
|
||||
{name: "React Hook Form"},
|
||||
{name: "Formik"},
|
||||
]
|
||||
|
||||
const promptResult: any = await this.enquirer.prompt({
|
||||
type: "select",
|
||||
name: "form",
|
||||
message: "Pick a form library (you can switch to something else later if you want)",
|
||||
choices: formChoices,
|
||||
})
|
||||
|
||||
const {"dry-run": dryRun, "skip-install": skipInstall, npm} = flags
|
||||
|
||||
const generator = new (require("@blitzjs/generator").AppGenerator)({
|
||||
destinationRoot,
|
||||
appName,
|
||||
dryRun,
|
||||
useTs: !flags.js,
|
||||
yarn: !npm,
|
||||
form: promptResult.form,
|
||||
version: this.config.version,
|
||||
skipInstall,
|
||||
skipGit: flags["no-git"],
|
||||
})
|
||||
|
||||
this.log("\n" + log.withBrand("Hang tight while we set up your new Blitz app!") + "\n")
|
||||
await generator.run()
|
||||
|
||||
const postInstallSteps = [`cd ${args.name}`]
|
||||
const needsInstall = dryRun || skipInstall
|
||||
|
||||
if (needsInstall) {
|
||||
postInstallSteps.push(npm ? "npm install" : "yarn")
|
||||
postInstallSteps.push("blitz db migrate (when asked, you can name the migration anything)")
|
||||
} else {
|
||||
const spinner = log.spinner(log.withBrand("Initializing SQLite database")).start()
|
||||
|
||||
try {
|
||||
// Required in order for DATABASE_URL to be available
|
||||
require("dotenv-expand")(require("dotenv-flow").config({silent: true}))
|
||||
await require("./db").Db.run(["migrate", "--name", "Initial Migration"])
|
||||
spinner.succeed()
|
||||
} catch {
|
||||
spinner.fail()
|
||||
postInstallSteps.push(
|
||||
"blitz db migrate (when asked, you can name the migration anything)",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
postInstallSteps.push("blitz start")
|
||||
|
||||
this.log("\n" + log.withBrand("Your new Blitz app is ready! Next steps:") + "\n")
|
||||
this.log(chalk.yellow(` 1. cd ${args.name}`))
|
||||
this.log(chalk.yellow(` 2. blitz db migrate`))
|
||||
this.log(chalk.yellow(` 3. blitz start`))
|
||||
|
||||
postInstallSteps.forEach((step, index) => {
|
||||
this.log(chalk.yellow(` ${index + 1}. ${step}`))
|
||||
})
|
||||
this.log("") // new line
|
||||
} catch (err) {
|
||||
if (err instanceof PromptAbortedError) this.exit(0)
|
||||
|
||||
|
||||
46
packages/cli/src/commands/seed.ts
Normal file
46
packages/cli/src/commands/seed.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {Command} from "@oclif/command"
|
||||
import {join} from "path"
|
||||
import pkgDir from "pkg-dir"
|
||||
import {log} from "@blitzjs/display"
|
||||
import {runMigrate} from "./db"
|
||||
|
||||
const projectRoot = pkgDir.sync() || process.cwd()
|
||||
|
||||
export class Seed extends Command {
|
||||
static description = `Fill database with seed data`
|
||||
|
||||
async run() {
|
||||
log.branded("Seeding database")
|
||||
let spinner = log.spinner("Loading seeds").start()
|
||||
|
||||
let seeds: Function
|
||||
|
||||
try {
|
||||
seeds = require(join(projectRoot, "db/seeds")).default
|
||||
|
||||
if (seeds === undefined) {
|
||||
throw new Error(`Cant find default export from db/seeds`)
|
||||
}
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
this.error(`Couldn't import default from db/seeds.ts or db/seeds/index.ts file`)
|
||||
}
|
||||
spinner.succeed()
|
||||
|
||||
spinner = log.spinner("Checking for database migrations").start()
|
||||
await runMigrate({}, `--schema=${join(process.cwd(), "db", "schema.prisma")}`)
|
||||
spinner.succeed()
|
||||
|
||||
try {
|
||||
console.log(log.withCaret("Seeding..."))
|
||||
await seeds()
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
this.error(`Couldn't run imported function, are you sure it's a function?`)
|
||||
}
|
||||
|
||||
const db = require(join(projectRoot, "db/index")).default
|
||||
await db.disconnect()
|
||||
log.success("Done seeding")
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import {Command, flags} from "@oclif/command"
|
||||
import {dev, prod} from "@blitzjs/server"
|
||||
|
||||
import {runPrismaGeneration} from "./db"
|
||||
import {Command, flags} from "@oclif/command"
|
||||
|
||||
export class Start extends Command {
|
||||
static description = "Start a development server"
|
||||
@@ -19,6 +17,13 @@ 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() {
|
||||
@@ -28,13 +33,15 @@ export class Start extends Command {
|
||||
rootFolder: process.cwd(),
|
||||
port: flags.port,
|
||||
hostname: flags.hostname,
|
||||
inspect: flags.inspect,
|
||||
clean: flags["no-incremental-build"],
|
||||
}
|
||||
|
||||
try {
|
||||
if (flags.production) {
|
||||
await prod(config, runPrismaGeneration({silent: true}))
|
||||
await prod(config)
|
||||
} else {
|
||||
await dev(config, runPrismaGeneration({silent: true}))
|
||||
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,7 +1,11 @@
|
||||
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
|
||||
require("dotenv").config()
|
||||
require("dotenv-expand")(require("dotenv-flow").config({silent: true}))
|
||||
|
||||
export function run() {
|
||||
oclifRun()
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ const spawn = jest.fn(() => {
|
||||
onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
|
||||
callback(0)
|
||||
})
|
||||
return {on: onSpy}
|
||||
return {on: onSpy, off: jest.fn()}
|
||||
})
|
||||
|
||||
jest.doMock("cross-spawn", () => ({spawn}))
|
||||
|
||||
@@ -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})
|
||||
})
|
||||
|
||||
it("runs repl", async () => {
|
||||
await Console.prototype.run()
|
||||
expect(repl.runRepl).toHaveBeenCalled()
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import * as path from "path"
|
||||
import {resolveBinAsync} from "@blitzjs/server"
|
||||
|
||||
let onSpy: jest.Mock
|
||||
const spawn = jest.fn(() => {
|
||||
onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
|
||||
callback(0)
|
||||
})
|
||||
return {on: onSpy}
|
||||
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"
|
||||
@@ -18,6 +16,8 @@ let prismaBin: string
|
||||
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")
|
||||
@@ -25,17 +25,27 @@ beforeAll(async () => {
|
||||
migrateSaveParams = [
|
||||
prismaBin,
|
||||
["migrate", "save", schemaArg, "--create-db", "--experimental"],
|
||||
{stdio: "inherit"},
|
||||
{stdio: "inherit", env: process.env},
|
||||
]
|
||||
migrateUpDevParams = [
|
||||
prismaBin,
|
||||
["migrate", "up", schemaArg, "--create-db", "--experimental"],
|
||||
{stdio: "inherit"},
|
||||
{stdio: "inherit", env: process.env},
|
||||
]
|
||||
migrateUpProdParams = [
|
||||
prismaBin,
|
||||
["migrate", "up", schemaArg, "--create-db", "--experimental", "--auto-approve"],
|
||||
{stdio: "inherit"},
|
||||
{stdio: "inherit", env: process.env},
|
||||
]
|
||||
migrateSaveWithNameParams = [
|
||||
prismaBin,
|
||||
["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},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -50,23 +60,35 @@ describe("Db command", () => {
|
||||
|
||||
function expectDbMigrateOutcome() {
|
||||
expect(spawn).toBeCalledWith(...migrateSaveParams)
|
||||
expect(spawn.mock.calls.length).toBe(3)
|
||||
expect(spawn).toHaveBeenCalledTimes(3)
|
||||
|
||||
// following expection is not working
|
||||
//expect(onSpy).toHaveBeenCalledWith(0);
|
||||
expect(onSpy).toHaveBeenCalledTimes(3)
|
||||
|
||||
expect(spawn).toBeCalledWith(...migrateUpDevParams)
|
||||
}
|
||||
|
||||
function expectProductionDbMigrateOutcome() {
|
||||
expect(spawn.mock.calls.length).toBe(2)
|
||||
expect(spawn).toHaveBeenCalledTimes(2)
|
||||
|
||||
// following expection is not working
|
||||
//expect(onSpy).toHaveBeenCalledWith(0);
|
||||
expect(onSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
expect(spawn).toBeCalledWith(...migrateUpProdParams)
|
||||
}
|
||||
|
||||
function expectDbMigrateWithNameOutcome() {
|
||||
expect(spawn).toBeCalledWith(...migrateSaveWithNameParams)
|
||||
expect(spawn).toHaveBeenCalledTimes(3)
|
||||
|
||||
expect(onSpy).toHaveBeenCalledTimes(3)
|
||||
}
|
||||
|
||||
function expectDbMigrateWithUnknownFlag() {
|
||||
expect(spawn).toBeCalledWith(...migrateSaveWithUnknownParams)
|
||||
expect(spawn).toHaveBeenCalledTimes(3)
|
||||
|
||||
expect(onSpy).toHaveBeenCalledTimes(3)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -128,16 +150,38 @@ 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"])
|
||||
|
||||
expect(spawn).toHaveBeenCalled()
|
||||
expect(spawn).toHaveBeenCalledWith(prismaBin, ["introspect", schemaArg], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
})
|
||||
expect(spawn).toHaveBeenCalledWith(prismaBin, ["generate", schemaArg], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
})
|
||||
})
|
||||
|
||||
it("runs db studio", async () => {
|
||||
await Db.run(["studio"])
|
||||
|
||||
expect(spawn).toHaveBeenCalled()
|
||||
expect(spawn).toHaveBeenCalledWith(prismaBin, ["studio", schemaArg, "--experimental"], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
})
|
||||
})
|
||||
|
||||
it("does not run db in case of invalid command", async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as os from "os"
|
||||
import fetch from "node-fetch"
|
||||
import nock from "nock"
|
||||
import rimraf from "rimraf"
|
||||
import {stdout} from "stdout-stderr"
|
||||
|
||||
jest.setTimeout(120 * 1000)
|
||||
const blitzCliPackageJson = require("../../package.json")
|
||||
@@ -25,23 +26,52 @@ async function getBlitzDistTags() {
|
||||
*/
|
||||
const testIfNotWindows = process.platform === "win32" ? test.skip : test
|
||||
|
||||
jest.mock("enquirer", () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
prompt: jest.fn().mockImplementation(() => ({form: "React Final Form"})),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("`new` command", () => {
|
||||
describe("when scaffolding new project", () => {
|
||||
beforeEach(() => {
|
||||
stdout.stripColor = true
|
||||
stdout.start()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
stdout.stop()
|
||||
})
|
||||
|
||||
jest.setTimeout(120 * 1000)
|
||||
|
||||
function makeTempDir() {
|
||||
const tmpDirPath = path.join(os.tmpdir(), "blitzjs-test-")
|
||||
return fs.mkdtempSync(tmpDirPath)
|
||||
}
|
||||
|
||||
async function whileStayingInCWD(task: () => PromiseLike<void>) {
|
||||
const oldCWD = process.cwd()
|
||||
await task()
|
||||
process.chdir(oldCWD)
|
||||
}
|
||||
|
||||
async function withNewApp(test: (dirName: string, packageJson: any) => Promise<void> | void) {
|
||||
function makeTempDir() {
|
||||
const tmpDirPath = path.join(os.tmpdir(), "blitzjs-test-")
|
||||
function getStepsFromOutput() {
|
||||
const output = stdout.output
|
||||
const exp = /^ \d. (.*)$/gm
|
||||
const matches = []
|
||||
let match
|
||||
|
||||
return fs.mkdtempSync(tmpDirPath)
|
||||
while ((match = exp.exec(output)) != null) {
|
||||
matches.push(match[1].trim())
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
async function withNewApp(test: (dirName: string, packageJson: any) => Promise<void> | void) {
|
||||
const tempDir = makeTempDir()
|
||||
|
||||
await whileStayingInCWD(() => New.run([tempDir, "--skip-install"]))
|
||||
@@ -60,7 +90,7 @@ describe("`new` command", () => {
|
||||
testIfNotWindows(
|
||||
"pins Blitz to the current version",
|
||||
async () =>
|
||||
await withNewApp(async (_, packageJson) => {
|
||||
await withNewApp(async (dirName, packageJson) => {
|
||||
const {
|
||||
dependencies: {blitz: blitzVersion},
|
||||
} = packageJson
|
||||
@@ -71,9 +101,24 @@ describe("`new` command", () => {
|
||||
} else {
|
||||
expect(blitzVersion).toEqual(latest)
|
||||
}
|
||||
|
||||
expect(getStepsFromOutput()).toStrictEqual([
|
||||
`cd ${dirName}`,
|
||||
"yarn",
|
||||
"blitz db migrate (when asked, you can name the migration anything)",
|
||||
"blitz start",
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
testIfNotWindows("performs all steps on a full install", async () => {
|
||||
const tempDir = makeTempDir()
|
||||
await whileStayingInCWD(() => New.run([tempDir]))
|
||||
rimraf.sync(tempDir)
|
||||
|
||||
expect(getStepsFromOutput()).toStrictEqual([`cd ${tempDir}`, "blitz start"])
|
||||
})
|
||||
|
||||
it("fetches latest version from template", async () => {
|
||||
const expectedVersion = "3.0.0"
|
||||
const templatePackage = {name: "eslint-plugin-react-hooks", version: "3.x"}
|
||||
|
||||
81
packages/cli/test/commands/seed.test.ts
Normal file
81
packages/cli/test/commands/seed.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {join} from "path"
|
||||
import pkgDir from "pkg-dir"
|
||||
import {resolveBinAsync} from "@blitzjs/server"
|
||||
|
||||
let onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
|
||||
callback(0)
|
||||
})
|
||||
const spawn = jest.fn(() => ({on: onSpy, off: jest.fn()}))
|
||||
|
||||
jest.doMock("cross-spawn", () => ({spawn}))
|
||||
|
||||
pkgDir.sync = jest.fn(() => join(__dirname, "../__fixtures__/"))
|
||||
|
||||
let seedsFn: jest.Mock
|
||||
jest.doMock("../__fixtures__/db/seeds", () => {
|
||||
seedsFn = jest.fn()
|
||||
return {default: seedsFn}
|
||||
})
|
||||
|
||||
let disconnect: jest.Mock
|
||||
jest.doMock("../__fixtures__/db", () => {
|
||||
disconnect = jest.fn()
|
||||
return {default: {disconnect}}
|
||||
})
|
||||
|
||||
import {Seed} from "../../src/commands/seed"
|
||||
|
||||
let schemaArg: string
|
||||
let prismaBin: string
|
||||
let migrateUpDevParams: any[]
|
||||
let migrateSaveParams: any[]
|
||||
|
||||
beforeAll(async () => {
|
||||
schemaArg = `--schema=${join(process.cwd(), "db", "schema.prisma")}`
|
||||
prismaBin = await resolveBinAsync("@prisma/cli", "prisma")
|
||||
|
||||
migrateSaveParams = [
|
||||
prismaBin,
|
||||
["migrate", "save", schemaArg, "--create-db", "--experimental"],
|
||||
{stdio: "inherit", env: process.env},
|
||||
]
|
||||
migrateUpDevParams = [
|
||||
prismaBin,
|
||||
["migrate", "up", schemaArg, "--create-db", "--experimental"],
|
||||
{stdio: "inherit", env: process.env},
|
||||
]
|
||||
|
||||
jest.spyOn(global.console, "log").mockImplementation(jest.fn((output: string) => {}))
|
||||
})
|
||||
|
||||
describe("Start command", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = "test"
|
||||
})
|
||||
|
||||
function expectDbMigrateOutcome() {
|
||||
expect(spawn).toBeCalledWith(...migrateSaveParams)
|
||||
expect(spawn.mock.calls.length).toBe(3)
|
||||
expect(onSpy).toHaveBeenCalledTimes(3)
|
||||
expect(spawn).toBeCalledWith(...migrateUpDevParams)
|
||||
}
|
||||
|
||||
it("runs migrations and closes db at the end", async () => {
|
||||
await Seed.run()
|
||||
expectDbMigrateOutcome()
|
||||
})
|
||||
|
||||
it("seeds the db", async () => {
|
||||
await Seed.run()
|
||||
expect(seedsFn).toBeCalled()
|
||||
})
|
||||
|
||||
it("closes connection at the end", async () => {
|
||||
await Seed.run()
|
||||
expect(disconnect).toBeCalled()
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@ const spawn = jest.fn(() => {
|
||||
onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
|
||||
callback(0)
|
||||
})
|
||||
return {on: onSpy}
|
||||
return {on: onSpy, off: jest.fn()}
|
||||
})
|
||||
|
||||
jest.doMock("cross-spawn", () => ({spawn}))
|
||||
|
||||
@@ -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.17.1-canary.5",
|
||||
"version": "0.23.1-canary.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.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
@@ -40,15 +40,18 @@
|
||||
"url": "https://github.com/blitz-js/blitz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blitzjs/config": "0.17.1-canary.5",
|
||||
"@blitzjs/display": "0.17.1-canary.5",
|
||||
"@blitzjs/config": "0.23.1-canary.0",
|
||||
"@blitzjs/display": "0.23.1-canary.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.2",
|
||||
"url": "0.11.0"
|
||||
},
|
||||
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
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"
|
||||
@@ -22,13 +25,10 @@ export {
|
||||
GetServerSideProps,
|
||||
InferGetStaticPropsType,
|
||||
InferGetServerSidePropsType,
|
||||
NextPage as BlitzPage,
|
||||
NextApiRequest as BlitzApiRequest,
|
||||
NextApiResponse as BlitzApiResponse,
|
||||
} from "next"
|
||||
|
||||
export {AppProps} from "next/app"
|
||||
|
||||
export {default as Head} from "next/head"
|
||||
|
||||
export {default as Link} from "next/link"
|
||||
@@ -49,4 +49,14 @@ export {default as dynamic} from "next/dynamic"
|
||||
|
||||
export {default as ErrorComponent} from "next/error"
|
||||
|
||||
export type BlitzComponentType<C = NextPageContext, IP = {}, P = {}> = NextComponentType<C, IP, P>
|
||||
|
||||
export interface AppProps<P = {}> extends NextAppProps<P> {
|
||||
Component: BlitzComponentType<NextPageContext, any, P> & {
|
||||
getLayout?: (component: JSX.Element) => JSX.Element
|
||||
}
|
||||
}
|
||||
export type BlitzPage<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
getLayout?: (component: JSX.Element) => JSX.Element
|
||||
}
|
||||
export {isLocalhost} from "./utils/index"
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/* eslint-disable es5/no-for-of -- file only used on the server */
|
||||
/* eslint-disable es5/no-es6-methods -- file only used on the server */
|
||||
import {BlitzApiRequest, BlitzApiResponse} from "."
|
||||
import {IncomingMessage, ServerResponse} from "http"
|
||||
import {EnhancedResolverModule} from "./rpc"
|
||||
import {getConfig} from "@blitzjs/config"
|
||||
import {log} from "@blitzjs/display"
|
||||
|
||||
export interface MiddlewareRequest extends BlitzApiRequest {}
|
||||
export interface MiddlewareRequest extends BlitzApiRequest {
|
||||
protocol?: string
|
||||
}
|
||||
export interface MiddlewareResponse extends BlitzApiResponse {
|
||||
/**
|
||||
* This will be passed as the second argument to Blitz queries/mutations.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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,
|
||||
@@ -7,14 +9,17 @@ import {
|
||||
} from "./middleware"
|
||||
import {SessionContext, PublicData} from "./supertokens"
|
||||
import {log} from "@blitzjs/display"
|
||||
import passport, {Strategy} from "passport"
|
||||
import passport, {AuthenticateOptions, Strategy} from "passport"
|
||||
import cookieSession from "cookie-session"
|
||||
import {isLocalhost} from "./utils/index"
|
||||
import {secureProxyMiddleware} from "./secure-proxy-middleware"
|
||||
|
||||
export type BlitzPassportConfig = {
|
||||
successRedirectUrl?: string
|
||||
errorRedirectUrl?: string
|
||||
authenticateOptions?: AuthenticateOptions
|
||||
strategies: Required<Strategy>[]
|
||||
secureProxy?: boolean
|
||||
}
|
||||
|
||||
export type VerifyCallbackResult = {
|
||||
@@ -34,19 +39,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()
|
||||
}
|
||||
@@ -71,7 +80,9 @@ export function passportAuth(config: BlitzPassportConfig) {
|
||||
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(
|
||||
@@ -109,7 +120,7 @@ export function passportAuth(config: BlitzPassportConfig) {
|
||||
"/"
|
||||
|
||||
if (error) {
|
||||
redirectUrl += "?authError=" + error.toString()
|
||||
redirectUrl += "?authError=" + encodeURIComponent(error.toString())
|
||||
res.setHeader("Location", redirectUrl)
|
||||
res.statusCode = 302
|
||||
res.end()
|
||||
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
} from "./supertokens"
|
||||
import {CSRFTokenMismatchError} from "./errors"
|
||||
import {serialize, deserialize} from "superjson"
|
||||
import merge from "deepmerge"
|
||||
|
||||
type Options = {
|
||||
fromQueryHook?: boolean
|
||||
resultOfGetFetchMore?: any
|
||||
}
|
||||
|
||||
export async function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const headers: Record<string, any> = {
|
||||
@@ -29,59 +31,83 @@ export async function executeRpcCall(url: string, params: any, opts: Options = {
|
||||
headers[HEADER_CSRF] = antiCSRFToken
|
||||
}
|
||||
|
||||
// query hook already serializes the params because otherwise react-query will mess it up
|
||||
const serialized = opts.fromQueryHook ? params : 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,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
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 payload
|
||||
try {
|
||||
payload = await result.json()
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse json from request to ${url}`)
|
||||
}
|
||||
|
||||
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
|
||||
} 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
|
||||
serialized = serialize(params)
|
||||
}
|
||||
|
||||
// Create a new AbortController instance for this request
|
||||
const controller = new AbortController()
|
||||
|
||||
const promise: CancellablePromise<any> = 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,
|
||||
},
|
||||
}),
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then(async (result) => {
|
||||
if (result.headers) {
|
||||
if (result.headers.get(HEADER_PUBLIC_DATA_TOKEN)) {
|
||||
publicDataStore.updateState()
|
||||
}
|
||||
if (result.headers.get(HEADER_SESSION_REVOKED)) {
|
||||
publicDataStore.clear()
|
||||
}
|
||||
if (result.headers.get(HEADER_CSRF_ERROR)) {
|
||||
throw new CSRFTokenMismatchError()
|
||||
}
|
||||
}
|
||||
|
||||
let payload
|
||||
try {
|
||||
payload = await result.json()
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse json from request to ${url}`)
|
||||
}
|
||||
|
||||
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
|
||||
} 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
|
||||
}
|
||||
})
|
||||
|
||||
promise.cancel = () => controller.abort()
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
executeRpcCall.warm = (url: string) => {
|
||||
@@ -99,13 +125,18 @@ interface ResolverEnhancement {
|
||||
apiUrl: string
|
||||
}
|
||||
}
|
||||
|
||||
interface CancellablePromise<T> extends Promise<T> {
|
||||
cancel?: Function
|
||||
}
|
||||
|
||||
export interface RpcFunction {
|
||||
(params: any, opts: any): Promise<any>
|
||||
(params: any, opts: any): CancellablePromise<any>
|
||||
}
|
||||
export interface EnhancedRpcFunction extends RpcFunction, ResolverEnhancement {}
|
||||
|
||||
export interface EnhancedResolverModule extends ResolverEnhancement {
|
||||
(input: any, ctx: Record<string, any>): Promise<unknown>
|
||||
(input: any, ctx: Record<string, any>): CancellablePromise<unknown>
|
||||
middleware?: Middleware[]
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
})
|
||||
})
|
||||
23
packages/core/src/secure-proxy-middleware.ts
Normal file
23
packages/core/src/secure-proxy-middleware.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {Middleware, MiddlewareRequest, MiddlewareResponse} from "middleware"
|
||||
|
||||
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,5 +1,7 @@
|
||||
import {useState, useEffect} from "react"
|
||||
import {useState} from "react"
|
||||
import BadBehavior from "bad-behavior"
|
||||
import {useIsomorphicLayoutEffect} from "./utils/hooks"
|
||||
import {queryCache} from "react-query"
|
||||
|
||||
export const TOKEN_SEPARATOR = ";"
|
||||
export const HANDLE_SEPARATOR = ":"
|
||||
@@ -26,13 +28,13 @@ function assert(condition: any, message: string): asserts condition {
|
||||
}
|
||||
|
||||
export interface PublicData extends Record<any, any> {
|
||||
userId: string | number | null
|
||||
userId: any
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export interface SessionModel extends Record<any, any> {
|
||||
handle: string
|
||||
userId?: string | number
|
||||
userId?: any
|
||||
expiresAt?: Date
|
||||
hashedSessionToken?: string
|
||||
antiCSRFToken?: string
|
||||
@@ -45,7 +47,7 @@ export type SessionConfig = {
|
||||
method?: "essential" | "advanced"
|
||||
sameSite?: "none" | "lax" | "strict"
|
||||
getSession: (handle: string) => Promise<SessionModel | null>
|
||||
getSessions: (userId: string | number) => Promise<SessionModel[]>
|
||||
getSessions: (userId: any) => Promise<SessionModel[]>
|
||||
createSession: (session: SessionModel) => Promise<SessionModel>
|
||||
updateSession: (handle: string, session: Partial<SessionModel>) => Promise<SessionModel>
|
||||
deleteSession: (handle: string) => Promise<SessionModel>
|
||||
@@ -56,7 +58,7 @@ export interface SessionContext {
|
||||
/**
|
||||
* null if anonymous
|
||||
*/
|
||||
userId: string | number | null
|
||||
userId: any
|
||||
roles: string[]
|
||||
handle: string | null
|
||||
publicData: PublicData
|
||||
@@ -72,6 +74,25 @@ export interface SessionContext {
|
||||
setPublicData: (data: Record<any, any>) => 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
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
export const getAntiCSRFToken = () => readCookie(COOKIE_CSRF_TOKEN)
|
||||
export const getPublicDataToken = () => readCookie(COOKIE_PUBLIC_DATA_TOKEN)
|
||||
|
||||
@@ -133,6 +154,7 @@ export const publicDataStore = {
|
||||
},
|
||||
clear() {
|
||||
deleteCookie(COOKIE_PUBLIC_DATA_TOKEN)
|
||||
queryCache.clear()
|
||||
this.updateState()
|
||||
},
|
||||
}
|
||||
@@ -140,35 +162,17 @@ publicDataStore.initialize()
|
||||
|
||||
export const useSession = () => {
|
||||
const [publicData, setPublicData] = useState(emptyPublicData)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
// Initialize on mount
|
||||
setPublicData(publicDataStore.getData())
|
||||
setIsLoading(false)
|
||||
const subscription = publicDataStore.observable.subscribe(setPublicData)
|
||||
return subscription.unsubscribe
|
||||
}, [])
|
||||
|
||||
return publicData
|
||||
}
|
||||
|
||||
// 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 deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")
|
||||
|
||||
export const setCookie = (name: string, value: string, expires: string) => {
|
||||
const result = `${name}=${value};path=/;expires=${expires}`
|
||||
document.cookie = result
|
||||
return {...publicData, isLoading}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
useInfiniteQuery as useInfiniteReactQuery,
|
||||
InfiniteQueryResult,
|
||||
InfiniteQueryOptions,
|
||||
InfiniteQueryConfig,
|
||||
} from "react-query"
|
||||
import {useIsDevPrerender, emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {getQueryCacheFunctions, QueryCacheFunctions} from "./utils/query-cache"
|
||||
import {getQueryCacheFunctions, QueryCacheFunctions, getInfiniteQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
import {serialize} from "superjson"
|
||||
|
||||
type RestQueryResult<T extends QueryFn> = Omit<
|
||||
InfiniteQueryResult<PromiseReturnType<T>, any>,
|
||||
@@ -15,10 +14,12 @@ type RestQueryResult<T extends QueryFn> = Omit<
|
||||
> &
|
||||
QueryCacheFunctions<PromiseReturnType<T>[]>
|
||||
|
||||
const isServer = typeof window === "undefined"
|
||||
|
||||
export function useInfiniteQuery<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
|
||||
options: InfiniteQueryOptions<PromiseReturnType<T>, any>,
|
||||
options: InfiniteQueryConfig<PromiseReturnType<T>, any>,
|
||||
): [PromiseReturnType<T>[], RestQueryResult<T>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
|
||||
@@ -30,16 +31,14 @@ export function useInfiniteQuery<T extends QueryFn>(
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = useIsDevPrerender()
|
||||
? emptyQueryFn
|
||||
: ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getInfiniteQueryKey(queryFn, params)
|
||||
|
||||
const {data, ...queryRest} = useInfiniteReactQuery({
|
||||
queryKey: [
|
||||
queryRpcFn._meta.apiUrl,
|
||||
serialize(typeof params === "function" ? (params as Function)() : params),
|
||||
],
|
||||
queryFn: (_: string, params, more?) => queryRpcFn({...params, ...more}, {fromQueryHook: true}),
|
||||
queryKey,
|
||||
queryFn: (_infinite: boolean, _apiUrl: string, params: any, resultOfGetFetchMore?: any) =>
|
||||
queryRpcFn(params, {fromQueryHook: true, resultOfGetFetchMore}),
|
||||
config: {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
@@ -49,7 +48,7 @@ export function useInfiniteQuery<T extends QueryFn>(
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryRpcFn._meta.apiUrl),
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
|
||||
}
|
||||
|
||||
return [data as PromiseReturnType<T>[], rest as RestQueryResult<T>]
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
usePaginatedQuery as usePaginatedReactQuery,
|
||||
PaginatedQueryResult,
|
||||
QueryOptions,
|
||||
PaginatedQueryConfig,
|
||||
} from "react-query"
|
||||
import {useIsDevPrerender, emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {QueryCacheFunctions, getQueryCacheFunctions} from "./utils/query-cache"
|
||||
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
import {serialize} from "superjson"
|
||||
|
||||
type RestQueryResult<T extends QueryFn> = Omit<
|
||||
PaginatedQueryResult<PromiseReturnType<T>>,
|
||||
@@ -15,10 +14,12 @@ type RestQueryResult<T extends QueryFn> = Omit<
|
||||
> &
|
||||
QueryCacheFunctions<PromiseReturnType<T>>
|
||||
|
||||
const isServer = typeof window === "undefined"
|
||||
|
||||
export function usePaginatedQuery<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
|
||||
options?: QueryOptions<PaginatedQueryResult<PromiseReturnType<T>>>,
|
||||
options?: PaginatedQueryConfig<PromiseReturnType<T>>,
|
||||
): [PromiseReturnType<T>, RestQueryResult<T>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
|
||||
@@ -30,16 +31,13 @@ export function usePaginatedQuery<T extends QueryFn>(
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = useIsDevPrerender()
|
||||
? emptyQueryFn
|
||||
: ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getQueryKey(queryFn, params)
|
||||
|
||||
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
|
||||
queryKey: [
|
||||
queryRpcFn._meta.apiUrl,
|
||||
serialize(typeof params === "function" ? (params as Function)() : params),
|
||||
],
|
||||
queryFn: (_: string, params) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
queryKey,
|
||||
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
config: {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
@@ -49,7 +47,7 @@ export function usePaginatedQuery<T extends QueryFn>(
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryRpcFn._meta.apiUrl),
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
|
||||
}
|
||||
|
||||
return [resolvedData as PromiseReturnType<T>, rest as RestQueryResult<T>]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {useRouter} from "next/router"
|
||||
import {useRouterQuery} from "./use-router-query"
|
||||
import {fromPairs} from "lodash"
|
||||
|
||||
type ParsedUrlQueryValue = string | string[] | undefined
|
||||
|
||||
@@ -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]),
|
||||
@@ -51,9 +52,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
|
||||
|
||||
if (returnType === "string") {
|
||||
const params: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(rawParams)) {
|
||||
if (typeof value === "string") {
|
||||
params[key] = value
|
||||
for (const key in rawParams) {
|
||||
if (typeof rawParams[key] === "string") {
|
||||
params[key] = rawParams[key] as string
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -61,9 +62,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
|
||||
|
||||
if (returnType === "number") {
|
||||
const params: Record<string, number> = {}
|
||||
for (const [key, value] of Object.entries(rawParams)) {
|
||||
if (value) {
|
||||
params[key] = Number(value)
|
||||
for (const key in rawParams) {
|
||||
if (rawParams[key]) {
|
||||
params[key] = Number(rawParams[key])
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -71,9 +72,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
|
||||
|
||||
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
|
||||
for (const key in rawParams) {
|
||||
if (Array.isArray(rawParams[key])) {
|
||||
params[key] = rawParams[key] as Array<string | undefined>
|
||||
}
|
||||
}
|
||||
return params
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {useQuery as useReactQuery, QueryResult, QueryOptions} from "react-query"
|
||||
import {useQuery as useReactQuery, QueryResult, QueryConfig} from "react-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {QueryCacheFunctions, getQueryCacheFunctions} from "./utils/query-cache"
|
||||
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
import {serialize} from "superjson"
|
||||
|
||||
type RestQueryResult<T extends QueryFn> = Omit<QueryResult<PromiseReturnType<T>>, "data"> &
|
||||
QueryCacheFunctions<PromiseReturnType<T>>
|
||||
@@ -20,35 +19,19 @@ export const emptyQueryFn: EnhancedRpcFunction = (() => {
|
||||
|
||||
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
|
||||
if (error.name === "AuthenticationError") return false
|
||||
if (error.name === "AuthorizationError") return false
|
||||
if (error.name === "CSRFTokenMismatchError") return false
|
||||
if (error.name === "NotFoundError") return false
|
||||
if (error.name === "ZodError") return false
|
||||
// Prisma errors
|
||||
if (typeof error.code === "string" && error.code.startsWith("P")) return false
|
||||
if (failureCount > 2) return false
|
||||
|
||||
return true
|
||||
// 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>>>,
|
||||
options?: QueryConfig<PromiseReturnType<T>>,
|
||||
): [PromiseReturnType<T>, RestQueryResult<T>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("useQuery is missing the first argument - it must be a query function")
|
||||
@@ -60,16 +43,13 @@ export function useQuery<T extends QueryFn>(
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = useIsDevPrerender()
|
||||
? emptyQueryFn
|
||||
: ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getQueryKey(queryFn, params)
|
||||
|
||||
const {data, ...queryRest} = useReactQuery({
|
||||
queryKey: [
|
||||
queryRpcFn._meta.apiUrl,
|
||||
serialize(typeof params === "function" ? (params as Function)() : params),
|
||||
],
|
||||
queryFn: (_: string, params) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
queryKey,
|
||||
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
config: {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
@@ -79,7 +59,7 @@ export function useQuery<T extends QueryFn>(
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryRpcFn._meta.apiUrl),
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
|
||||
}
|
||||
|
||||
return [data as PromiseReturnType<T>, rest as RestQueryResult<T>]
|
||||
|
||||
7
packages/core/src/utils/hooks.ts
Normal file
7
packages/core/src/utils/hooks.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {useEffect, useLayoutEffect} from "react"
|
||||
const isServer = typeof window === "undefined"
|
||||
|
||||
// React currently throws a warning when using useLayoutEffect on the server.
|
||||
// To get around it, we can conditionally useEffect on the server (no-op) and
|
||||
// useLayoutEffect in the browser.
|
||||
export const useIsomorphicLayoutEffect = isServer ? useEffect : useLayoutEffect
|
||||
@@ -1,10 +1,10 @@
|
||||
import {QueryKeyPart} from "react-query"
|
||||
import {QueryKey} from "react-query"
|
||||
import {BlitzApiRequest} from "../"
|
||||
import {IncomingMessage} from "http"
|
||||
|
||||
export const isServer = typeof window === "undefined"
|
||||
|
||||
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKeyPart[]] {
|
||||
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKey[]] {
|
||||
return [cacheKey, typeof params === "function" ? (params as Function)() : params]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {queryCache} from "react-query"
|
||||
import {queryCache, QueryKey} from "react-query"
|
||||
import {serialize} from "superjson"
|
||||
import {InferUnaryParam, QueryFn} from "../types"
|
||||
import {EnhancedRpcFunction} from "rpc"
|
||||
|
||||
type MutateOptions = {
|
||||
refetch?: boolean
|
||||
@@ -8,7 +11,7 @@ export interface QueryCacheFunctions<T> {
|
||||
mutate: (newData: T | ((oldData: T | undefined) => T), opts?: MutateOptions) => void
|
||||
}
|
||||
|
||||
export const getQueryCacheFunctions = <T>(queryKey: string): QueryCacheFunctions<T> => ({
|
||||
export const getQueryCacheFunctions = <T>(queryKey: QueryKey): QueryCacheFunctions<T> => ({
|
||||
mutate: (newData, opts = {refetch: true}) => {
|
||||
queryCache.setQueryData(queryKey, newData)
|
||||
if (opts.refetch) {
|
||||
@@ -17,3 +20,46 @@ export const getQueryCacheFunctions = <T>(queryKey: string): QueryCacheFunctions
|
||||
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
|
||||
}
|
||||
|
||||
@@ -19,6 +19,26 @@ type DefaultParams = Parameters<typeof defaultRender>
|
||||
type RenderUI = DefaultParams[0]
|
||||
type RenderOptions = DefaultParams[1] & {router?: Partial<NextRouter>}
|
||||
|
||||
const mockRouter: NextRouter = {
|
||||
basePath: "",
|
||||
pathname: "/",
|
||||
route: "/",
|
||||
asPath: "/",
|
||||
query: {},
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
back: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
beforePopState: jest.fn(),
|
||||
events: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
isFallback: false,
|
||||
}
|
||||
|
||||
export function render(ui: RenderUI, {wrapper, router, ...options}: RenderOptions = {}) {
|
||||
if (!wrapper) {
|
||||
wrapper = ({children}) => (
|
||||
@@ -54,23 +74,3 @@ export function renderHook(
|
||||
|
||||
return defaultRenderHook(hook, {wrapper, ...options})
|
||||
}
|
||||
|
||||
const mockRouter: NextRouter = {
|
||||
basePath: "",
|
||||
pathname: "/",
|
||||
route: "/",
|
||||
asPath: "/",
|
||||
query: {},
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
back: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
beforePopState: jest.fn(),
|
||||
events: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
isFallback: false,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/display",
|
||||
"version": "0.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"description": "Display package for the Blitz CLI",
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/file-pipeline",
|
||||
"version": "0.17.1-canary.5",
|
||||
"version": "0.23.1-canary.0",
|
||||
"description": "Display package for the Blitz CLI",
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"license": "MIT",
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "4.0.0",
|
||||
"chokidar": "3.4.0",
|
||||
"chokidar": "3.4.2",
|
||||
"flush-write-stream": "2.0.0",
|
||||
"from2": "2.3.0",
|
||||
"fs-extra": "9.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from "crypto"
|
||||
import {transform} from "../transform"
|
||||
import {hash} from "../utils"
|
||||
/**
|
||||
* Returns a stage that prepares files coming into the stream
|
||||
* with correct event information as well as hash information
|
||||
@@ -19,12 +19,7 @@ export function createEnrichFiles() {
|
||||
}
|
||||
|
||||
if (!file.hash) {
|
||||
const hash = crypto
|
||||
.createHash("md5")
|
||||
.update(JSON.stringify({path: file.path, s: file.stat?.mtime}))
|
||||
.digest("hex")
|
||||
|
||||
file.hash = hash
|
||||
file.hash = hash(file.path + file.stat?.mtime.toString())
|
||||
}
|
||||
|
||||
return file
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# Future thinking - work-optimizer
|
||||
|
||||
So one future issue we have been trying to account for here is how to solve the dirty sync problem with streams. Basically, we want Blitz to do as little work as possible. At this point, we are blowing away Blitz folders when we start but it would be smarter to analyze the source and destination folders and only manipulate the files that are actually required to be changed. This is not required as of now but will be a consideration as we try and get this thing faster and faster to live up to its name. To prepare for this we have setup a work optimizer that checks the hash of the input file and guards against new work being done
|
||||
|
||||
The following is a rough plan for how to do this. (Likely to change/improve at a later point)
|
||||
|
||||
- Encode vinyl files + stats
|
||||
|
||||
```ts
|
||||
const hash = crypto
|
||||
.createHash("md5")
|
||||
.update(file.path + file.stats.mtime)
|
||||
.digest("hex")
|
||||
|
||||
file.hash = hash
|
||||
```
|
||||
|
||||
- Use those hashes to index file details in the following structures:
|
||||
|
||||
Following
|
||||
|
||||
```ts
|
||||
// reduced to as the first step during input
|
||||
const input = {abc123def456: "/foo/bar/baz", def456abc123: "/foo/bar/bop"}
|
||||
|
||||
// reduced to as the last step just before file write
|
||||
const complete = {
|
||||
abc123def456: {
|
||||
input: "/foo/bar/baz",
|
||||
output: ["/bas/boop/blop", "/bas/boop/ding", "/bas/boop/bar"],
|
||||
},
|
||||
def456abc123: {
|
||||
input: "/foo/bar/bing",
|
||||
output: ["/bas/boop/ping", "/bas/boop/foo", "/bas/boop/fawn"],
|
||||
},
|
||||
cbd123aef456: {
|
||||
input: "/foo/bar/bop",
|
||||
output: ["/bas/boop/thing"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Has this file hash been processed?
|
||||
|
||||
```ts
|
||||
const hash => !!output[hash];
|
||||
```
|
||||
|
||||
Which files do I need to delete based on input?
|
||||
|
||||
```ts
|
||||
const deleteHashes = Object.keys(output).filter((hash) => input[hash])
|
||||
```
|
||||
|
||||
- Output can also be indexed by filetype to keep going with our hacky error mapping (eventually this should probably be a sourcemap)
|
||||
|
||||
```json
|
||||
{
|
||||
"/bas/boop/bar": "/foo/bar/baz",
|
||||
"/bas/boop/blop": "/foo/bar/baz",
|
||||
"/bas/boop/ding": "/foo/bar/baz",
|
||||
"/bas/boop/fawn": "/foo/bar/bing",
|
||||
"/bas/boop/foo": "/foo/bar/bing",
|
||||
"/bas/boop/ping": "/foo/bar/bing",
|
||||
"/bas/boop/thing": "/foo/bar/bop"
|
||||
}
|
||||
```
|
||||
|
||||
Does my output match my input ie. am I in a stable state? or in our case can we return the promise.
|
||||
|
||||
```ts
|
||||
function isStable(input, output) {
|
||||
if (!input || !output) {
|
||||
return // We are not stable if we don't have both an input or output
|
||||
}
|
||||
|
||||
const inputKeys = Object.keys(input)
|
||||
const outputKeys = Object.keys(output)
|
||||
|
||||
if (inputKeys.length !== outputKeys.length) {
|
||||
return false
|
||||
}
|
||||
match = true
|
||||
for (let i = 0; i < inputKeys.length; i++) {
|
||||
match = match && outputKey[i] === inputKeys[i]
|
||||
if (!match) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
```
|
||||
@@ -1,37 +1,79 @@
|
||||
// Mostly concerned with solving the Dirty Sync problem
|
||||
import {log} from "@blitzjs/display"
|
||||
import {transform} from "../../transform"
|
||||
import {hash} from "../../utils"
|
||||
import debounce from "lodash/debounce"
|
||||
import {writeFile, existsSync, readFileSync} from "fs-extra"
|
||||
import {resolve, relative} from "path"
|
||||
import File from "vinyl"
|
||||
|
||||
const defaultSaveCache = debounce((filePath: string, data: object) => {
|
||||
return writeFile(filePath, Buffer.from(JSON.stringify(data, null, 2)))
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
}, 500)
|
||||
|
||||
const defaultReadCache = (filePath: string) => {
|
||||
// We need to do sync file reading here as this cache
|
||||
// must be loaded before the stream is added to the pipeline
|
||||
// or we end up with more complexity having to cache files as they come in
|
||||
return existsSync(filePath) ? readFileSync(filePath).toString() : ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns streams that help handling work optimisation in the file transform stream.
|
||||
*/
|
||||
// TODO: This needs quite a bit of work before we can manage a dirty start
|
||||
// Currently this does not do much aside from guard against repeated work
|
||||
export function createWorkOptimizer() {
|
||||
const todo: Array<string> = []
|
||||
const done: Array<string> = []
|
||||
export function createWorkOptimizer(
|
||||
src: string,
|
||||
dest: string,
|
||||
saveCache: (filePath: string, data: object) => Promise<void> = defaultSaveCache,
|
||||
readCache: (filePath: string) => string = defaultReadCache,
|
||||
) {
|
||||
const getOriginalPathHash = (file: File) => {
|
||||
return hash(relative(src, file.history[0]))
|
||||
}
|
||||
const doneCacheLocation = resolve(dest, ".blitz.incache.json")
|
||||
|
||||
const doneStr = readCache(doneCacheLocation)
|
||||
|
||||
const todo: Record<string, string> = {}
|
||||
const done: Record<string, string> = doneStr ? JSON.parse(doneStr) : {}
|
||||
|
||||
const stats = {todo, done}
|
||||
|
||||
const reportComplete = transform.file((file) => {
|
||||
if (file.hash) {
|
||||
done.push(file.hash)
|
||||
const reportComplete = transform.file(async (file) => {
|
||||
const pathHash = getOriginalPathHash(file)
|
||||
|
||||
delete todo[pathHash]
|
||||
|
||||
if (file.event === "add") {
|
||||
done[pathHash] = file.hash
|
||||
}
|
||||
|
||||
if (file.event === "unlink") {
|
||||
delete done[pathHash]
|
||||
}
|
||||
|
||||
await saveCache(resolve(dest, ".blitz.incache.json"), done)
|
||||
|
||||
return file
|
||||
})
|
||||
|
||||
const triage = transform.file((file, {push, next}) => {
|
||||
const pathHash = getOriginalPathHash(file)
|
||||
|
||||
if (!file.hash) {
|
||||
log.debug("File does not have hash! " + file.path)
|
||||
return next()
|
||||
}
|
||||
|
||||
// Dont send files that have already been done or have already been added
|
||||
if (done.includes(file.hash) || todo.includes(file.hash)) {
|
||||
if (done[pathHash] === file.hash || todo[pathHash] === file.hash) {
|
||||
log.debug("Rejecting because this job has been done before: " + file.path)
|
||||
return next()
|
||||
}
|
||||
|
||||
todo.push(file.hash)
|
||||
todo[pathHash] = file.hash
|
||||
|
||||
push(file)
|
||||
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import {createWorkOptimizer} from "."
|
||||
import {testStreamItems} from "../../test-utils"
|
||||
import {take} from "../../test-utils"
|
||||
import {hash} from "../../utils"
|
||||
import {pipeline} from "../../streams"
|
||||
import {normalize} from "path"
|
||||
import {normalize, resolve} from "path"
|
||||
import File from "vinyl"
|
||||
|
||||
function logItem(fileOrString: {path: string} | string) {
|
||||
if (typeof fileOrString === "string") {
|
||||
return fileOrString
|
||||
}
|
||||
return fileOrString.path
|
||||
}
|
||||
const pathToOneHash = hash(normalize("to/one"))
|
||||
const pathToTwoHash = hash(normalize("to/two"))
|
||||
const pathToThreeHash = hash(normalize("to/three"))
|
||||
|
||||
describe("agnosticSource", () => {
|
||||
test("basic throughput", async () => {
|
||||
const {triage, reportComplete} = createWorkOptimizer()
|
||||
const saveCache = jest.fn()
|
||||
const readCache = jest.fn()
|
||||
const {triage, reportComplete} = createWorkOptimizer(
|
||||
normalize("/path"),
|
||||
normalize("/dest"),
|
||||
saveCache,
|
||||
readCache,
|
||||
)
|
||||
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -27,6 +34,7 @@ describe("agnosticSource", () => {
|
||||
hash: "two",
|
||||
path: normalize("/path/to/two"),
|
||||
content: Buffer.from("two"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -35,21 +43,31 @@ describe("agnosticSource", () => {
|
||||
hash: "three",
|
||||
path: normalize("/path/to/three"),
|
||||
content: Buffer.from("three"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
const expected = ["/path/to/one", "/path/to/two", "/path/to/three"].map(normalize)
|
||||
const stream = pipeline(triage, reportComplete)
|
||||
await testStreamItems(stream, expected, logItem)
|
||||
const items = await take<File>(stream, 3)
|
||||
expect(items.map(({path}) => path)).toEqual(expected)
|
||||
})
|
||||
|
||||
test("same file is rejected", async () => {
|
||||
const {triage, reportComplete} = createWorkOptimizer()
|
||||
const saveCache = jest.fn()
|
||||
const readCache = jest.fn()
|
||||
const {triage, reportComplete} = createWorkOptimizer(
|
||||
normalize("/path"),
|
||||
normalize("/dest"),
|
||||
saveCache,
|
||||
readCache,
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -58,6 +76,7 @@ describe("agnosticSource", () => {
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -66,11 +85,156 @@ describe("agnosticSource", () => {
|
||||
hash: "two",
|
||||
path: normalize("/path/to/two"),
|
||||
content: Buffer.from("two"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
const expected = ["/path/to/one", "/path/to/two"].map(normalize)
|
||||
const stream = pipeline(triage, reportComplete)
|
||||
await testStreamItems(stream, expected, logItem)
|
||||
const items = await take<File>(stream, 2)
|
||||
expect(items.map(({path}) => path)).toEqual(expected)
|
||||
})
|
||||
|
||||
test("read cache from disk and skips cached files with the same hash and path", async () => {
|
||||
const saveCache = jest.fn()
|
||||
const readCache = jest.fn(() => {
|
||||
return `{"${pathToOneHash}": "one","${pathToTwoHash}": "two"}`
|
||||
})
|
||||
|
||||
const {triage, reportComplete} = createWorkOptimizer(
|
||||
normalize("/path"),
|
||||
normalize("/dest"),
|
||||
saveCache,
|
||||
readCache,
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "two",
|
||||
path: normalize("/path/to/two"),
|
||||
content: Buffer.from("two"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "three",
|
||||
path: normalize("/path/to/three"),
|
||||
content: Buffer.from("three"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
const stream = pipeline(triage, reportComplete)
|
||||
const [item] = await take<File>(stream, 1)
|
||||
expect(item.path).toEqual(normalize("/path/to/three"))
|
||||
})
|
||||
|
||||
test("save cache should be saved correctly", async () => {
|
||||
const saveCache = jest.fn()
|
||||
const readCache = jest.fn()
|
||||
const {triage, reportComplete} = createWorkOptimizer(
|
||||
normalize("/path"),
|
||||
normalize("/dest"),
|
||||
saveCache,
|
||||
readCache,
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "two",
|
||||
path: normalize("/path/to/two"),
|
||||
content: Buffer.from("two"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "three",
|
||||
path: normalize("/path/to/three"),
|
||||
content: Buffer.from("three"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
const stream = pipeline(triage, reportComplete)
|
||||
await take(stream, 3)
|
||||
|
||||
const doneObj = {
|
||||
[`${pathToOneHash}`]: "one",
|
||||
[`${pathToTwoHash}`]: "two",
|
||||
[`${pathToThreeHash}`]: "three",
|
||||
}
|
||||
|
||||
expect(saveCache).toHaveBeenCalledWith(resolve(normalize("/dest/.blitz.incache.json")), doneObj)
|
||||
})
|
||||
|
||||
test("should keep track of deleted files", async () => {
|
||||
const saveCache = jest.fn()
|
||||
const readCache = jest.fn()
|
||||
const {triage, reportComplete} = createWorkOptimizer(
|
||||
normalize("/path"),
|
||||
normalize("/dest"),
|
||||
saveCache,
|
||||
readCache,
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "two",
|
||||
path: normalize("/path/to/two"),
|
||||
content: Buffer.from("two"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "three",
|
||||
path: normalize("/path/to/three"),
|
||||
content: Buffer.from("three"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "something else",
|
||||
path: normalize("/path/to/two"),
|
||||
event: "unlink",
|
||||
}),
|
||||
)
|
||||
|
||||
const stream = pipeline(triage, reportComplete)
|
||||
await take(stream, 4)
|
||||
|
||||
const expectedDone = {
|
||||
[pathToOneHash]: "one",
|
||||
[pathToThreeHash]: "three",
|
||||
}
|
||||
|
||||
expect(saveCache).toHaveBeenCalledWith(
|
||||
resolve(normalize("/dest/.blitz.incache.json")),
|
||||
expectedDone,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,11 @@ import {FILE_WRITTEN, FILE_DELETED} from "../../events"
|
||||
import {Writable} from "stream"
|
||||
import {isFile} from "../../utils"
|
||||
import {transform} from "../../transform"
|
||||
|
||||
const isUnlinkFile = (file: File) => {
|
||||
return file.event === "unlink" || file.event === "unlinkDir"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Stage that writes files to the destination path
|
||||
*/
|
||||
@@ -29,5 +34,3 @@ export const createWrite = (
|
||||
|
||||
return {stream}
|
||||
}
|
||||
|
||||
const isUnlinkFile = (file: File) => file.event === "unlink" || file.event === "unlinkDir"
|
||||
|
||||
@@ -1,16 +1,61 @@
|
||||
import {Writable} from "stream"
|
||||
import File from "vinyl"
|
||||
import {pipeline, through} from "./streams"
|
||||
import {Stage, StageArgs, StageConfig} from "./types"
|
||||
import {Stage, StageArgs, StageConfig, EventedFile} from "./types"
|
||||
import {agnosticSource} from "./helpers/agnostic-source"
|
||||
import {createEnrichFiles} from "./helpers/enrich-files"
|
||||
import {createFileCache} from "./helpers/file-cache"
|
||||
import {createFileCache, FileCache} from "./helpers/file-cache"
|
||||
import {createIdleHandler} from "./helpers/idle-handler"
|
||||
import {createWorkOptimizer} from "./helpers/work-optimizer"
|
||||
import {createWrite} from "./helpers/writer"
|
||||
|
||||
import {Stats} from "fs"
|
||||
export function isSourceFile(file: File) {
|
||||
return file.hash.indexOf(":") === -1
|
||||
return file.hash?.indexOf(":") === -1
|
||||
}
|
||||
|
||||
function createStageArgs(
|
||||
config: StageConfig,
|
||||
input: Writable,
|
||||
bus: Writable,
|
||||
cache: FileCache,
|
||||
): StageArgs {
|
||||
const getInputCache = () => cache
|
||||
|
||||
function processNewFile(file: File) {
|
||||
if (!file.stat) {
|
||||
// Add a stats here so we can then generate a new ID
|
||||
// during enrichment
|
||||
const stat = new Stats()
|
||||
file.stat = stat
|
||||
file.event = "add"
|
||||
}
|
||||
input.write(file)
|
||||
}
|
||||
|
||||
function processNewChildFile({
|
||||
parent,
|
||||
child,
|
||||
stageId,
|
||||
subfileId,
|
||||
}: {
|
||||
parent: EventedFile
|
||||
child: File
|
||||
stageId: string
|
||||
subfileId: string
|
||||
}) {
|
||||
child.hash = [parent.hash, stageId, subfileId].join("|")
|
||||
|
||||
processNewFile(child)
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
input,
|
||||
bus,
|
||||
getInputCache,
|
||||
processNewFile,
|
||||
processNewChildFile,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,18 +75,13 @@ export function createPipeline(
|
||||
) {
|
||||
// Helper streams don't account for business stages
|
||||
const input = through.obj()
|
||||
const optimizer = createWorkOptimizer()
|
||||
const optimizer = createWorkOptimizer(config.src, config.dest)
|
||||
const enrichFiles = createEnrichFiles()
|
||||
const srcCache = createFileCache(isSourceFile)
|
||||
const idleHandler = createIdleHandler(bus)
|
||||
|
||||
// Send this object to every stage
|
||||
const api: StageArgs = {
|
||||
config,
|
||||
input,
|
||||
bus,
|
||||
getInputCache: () => srcCache.cache,
|
||||
}
|
||||
const api = createStageArgs(config, input, bus, srcCache.cache)
|
||||
|
||||
// Initialize each stage
|
||||
const initializedStages = stages.map((stage) => stage(api))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {through, pipeline} from "./streams"
|
||||
|
||||
const defaultLogger = (file: any) => (typeof file === "string" ? file : file.path)
|
||||
|
||||
// Test expected log items
|
||||
export function testStreamItems(
|
||||
stream: NodeJS.ReadWriteStream,
|
||||
expected: any[],
|
||||
@@ -25,3 +27,22 @@ export function testStreamItems(
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function take<T>(stream: NodeJS.ReadWriteStream, num: number): Promise<T[]> {
|
||||
return new Promise((done) => {
|
||||
let items: T[] = []
|
||||
const st = pipeline(
|
||||
stream,
|
||||
through.obj((item, _, next) => {
|
||||
items.push(item)
|
||||
if (items.length === num) {
|
||||
st.end()
|
||||
setImmediate(() => {
|
||||
done(items)
|
||||
})
|
||||
}
|
||||
next(null, item)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user