1
0
mirror of synced 2026-02-09 09:00:09 -05:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Brandon Bayer
77b0730aaf working, but requires ctx change 2020-09-10 10:42:04 -04:00
Brandon Bayer
5184897a99 fix 2020-09-10 10:29:27 -04:00
Brandon Bayer
43644d7b64 working!! 2020-09-10 10:16:57 -04:00
Brandon Bayer
5e1c2bf1fb working with interfaces 2020-09-10 10:03:55 -04:00
130 changed files with 932 additions and 2036 deletions

View File

@@ -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,6 +565,15 @@
"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",
@@ -887,8 +896,7 @@
"avatar_url": "https://avatars1.githubusercontent.com/u/8602086?v=4",
"profile": "http://ricardotrejos.tech",
"contributions": [
"code",
"doc"
"code"
]
},
{
@@ -982,192 +990,6 @@
"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,

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<img alt="" src="https://img.shields.io/badge/Join%20our%20community-6700EB.svg?style=for-the-badge&labelColor=000000&logoWidth=20&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ9SURBVHgB7d3dVdtAEIbhcSpICUoH0IEogQqSVBBSAU4FSSpIOoAORAfQgSghHXzZ1U/YcMD4R9rZmf2ec3y448LyiNf27iLiGIAmPLrweC9Un3DhrzG6EarLNP09nlwJ1SOZ/lQr5N80/S/p2QMVCBf5N17XCfm1Y/rBHqjAG9PPHvBsz+mf9WAP+HLA9M/YA14cOP2payH7jpj+VCtk1wnTP+vj7xCy6cTpn7EHLMLp059iD1iD8eveJbVCNsSLheX1YA/YgOWnf8YeKB3Wmf7Ud6Fy4f/FHmtpxbl3YlC4MJ/Cj0bWdwPnPbARg+L0S54XQHS32WwuxClzd4CM0z9rPfeAuTtA5ulPXYQ7wZ04Y+oOoDD9KZc9YOoOoDj9s4dwFzgXR6w1wIPoOvPWA9buAHEJ173o3gWiy3AnuBUHLEbgmYwvAk1/wuM8vAgexThzbwPDkx7/DHwVXfFOxP2GmsKd4Ab6zPeAyU8CI7AHFmH2BRCBPXAyk18GzUrqAXCTiR4ssyj0VFw/oCU8+e+RZ33AWz6KMaYbIIWxB+JSLs1bsbkeMN0AqakHvoku9oA2sAfqBvbAQdw0QArsgb25aYBUQT3QgT2gB+yBuqGcHij2UCqXDZACe2Anlw2QYg/QAOyBuoE98CL3DZDCuK4/rh/Q7oGL6U+TOvcNkJoijN8X1C48+T+g75eQDrAH/qmqAVJgDwyqaoAUe4AGYA/UDZX3QLUNkEIZPRCd5+6BahsgVUgPROwBTSijB7jpVAvGHriHvmw9wAZ4BpX1ABvgmakHtPcbRuwBTWAPULgAV9D/jKDY9YRvwvgEaurD44uQHvAol7qBW7WKluVtIHiUS7GyvA0s6CiXDnxrpQfsgbqBS7GKk/2jYHCrVlGyfxTMrVo0ALdq1Q3sgSKofh0M9oA61a+D2QM0AHugbmAPqClmSRjK2apVVQ8UsySsoK1aHdgDesCtWnUDeyCrIpeFg1u3sylyWTi3btMA7IG6gT2wuuK3hoE9sKrit4YVslWLPaAN7IG6ocKt2zmY2h4O9sDiTG0PZw/QANy6XTewBxZj9ogYVHy025LMHhEz9cBn0We6B0yfERReBLfhx0/R1YQHPx/QBPbA0VwcEwf2wNFcHBPHHjiem3MC2QPHcXdSaJjA+KfgTPQ8hhfjBzHC40mhlzJ+Xq9lK4a4PCs43AVaGTed5mZq+iOXZwWHi3AnOj2wFWNcnxYe7gTxLtBKHuamP/J+Wnh8a5irB7ZC5Yk9gPX1QuXC+usHWqGyhYvUYR0a7zboUOFCNVhnk0krZAOW7wFOvzXhom2xnEbIHizTA1wEYhWW6YFGyC6c1gOcfg9wfA80Qj7g8B7g9HuCww+haIR8wf49wOn3Cvv9k8tGyC/s7gFOv3fY3QONkH+v9MBWqB7PeqDn9FcIT//kcitUn6kHOu/T/xfWzlQy3dEHhwAAAABJRU5ErkJggg==">
</a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-122-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-103-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<a aria-label="License" href="https://github.com/blitz-js/blitz/blob/canary/LICENSE">
<img alt="" src="https://img.shields.io/npm/l/blitz.svg?style=for-the-badge&labelColor=000000&color=blue">
@@ -116,35 +116,23 @@ 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="200px">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/Fauna_Logo_Blue.png" width="175px">
</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>
@@ -200,7 +188,6 @@ _Issue triage, pull request triage, community encouragement and moderation, etc_
</tr>
<tr>
<td align="center"><a href="https://twitter.com/ivandevp"><img src="https://avatars3.githubusercontent.com/u/9284690?v=4" width="100px;" alt=""/><br /><sub><b>Ivan Medina</b></sub></a></td>
<td align="center"><a href="https://twitter.com/nitaking_"><img src="https://avatars2.githubusercontent.com/u/10850034?v=4" width="100px;" alt=""/><br /><sub><b>Satoshi Nitawaki</b></sub></a></td>
</tr>
</table>
<!-- markdownlint-enable -->
@@ -263,7 +250,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>
@@ -291,94 +278,70 @@ 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://github.com/Weilbyte"><img src="https://avatars1.githubusercontent.com/u/43392677?v=4" width="100px;" alt=""/><br /><sub><b>Weilbyte</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Weilbyte" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Weilbyte" title="Documentation">📖</a></td>
<td align="center"><a href="http://ricardotrejos.tech"><img src="https://avatars1.githubusercontent.com/u/8602086?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Trejos</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cardotrejos" title="Code">💻</a></td>
<td align="center"><a href="https://gkaragkiaouris.tech/"><img src="https://avatars0.githubusercontent.com/u/8822835?v=4" width="100px;" alt=""/><br /><sub><b>George Karagkiaouris</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=karaggeorge" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=karaggeorge" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/brady-pascoe-3bba6b13a/"><img src="https://avatars0.githubusercontent.com/u/18705892?v=4" width="100px;" alt=""/><br /><sub><b>Brady Pascoe</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=bpas247" title="Code">💻</a></td>
<td align="center"><a href="https://www.yeahcoach.com"><img src="https://avatars1.githubusercontent.com/u/761766?v=4" width="100px;" alt=""/><br /><sub><b>Jirka Svoboda</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=svobik7" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/alan2207"><img src="https://avatars3.githubusercontent.com/u/12713315?v=4" width="100px;" alt=""/><br /><sub><b>Alan Alickovic</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=alan2207" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=alan2207" title="Documentation">📖</a></td>
<td align="center"><a href="https://yngve.hoiseth.net"><img src="https://avatars0.githubusercontent.com/u/8469540?v=4" width="100px;" alt=""/><br /><sub><b>Yngve Høiseth</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=yhoiseth" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/bruno_crosier"><img src="https://avatars1.githubusercontent.com/u/18399089?v=4" width="100px;" alt=""/><br /><sub><b>Bruno Crosier</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=brunocrosier" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://twitter.com/bruno_crosier"><img src="https://avatars1.githubusercontent.com/u/18399089?v=4" width="100px;" alt=""/><br /><sub><b>Bruno Crosier</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=brunocrosier" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jschepmans"><img src="https://avatars2.githubusercontent.com/u/5782977?v=4" width="100px;" alt=""/><br /><sub><b>Johan Schepmans</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jschepmans" title="Code">💻</a></td>
<td align="center"><a href="https://twitter.com/dillonraphael"><img src="https://avatars0.githubusercontent.com/u/3496193?v=4" width="100px;" alt=""/><br /><sub><b>Dillon Raphael</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dillonraphael" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/clgeoio"><img src="https://avatars2.githubusercontent.com/u/37571416?v=4" width="100px;" alt=""/><br /><sub><b>Cody G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/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>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -2,7 +2,6 @@ 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)
@@ -17,13 +16,8 @@ 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(
{
@@ -91,41 +85,5 @@ export default passportAuth({
done(null, {publicData})
},
),
new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL:
process.env.NODE_ENV === "production"
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/auth0/callback"
: "http://localhost:3000/api/auth/auth0/callback",
},
async function (_token, _tokenSecret, extraParams, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
// This can happen if you haven't enabled email access in your twitter app permissions
return done(new Error("GitHub OAuth response doesn't have email."))
}
const user = await db.user.upsert({
where: {email},
create: {
email,
name: profile.displayName,
},
update: {email},
})
const publicData = {
userId: user.id,
roles: [user.role],
source: "auth0",
githubUsername: profile.username,
}
done(undefined, {publicData})
},
),
],
})

View File

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

View File

@@ -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} from "app/auth/validations"
import {SignupInput, SignupInputType} from "app/auth/validations"
const SignupPage: BlitzPage = () => {
const router = useRouter()
@@ -18,7 +18,7 @@ const SignupPage: BlitzPage = () => {
<div>
<h1>Create an Account</h1>
<Form
<Form<SignupInputType>
submitText="Create Account"
schema={SignupInput}
onSubmit={async (values) => {

View File

@@ -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<S extends z.ZodType<any, any>> = {
type FormProps<FormValues> = {
/** All your form fields */
children: ReactNode
/** Text to display in the submit button */
submitText: string
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
schema?: S
onSubmit: FinalFormProps<FormValues>["onSubmit"]
initialValues?: FinalFormProps<FormValues>["initialValues"]
schema?: z.ZodType<any, any>
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
export function Form<S extends z.ZodType<any, any>>({
export function Form<FormValues extends Record<string, unknown>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<S>) {
}: FormProps<FormValues>) {
return (
<FinalForm
<FinalForm<FormValues>
initialValues={initialValues}
validate={(values) => {
if (!schema) return

View File

@@ -1,14 +1,12 @@
import {AppProps, ErrorComponent, useRouter} from "blitz"
import {AppProps, ErrorComponent} 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ export default async function getCurrentUser(_ = null, ctx: {session?: SessionCo
if (!ctx.session?.userId) return null
const user = await db.user.findOne({
where: {id: ctx.session!.userId},
where: {id: ctx.session.userId},
select: {id: true, name: true, email: true, role: true},
})

View File

@@ -10,11 +10,12 @@ type GetUserInput = {
export default async function getUser(
{where, select}: GetUserInput,
ctx: {session?: SessionContext} = {},
ctx: {session: SessionContext},
) {
ctx.session?.authorize(["admin", "user"])
ctx.session.authorize(["admin", "user"])
const user = await db.user.findOne({where, select})
const user = await db.user.findOne({where: {id: ctx.session.userId}})
// const user = await db.user.findOne({where, select})
if (!user) throw new NotFoundError(`User with id ${where.id} does not exist`)

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/auth",
"version": "0.23.1-canary.0",
"version": "0.21.2-canary.1",
"scripts": {
"start": "blitz start",
"studio": "blitz db studio",
@@ -35,9 +35,8 @@
"dependencies": {
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"blitz": "0.21.2-canary.1",
"final-form": "4.20.1",
"passport-auth0": "1.3.3",
"passport-github2": "0.1.11",
"passport-twitter": "1.0.4",
"react": "0.0.0-experimental-7f28234f8",
@@ -50,7 +49,6 @@
"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",

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

@@ -0,0 +1,8 @@
import {DefaultAuthTypes} from "blitz"
import {User} from "db"
declare module "blitz" {
export interface AuthTypes extends DefaultAuthTypes {
userId: User["id"]
}
}

View File

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

View File

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

View File

@@ -2,18 +2,17 @@ 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] = useQuery(getProduct, {where: {id}})
const [product, {mutate}] = useQuery(getProduct, {where: {id}})
return (
<ProductForm
product={product}
onSuccess={() => {
queryCache.invalidateQueries("/api/products/queries/getProducts")
onSuccess={(updatedProduct) => {
mutate(updatedProduct)
router.push("/admin/products")
}}
/>

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/store",
"version": "0.23.1-canary.0",
"version": "0.21.2-canary.1",
"private": true,
"scripts": {
"build": "blitz db migrate && blitz build",
@@ -18,12 +18,13 @@
"dependencies": {
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"blitz": "0.21.2-canary.1",
"final-form": "4.19.1",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",
"react-error-boundary": "2.3.1",
"react-final-form": "6.4.0",
"superjson": "1.2.1",
"typescript": "3.8.3"
},
"devDependencies": {

View File

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

View File

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

View File

@@ -87,7 +87,6 @@
"@wessberg/rollup-plugin-ts": "1.3.3",
"babel-eslint": "10.x",
"babel-jest": "26.3.0",
"concurrently": "5.3.0",
"cpy-cli": "3.1.1",
"cross-env": "7.0.2",
"debug": "4.1.1",
@@ -95,8 +94,6 @@
"directory-tree": "2.2.4",
"eslint": "7.7.0",
"eslint-config-react-app": "5.2.1",
"eslint-plugin-es": "mysticatea/eslint-plugin-es",
"eslint-plugin-es5": "1.5.0",
"eslint-plugin-flowtype": "5.2.0",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-jsx-a11y": "6.3.1",

View File

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

View File

@@ -5,16 +5,12 @@ import chalk from "chalk"
import {parseSemver} from "../utils/parse-semver"
async function main() {
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`,
),
)
}
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(
@@ -41,6 +37,7 @@ async function main() {
const cli = require(cliPkgPath)
const options = require("minimist")(process.argv.slice(2))
const hasVersionFlag = options._.length === 0 && (options.v || options.version)
const hasVerboseFlag = options._.length === 0 && (options.V || options.verbose)

View File

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

View File

@@ -1,4 +1,6 @@
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"
@@ -10,7 +12,7 @@ export class Build extends Command {
}
try {
await require("@blitzjs/server").build(config)
await build(config, runPrismaGeneration({silent: true, failSilently: true}))
} catch (err) {
console.error(err)
process.exit(1) // clean up?

View File

@@ -1,7 +1,16 @@
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"
const projectRoot = require("pkg-dir").sync() || process.cwd()
const isTypescript = require("fs").existsSync(require("path").join(projectRoot, "tsconfig.json"))
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"))
export class Console extends Command {
static description = "Run the Blitz console REPL"
@@ -13,17 +22,19 @@ 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) {
require("../utils/setup-ts-node").setupTsnode()
setupTsnode()
}
require("@blitzjs/repl").runRepl(Console.replOptions)
await runPrismaGeneration({silent: true, failSilently: true})
spinner.succeed()
runRepl(Console.replOptions)
}
}

View File

@@ -1,7 +1,17 @@
import {Command, flags} from "@oclif/command"
import {resolveBinAsync} from "@blitzjs/server"
import {log} from "@blitzjs/display"
import {Command, flags} from "@oclif/command"
import chalk from "chalk"
import {spawn} from "cross-spawn"
import {prompt} from "enquirer"
import * as fs from "fs"
import * as path from "path"
import {promisify} from "util"
import {projectRoot} from "../utils/get-project-root"
import pEvent from "p-event"
import {getConfig, getSchema} from "@prisma/sdk"
const getPrismaBin = () => require("@blitzjs/server").resolveBinAsync("@prisma/cli", "prisma")
const getPrismaBin = () => resolveBinAsync("@prisma/cli", "prisma")
let prismaBin: string
let schemaArg: string
@@ -16,11 +26,11 @@ const runPrisma = async (args: string[], silent = false) => {
}
}
const cp = require("cross-spawn").spawn(prismaBin, args, {
const cp = spawn(prismaBin, args, {
stdio: silent ? "ignore" : "inherit",
env: process.env,
})
const code = await require("p-event")(cp, "exit", {rejectionEvents: []})
const code = await pEvent(cp, "exit", {rejectionEvents: []})
return code === 0
}
@@ -43,8 +53,8 @@ export const runPrismaGeneration = async ({silent = false, failSilently = false}
}
}
const runMigrateUp = async ({silent = false} = {}, schemaArgLocal = schemaArg) => {
const args = ["migrate", "up", schemaArgLocal, "--create-db", "--experimental"]
const runMigrateUp = async ({silent = false} = {}) => {
const args = ["migrate", "up", schemaArg, "--create-db", "--experimental"]
if (process.env.NODE_ENV === "production" || silent) {
args.push("--auto-approve")
@@ -59,17 +69,16 @@ const runMigrateUp = async ({silent = false} = {}, schemaArgLocal = schemaArg) =
return runPrismaGeneration({silent})
}
export const runMigrate = async (flags: object = {}, schemaArgLocal = schemaArg) => {
export const runMigrate = async (name?: string) => {
if (process.env.NODE_ENV === "production") {
return runMigrateUp({}, schemaArgLocal)
return runMigrateUp()
}
// @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 silent = Boolean(name)
const args = ["migrate", "save", schemaArg, "--create-db", "--experimental"]
if (name) {
args.push("--name", name)
}
const success = await runPrisma(args, silent)
@@ -77,7 +86,7 @@ export const runMigrate = async (flags: object = {}, schemaArgLocal = schemaArg)
throw new Error("Migration failed")
}
return runMigrateUp({silent}, schemaArgLocal)
return runMigrateUp({silent})
}
export async function resetPostgres(connectionString: string, db: any): Promise<void> {
@@ -122,12 +131,8 @@ export async function resetMysql(connectionString: string, db: any): Promise<voi
export async function resetSqlite(connectionString: string): Promise<void> {
const relativePath = connectionString.replace(/^file:/, "").replace(/^(?:\.\.[\\/])+/, "")
const dbPath = require("path").join(
require("../utils/get-project-root").projectRoot,
"db",
relativePath,
)
const unlink = require("util").promisify(require("fs").unlink)
const dbPath = path.join(projectRoot, "db", relativePath)
const unlink = promisify(fs.unlink)
try {
// delete database from folder
await unlink(dbPath)
@@ -151,21 +156,17 @@ export function getDbName(connectionString: string): string {
export class Db extends Command {
static description = `Run database commands
${require("chalk").bold(
"migrate",
)} Run any needed migrations via Prisma 2 and generate Prisma Client.
${chalk.bold("migrate")} Run any needed migrations via Prisma 2 and generate Prisma Client.
${require("chalk").bold(
${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.
${require("chalk").bold(
${chalk.bold(
"studio",
)} Open the Prisma Studio UI at http://localhost:5555 so you can easily see and change data in your database.
${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.
${chalk.bold("reset")} Reset the database and run a fresh migration via Prisma 2.
`
static args = [
@@ -181,25 +182,21 @@ ${require("chalk").bold(
help: flags.help({char: "h"}),
// Used by `new` command to perform the initial migration
name: flags.string({hidden: true}),
// Used by `reset` command to skip the confirmation prompt
force: flags.boolean({char: "f", hidden: true}),
}
static strict = false
async run() {
const {args, flags} = this.parse(Db)
const command = args["command"]
// Needs to happen at run-time since the `new` command needs to change the cwd before running
const schemaPath = require("path").join(process.cwd(), "db", "schema.prisma")
const schemaPath = path.join(process.cwd(), "db", "schema.prisma")
schemaArg = `--schema=${schemaPath}`
if (command === "migrate" || command === "m") {
try {
return await runMigrate(flags)
return await runMigrate(flags.name)
} catch (error) {
if (Object.keys(flags).length > 0) {
if (flags.name) {
throw error
} else {
process.exit(1)
@@ -217,45 +214,41 @@ ${require("chalk").bold(
}
if (command === "reset") {
const forceSkipConfirmation = flags.force
const spinner = log.spinner("Loading your database").start()
await runPrismaGeneration({silent: true, failSilently: true})
spinner.succeed()
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?",
})
const {confirm} = await prompt<{confirm: string}>({
type: "confirm",
name: "confirm",
message: "Are you sure you want to reset your database and erase ALL data?",
})
if (!confirm) {
return
}
if (!confirm) {
return
}
log.progress("Resetting your database...")
const {projectRoot} = require("../utils/get-project-root")
const prismaClientPath = require.resolve("@prisma/client", {paths: [projectRoot]})
const {PrismaClient} = require(prismaClientPath)
const db = new PrismaClient()
const schemaPath = require("path").join(projectRoot, "db/schema.prisma")
const datamodel = await require("@prisma/sdk").getSchema(schemaPath)
const config = await require("@prisma/sdk").getConfig({datamodel})
const schemaPath = path.join(projectRoot, "db/schema.prisma")
const datamodel = await getSchema(schemaPath)
const config = await getConfig({datamodel})
const dataSource = config.datasources[0]
const providerType = dataSource.activeProvider
const connectionString = dataSource.url.value
if (providerType === "postgresql") {
await resetPostgres(connectionString, db)
return
resetPostgres(connectionString, db)
} else if (providerType === "mysql") {
await resetMysql(connectionString, db)
return
resetMysql(connectionString, db)
} else if (providerType === "sqlite") {
await resetSqlite(connectionString)
return
resetSqlite(connectionString)
} else {
log.error("Could not find a valid database configuration")
return
this.log("Could not find a valid database configuration")
}
return
}
if (command === "help") {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
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"
@@ -19,7 +20,7 @@ export class Test extends Command {
if (watch) {
watchMode = watch === "watch" || watch === "w"
}
const packageManager = require("has-yarn")() ? "yarn" : "npm"
const packageManager = hasYarn() ? "yarn" : "npm"
if (watchMode) spawn(packageManager, ["test:watch"], {stdio: "inherit"})
else spawn(packageManager, ["test"], {stdio: "inherit"})

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ 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")
@@ -42,11 +41,6 @@ beforeAll(async () => {
["migrate", "save", schemaArg, "--create-db", "--experimental", "--name", "name"],
{stdio: "ignore", env: process.env},
]
migrateSaveWithUnknownParams = [
prismaBin,
["migrate", "save", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit", env: process.env},
]
})
describe("Db command", () => {
@@ -82,13 +76,6 @@ describe("Db command", () => {
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
@@ -150,18 +137,6 @@ 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"])
@@ -189,4 +164,10 @@ describe("Db command", () => {
expect(spawn.mock.calls.length).toBe(0)
})
it("runs db migrate silently with the right args when name flag is used", async () => {
await Db.run(["migrate", "--name", "name"])
expectDbMigrateWithNameOutcome()
})
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/core",
"description": "Blitz.js core functionality",
"version": "0.23.1-canary.0",
"version": "0.21.2-canary.1",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
@@ -40,19 +40,18 @@
"url": "https://github.com/blitz-js/blitz"
},
"dependencies": {
"@blitzjs/config": "0.23.1-canary.0",
"@blitzjs/display": "0.23.1-canary.0",
"@blitzjs/config": "0.21.2-canary.1",
"@blitzjs/display": "0.21.2-canary.1",
"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.23.0",
"react-query": "2.5.11",
"serialize-error": "6.0.0",
"superjson": "1.2.2",
"url": "0.11.0"
"superjson": "1.2.1",
"url": "0.11.0",
"utility-types": "3.10.0"
},
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"
}

View File

@@ -1,4 +1,4 @@
import {NextPage, NextComponentType, NextPageContext} from "next"
import {NextPage, NextComponentType} from "next"
import {AppProps as NextAppProps} from "next/app"
export * from "./use-query"
@@ -49,10 +49,10 @@ 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 type BlitzComponentType = NextComponentType
export interface AppProps<P = {}> extends NextAppProps<P> {
Component: BlitzComponentType<NextPageContext, any, P> & {
export interface AppProps extends NextAppProps {
Component: BlitzComponentType & {
getLayout?: (component: JSX.Element) => JSX.Element
}
}

View File

@@ -1,14 +1,10 @@
/* 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 {
protocol?: string
}
export interface MiddlewareRequest extends BlitzApiRequest {}
export interface MiddlewareResponse extends BlitzApiResponse {
/**
* This will be passed as the second argument to Blitz queries/mutations.

View File

@@ -1,6 +1,4 @@
/* 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 {BlitzApiRequest, BlitzApiResponse} from "."
import {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
@@ -9,17 +7,14 @@ import {
} from "./middleware"
import {SessionContext, PublicData} from "./supertokens"
import {log} from "@blitzjs/display"
import passport, {AuthenticateOptions, Strategy} from "passport"
import passport, {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 = {
@@ -39,23 +34,19 @@ 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[] = [
connectMiddleware(cookieSessionMiddleware as ConnectMiddleware),
connectMiddleware(passportMiddleware as ConnectMiddleware),
// 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(passport.session()),
]
if (config.secureProxy) {
middleware.push(secureProxyMiddleware)
}
if (!req.query.auth.length) {
return res.status(404).end()
}
@@ -80,9 +71,7 @@ export function passportAuth(config: BlitzPassportConfig) {
return next()
})
}
middleware.push(
connectMiddleware(passport.authenticate(strategy.name, {...config.authenticateOptions})),
)
middleware.push(connectMiddleware(passport.authenticate(strategy.name)))
} else if (req.query.auth[1] === "callback") {
log.info(`Processing callback for ${strategy.name}...`)
middleware.push(

View File

@@ -19,7 +19,7 @@ type Options = {
resultOfGetFetchMore?: any
}
export function executeRpcCall(url: string, params: any, opts: Options = {}) {
export async function executeRpcCall(url: string, params: any, opts: Options = {}) {
if (typeof window === "undefined") return
const headers: Record<string, any> = {
@@ -46,68 +46,56 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
serialized = serialize(params)
}
// Create a new AbortController instance for this request
const controller = new AbortController()
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,
},
}),
})
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()
}
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 payload
try {
payload = await result.json()
} catch (error) {
throw new Error(`Failed to parse json from request to ${url}`)
}
let payload
try {
payload = await result.json()
} catch (error) {
throw new Error(`Failed to parse json from request to ${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 (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
if (!opts.fromQueryHook) {
const queryKey = getQueryKey(url, params)
queryCache.setQueryData(queryKey, data)
}
return data
}
}
executeRpcCall.warm = (url: string) => {
@@ -125,18 +113,13 @@ interface ResolverEnhancement {
apiUrl: string
}
}
interface CancellablePromise<T> extends Promise<T> {
cancel?: Function
}
export interface RpcFunction {
(params: any, opts: any): CancellablePromise<any>
(params: any, opts: any): Promise<any>
}
export interface EnhancedRpcFunction extends RpcFunction, ResolverEnhancement {}
export interface EnhancedResolverModule extends ResolverEnhancement {
(input: any, ctx: Record<string, any>): CancellablePromise<unknown>
(input: any, ctx: Record<string, any>): Promise<unknown>
middleware?: Middleware[]
}

View File

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

View File

@@ -1,23 +0,0 @@
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"
}

View File

@@ -2,6 +2,7 @@ import {useState} from "react"
import BadBehavior from "bad-behavior"
import {useIsomorphicLayoutEffect} from "./utils/hooks"
import {queryCache} from "react-query"
import {DeepNonNullable} from "utility-types"
export const TOKEN_SEPARATOR = ";"
export const HANDLE_SEPARATOR = ":"
@@ -27,14 +28,19 @@ function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
}
export interface PublicData extends Record<any, any> {
export interface DefaultAuthTypes {
userId: any
}
export interface AuthTypes extends DefaultAuthTypes {}
export interface PublicData extends Record<any, any> {
userId: AuthTypes["userId"] | null
roles: string[]
}
export interface SessionModel extends Record<any, any> {
handle: string
userId?: any
userId?: AuthTypes["userId"]
expiresAt?: Date
hashedSessionToken?: string
antiCSRFToken?: string
@@ -47,31 +53,40 @@ export type SessionConfig = {
method?: "essential" | "advanced"
sameSite?: "none" | "lax" | "strict"
getSession: (handle: string) => Promise<SessionModel | null>
getSessions: (userId: any) => Promise<SessionModel[]>
getSessions: (userId: AuthTypes["userId"]) => Promise<SessionModel[]>
createSession: (session: SessionModel) => Promise<SessionModel>
updateSession: (handle: string, session: Partial<SessionModel>) => Promise<SessionModel>
deleteSession: (handle: string) => Promise<SessionModel>
unstable_isAuthorized: (userRoles: string[], input?: any) => boolean
}
export interface SessionContext {
export interface SessionContextBase {
/**
* null if anonymous
*/
userId: any
userId: unknown
roles: string[]
handle: string | null
publicData: PublicData
authorize: (input?: any) => void
isAuthorized: (input?: any) => boolean
authorize(input?: any): asserts this is AuthenticatedSessionContext
isAuthorized(input?: any): boolean
// authorize: (roleOrRoles?: string | string[]) => void
// isAuthorized: (roleOrRoles?: string | string[]) => boolean
create: (publicData: PublicData, privateData?: Record<any, any>) => Promise<void>
revoke: () => Promise<void>
revokeAll: () => Promise<void>
getPrivateData: () => Promise<Record<any, any>>
setPrivateData: (data: Record<any, any>) => Promise<void>
setPublicData: (data: Record<any, any>) => Promise<void>
create(publicData: DeepNonNullable<PublicData>, privateData?: Record<any, any>): Promise<void>
revoke(): Promise<void>
revokeAll(): Promise<void>
getPrivateData(): Promise<Record<any, any>>
setPrivateData(data: Record<any, any>): Promise<void>
setPublicData(data: Record<any, any>): Promise<void>
}
export interface AuthenticatedSessionContext extends SessionContextBase {
userId: AuthTypes["userId"]
}
// Anonymous session context
export interface SessionContext extends SessionContextBase {
userId: AuthTypes["userId"] | null
}
// Taken from https://github.com/HenrikJoreteg/cookie-getter

View File

@@ -1,9 +1,9 @@
import {
useInfiniteQuery as useInfiniteReactQuery,
InfiniteQueryResult,
InfiniteQueryConfig,
InfiniteQueryOptions,
} from "react-query"
import {emptyQueryFn, retryFunction} from "./use-query"
import {useIsDevPrerender, emptyQueryFn, retryFunction} from "./use-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {getQueryCacheFunctions, QueryCacheFunctions, getInfiniteQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
@@ -14,12 +14,10 @@ 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: InfiniteQueryConfig<PromiseReturnType<T>, any>,
options: InfiniteQueryOptions<PromiseReturnType<T>, any>,
): [PromiseReturnType<T>[], RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
@@ -31,13 +29,15 @@ export function useInfiniteQuery<T extends QueryFn>(
)
}
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryRpcFn = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getInfiniteQueryKey(queryFn, params)
const {data, ...queryRest} = useInfiniteReactQuery({
queryKey,
queryFn: (_infinite: boolean, _apiUrl: string, params: any, resultOfGetFetchMore?: any) =>
queryFn: (_infinite, _apiUrl, params, resultOfGetFetchMore?) =>
queryRpcFn(params, {fromQueryHook: true, resultOfGetFetchMore}),
config: {
suspense: true,

View File

@@ -1,9 +1,9 @@
import {
usePaginatedQuery as usePaginatedReactQuery,
PaginatedQueryResult,
PaginatedQueryConfig,
QueryOptions,
} from "react-query"
import {emptyQueryFn, retryFunction} from "./use-query"
import {useIsDevPrerender, emptyQueryFn, retryFunction} from "./use-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
@@ -14,12 +14,10 @@ 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?: PaginatedQueryConfig<PromiseReturnType<T>>,
options?: QueryOptions<PaginatedQueryResult<PromiseReturnType<T>>>,
): [PromiseReturnType<T>, RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
@@ -31,13 +29,15 @@ export function usePaginatedQuery<T extends QueryFn>(
)
}
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryRpcFn = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getQueryKey(queryFn, params)
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
queryKey,
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
queryFn: (_apiUrl, params) => queryRpcFn(params, {fromQueryHook: true}),
config: {
suspense: true,
retry: retryFunction,

View File

@@ -1,6 +1,5 @@
import {useRouter} from "next/router"
import {useRouterQuery} from "./use-router-query"
import {fromPairs} from "lodash"
type ParsedUrlQueryValue = string | string[] | undefined
@@ -32,7 +31,7 @@ function areQueryValuesEqual(value1: ParsedUrlQueryValue, value2: ParsedUrlQuery
}
export function extractRouterParams(routerQuery: ParsedUrlQuery, query: ParsedUrlQuery) {
return fromPairs(
return Object.fromEntries(
Object.entries(routerQuery).filter(
([key, value]) =>
typeof query[key] === "undefined" || !areQueryValuesEqual(value, query[key]),
@@ -52,9 +51,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
if (returnType === "string") {
const params: Record<string, string> = {}
for (const key in rawParams) {
if (typeof rawParams[key] === "string") {
params[key] = rawParams[key] as string
for (const [key, value] of Object.entries(rawParams)) {
if (typeof value === "string") {
params[key] = value
}
}
return params
@@ -62,9 +61,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
if (returnType === "number") {
const params: Record<string, number> = {}
for (const key in rawParams) {
if (rawParams[key]) {
params[key] = Number(rawParams[key])
for (const [key, value] of Object.entries(rawParams)) {
if (value) {
params[key] = Number(value)
}
}
return params
@@ -72,9 +71,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
if (returnType === "array") {
const params: Record<string, Array<string | undefined>> = {}
for (const key in rawParams) {
if (Array.isArray(rawParams[key])) {
params[key] = rawParams[key] as Array<string | undefined>
for (const [key, value] of Object.entries(rawParams)) {
if (Array.isArray(value)) {
params[key] = value
}
}
return params

View File

@@ -1,4 +1,4 @@
import {useQuery as useReactQuery, QueryResult, QueryConfig} from "react-query"
import {useQuery as useReactQuery, QueryResult, QueryOptions} from "react-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
@@ -19,6 +19,17 @@ 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
@@ -31,7 +42,7 @@ export const retryFunction = (failureCount: number, error: any) => {
export function useQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options?: QueryConfig<PromiseReturnType<T>>,
options?: QueryOptions<QueryResult<PromiseReturnType<T>>>,
): [PromiseReturnType<T>, RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("useQuery is missing the first argument - it must be a query function")
@@ -43,13 +54,15 @@ export function useQuery<T extends QueryFn>(
)
}
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryRpcFn = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getQueryKey(queryFn, params)
const {data, ...queryRest} = useReactQuery({
queryKey,
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
queryFn: (_apiUrl, params) => queryRpcFn(params, {fromQueryHook: true}),
config: {
suspense: true,
retry: retryFunction,

View File

@@ -1,10 +1,10 @@
import {QueryKey} from "react-query"
import {QueryKeyPart} from "react-query"
import {BlitzApiRequest} from "../"
import {IncomingMessage} from "http"
export const isServer = typeof window === "undefined"
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKey[]] {
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKeyPart[]] {
return [cacheKey, typeof params === "function" ? (params as Function)() : params]
}

View File

@@ -11,7 +11,7 @@ export interface QueryCacheFunctions<T> {
mutate: (newData: T | ((oldData: T | undefined) => T), opts?: MutateOptions) => void
}
export const getQueryCacheFunctions = <T>(queryKey: QueryKey): QueryCacheFunctions<T> => ({
export const getQueryCacheFunctions = <T>(queryKey: QueryKey<any>): QueryCacheFunctions<T> => ({
mutate: (newData, opts = {refetch: true}) => {
queryCache.setQueryData(queryKey, newData)
if (opts.refetch) {

View File

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

View File

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

View File

@@ -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,7 +19,12 @@ export function createEnrichFiles() {
}
if (!file.hash) {
file.hash = hash(file.path + file.stat?.mtime.toString())
const hash = crypto
.createHash("md5")
.update(JSON.stringify({path: file.path, s: file.stat?.mtime}))
.digest("hex")
file.hash = hash
}
return file

View File

@@ -0,0 +1,92 @@
# 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
}
```

View File

@@ -1,79 +1,37 @@
// 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.
*/
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) : {}
// 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> = []
const stats = {todo, done}
const reportComplete = transform.file(async (file) => {
const pathHash = getOriginalPathHash(file)
delete todo[pathHash]
if (file.event === "add") {
done[pathHash] = file.hash
const reportComplete = transform.file((file) => {
if (file.hash) {
done.push(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[pathHash] === file.hash || todo[pathHash] === file.hash) {
if (done.includes(file.hash) || todo.includes(file.hash)) {
log.debug("Rejecting because this job has been done before: " + file.path)
return next()
}
todo[pathHash] = file.hash
todo.push(file.hash)
push(file)

View File

@@ -1,31 +1,24 @@
import {createWorkOptimizer} from "."
import {take} from "../../test-utils"
import {hash} from "../../utils"
import {testStreamItems} from "../../test-utils"
import {pipeline} from "../../streams"
import {normalize, resolve} from "path"
import {normalize} from "path"
import File from "vinyl"
const pathToOneHash = hash(normalize("to/one"))
const pathToTwoHash = hash(normalize("to/two"))
const pathToThreeHash = hash(normalize("to/three"))
function logItem(fileOrString: {path: string} | string) {
if (typeof fileOrString === "string") {
return fileOrString
}
return fileOrString.path
}
describe("agnosticSource", () => {
test("basic throughput", async () => {
const saveCache = jest.fn()
const readCache = jest.fn()
const {triage, reportComplete} = createWorkOptimizer(
normalize("/path"),
normalize("/dest"),
saveCache,
readCache,
)
const {triage, reportComplete} = createWorkOptimizer()
triage.write(
new File({
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
@@ -34,7 +27,6 @@ describe("agnosticSource", () => {
hash: "two",
path: normalize("/path/to/two"),
content: Buffer.from("two"),
event: "add",
}),
)
@@ -43,31 +35,21 @@ 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)
const items = await take<File>(stream, 3)
expect(items.map(({path}) => path)).toEqual(expected)
await testStreamItems(stream, expected, logItem)
})
test("same file is rejected", async () => {
const saveCache = jest.fn()
const readCache = jest.fn()
const {triage, reportComplete} = createWorkOptimizer(
normalize("/path"),
normalize("/dest"),
saveCache,
readCache,
)
const {triage, reportComplete} = createWorkOptimizer()
triage.write(
new File({
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
@@ -76,7 +58,6 @@ describe("agnosticSource", () => {
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
@@ -85,156 +66,11 @@ 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)
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,
)
await testStreamItems(stream, expected, logItem)
})
})

View File

@@ -8,9 +8,7 @@ import {Writable} from "stream"
import {isFile} from "../../utils"
import {transform} from "../../transform"
const isUnlinkFile = (file: File) => {
return file.event === "unlink" || file.event === "unlinkDir"
}
const isUnlinkFile = (file: File) => file.event === "unlink" || file.event === "unlinkDir"
/**
* Returns a Stage that writes files to the destination path

View File

@@ -1,61 +1,16 @@
import {Writable} from "stream"
import File from "vinyl"
import {pipeline, through} from "./streams"
import {Stage, StageArgs, StageConfig, EventedFile} from "./types"
import {Stage, StageArgs, StageConfig} from "./types"
import {agnosticSource} from "./helpers/agnostic-source"
import {createEnrichFiles} from "./helpers/enrich-files"
import {createFileCache, FileCache} from "./helpers/file-cache"
import {createFileCache} 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
}
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,
}
return file.hash.indexOf(":") === -1
}
/**
@@ -75,13 +30,18 @@ export function createPipeline(
) {
// Helper streams don't account for business stages
const input = through.obj()
const optimizer = createWorkOptimizer(config.src, config.dest)
const optimizer = createWorkOptimizer()
const enrichFiles = createEnrichFiles()
const srcCache = createFileCache(isSourceFile)
const idleHandler = createIdleHandler(bus)
// Send this object to every stage
const api = createStageArgs(config, input, bus, srcCache.cache)
const api: StageArgs = {
config,
input,
bus,
getInputCache: () => srcCache.cache,
}
// Initialize each stage
const initializedStages = stages.map((stage) => stage(api))

View File

@@ -1,8 +1,6 @@
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[],
@@ -27,22 +25,3 @@ 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)
}),
)
})
}

View File

@@ -52,7 +52,7 @@ describe("transformFiles", () => {
.concat(["ready"]),
logFile,
),
transformFiles(normalize("/foo"), [], normalize("/bar"), {source, writer}),
transformFiles(normalize("/foo"), [], normalize("/bar"), {source, writer, noclean: true}),
])
}, 10000)
})

View File

@@ -1,10 +1,11 @@
import {ensureDir, pathExists, remove} from "fs-extra"
import {Transform} from "stream"
import {createDisplay} from "../display"
import {ERROR_THROWN, READY} from "../events"
import {pipe} from "../streams"
import {createPipeline} from "../pipeline"
import {pipe, through} from "../streams"
import {pathExists, ensureDir, remove} from "fs-extra"
import {through} from "../streams"
import {createDisplay} from "../display"
import {READY, ERROR_THROWN} from "../events"
import {Stage} from "../types"
import {Transform} from "stream"
type FSStreamer = {stream: NodeJS.ReadWriteStream}
@@ -15,7 +16,8 @@ type SynchronizeFilesOptions = {
bus?: Transform
source?: FSStreamer
writer?: FSStreamer
clean?: boolean
noclean?: boolean
isTypescript?: boolean
}
const defaultBus = through.obj()
@@ -39,13 +41,17 @@ export async function transformFiles(
bus = defaultBus,
source,
writer,
clean: requestClean,
noclean = false,
isTypescript = true,
} = options
if (requestClean) await clean(dest)
// HACK: cleaning the dev folder on every restart means we do more work than necessary
// TODO: remove this clean and devise a way to resolve differences in stream
if (!noclean) await clean(dest)
// const errors = createErrorsStream(reporter.stream)
const display = createDisplay()
return await new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
const config = {
cwd: src,
src,
@@ -53,6 +59,7 @@ export async function transformFiles(
include,
ignore,
watch,
isTypescript,
}
const fileTransformPipeline = createPipeline(config, stages, bus, source, writer)

View File

@@ -53,7 +53,7 @@ describe("transformFiles", () => {
["/foo/one", "/foo/two", "/foo/three"].map(normalize).concat(["ready"]),
logFile,
),
transformFiles(normalize("/foo"), [], normalize("/bar"), {source, writer}),
transformFiles(normalize("/foo"), [], normalize("/bar"), {source, writer, noclean: true}),
])
})
})

View File

@@ -1,6 +1,6 @@
import {Writable} from "stream"
import File from "vinyl"
import {FileCache} from "./helpers/file-cache"
import File from "vinyl"
export type EventedFile = {
event: "add" | "change" | "unlink" | "unlinkDir"
@@ -21,6 +21,7 @@ export type StageConfig = {
include: string[]
ignore: string[]
watch: boolean
isTypescript: boolean
}
/**
@@ -31,13 +32,6 @@ export type StageArgs = {
input: Writable
bus: Writable
getInputCache: () => FileCache
processNewFile: (file: File) => void
processNewChildFile: (a: {
parent: EventedFile
child: File
stageId: string
subfileId: string
}) => void
}
/**

View File

@@ -1,6 +1,5 @@
import File from "vinyl"
import {PipelineEvent, EventedFile} from "./types"
import crypto from "crypto"
export function isFile(file: any): file is EventedFile {
return File.isVinyl(file)
@@ -9,7 +8,3 @@ export function isFile(file: any): file is EventedFile {
export function isEvent(file: any): file is PipelineEvent {
return typeof file === "string"
}
export function hash(input: string) {
return crypto.createHash("md5").update(input).digest("hex")
}

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/generator",
"version": "0.23.1-canary.0",
"version": "0.21.2-canary.1",
"description": "File generation for the Blitz CLI",
"homepage": "https://github.com/blitz-js/blitz#readme",
"license": "MIT",
@@ -36,7 +36,7 @@
"dependencies": {
"@babel/core": "7.9.0",
"@babel/plugin-transform-typescript": "7.9.4",
"@blitzjs/display": "0.23.1-canary.0",
"@blitzjs/display": "0.21.2-canary.1",
"@types/jscodeshift": "0.7.1",
"chalk": "4.0.0",
"cross-spawn": "7.0.3",
@@ -48,7 +48,7 @@
"jscodeshift": "0.10.0",
"mem-fs": "1.1.3",
"mem-fs-editor": "6.0.0",
"node-fetch": "2.6.1",
"node-fetch": "2.6.0",
"pluralize": "8.0.0",
"recast": "0.19.1",
"username": "5.1.0",

View File

@@ -1,6 +1,5 @@
import {Generator, GeneratorOptions} from "../generator"
import {join} from "path"
import {camelCaseToKebabCase} from "../utils/kebab-case"
export interface PageGeneratorOptions extends GeneratorOptions {
ModelName: string
@@ -52,10 +51,9 @@ export class PageGenerator extends Generator<PageGeneratorOptions> {
}
getTargetDirectory() {
const kebabCaseModelName = camelCaseToKebabCase(this.options.modelNames)
const parent = this.options.parentModels
? `${this.options.parentModels}/__parentModelParam__/`
: ""
return `app/${this.getModelNamesPath()}/pages/${parent}${kebabCaseModelName}`
return `app/${this.getModelNamesPath()}/pages/${parent}${this.options.modelNames}`
}
}

View File

@@ -2,7 +2,7 @@ import {Generator, GeneratorOptions} from "../generator"
import {join} from "path"
export interface QueryGeneratorOptions extends GeneratorOptions {
rawInput: string
modelName: string
}
export class QueryGenerator extends Generator<QueryGeneratorOptions> {
@@ -12,7 +12,7 @@ export class QueryGenerator extends Generator<QueryGeneratorOptions> {
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
rawInput: this.options.rawInput,
modelName: this.options.modelName,
}
}

View File

@@ -1,3 +0,0 @@
export function camelCaseToKebabCase(transformString: string) {
return transformString.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase()
}

View File

@@ -6,7 +6,6 @@ type NpmDepResponse = {versions: Record<string, PackageInformation>}
export const fetchAllVersions = async (dependency: string) => {
const res = await got(`https://registry.npmjs.org/${dependency}`, {
retry: {limit: 3},
timeout: 3000,
responseType: "json",
}).json<NpmDepResponse>()
return Object.keys(res.versions)
@@ -17,7 +16,6 @@ type NpmDistTagsResponse = {latest: string; canary: string}
export const fetchDistTags = async (dependency: string) => {
const res = await got(`https://registry.npmjs.org/-/package/${dependency}/dist-tags`, {
retry: {limit: 3},
timeout: 3000,
responseType: "json",
}).json<NpmDistTagsResponse>()
return res

View File

@@ -1,8 +1,7 @@
# This env file should NOT be checked into source control
# This is the place for values that changed for every developer
# SQLite is ready to go out of the box, but you can switch to Postgres
# by first changing the provider from "sqlite" to "postgres" in the Prisma
# schema file and by second swapping the DATABASE_URL below.
# SQLite is ready to go out of the box, but you can switch to Postgres easily
# by swapping the DATABASE_URL.
DATABASE_URL="file:./db.sqlite"
# DATABASE_URL=postgresql://__username__@localhost:5432/__name__

View File

@@ -1,7 +1,6 @@
# THIS FILE SHOULD NOT BE CHECKED INTO YOUR VERSION CONTROL SYSTEM
# SQLite is ready to go out of the box, but you can switch to Postgres
# by first changing the provider from "sqlite" to "postgres" in the Prisma
# schema file and by second swapping the DATABASE_URL below.
# SQLite is ready to go out of the box, but you can switch to Postgres easily
# by swapping the DATABASE_URL.
DATABASE_URL="file:./db_test.sqlite"
# DATABASE_URL=postgresql://__username__@localhost:5432/__name___test

View File

@@ -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<S extends z.ZodType<any, any>> = {
type FormProps<FormValues> = {
/** All your form fields */
children: ReactNode
/** Text to display in the submit button */
submitText: string
schema?: S
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
onSubmit: FinalFormProps<FormValues>["onSubmit"]
initialValues?: FinalFormProps<FormValues>["initialValues"]
schema?: z.ZodType<any, any>
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
export function Form<S extends z.ZodType<any, any>>({
export function Form<FormValues extends Record<string, unknown>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<S>) {
}: FormProps<FormValues>) {
return (
<FinalForm
<FinalForm<FormValues>
initialValues={initialValues}
validate={(values) => {
if (!schema) return

View File

@@ -18,8 +18,6 @@ export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFi
meta: {touched, error, submitError, submitting},
} = useField(name)
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
return (
<div {...outerProps}>
<label>
@@ -27,9 +25,9 @@ export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFi
<input {...input} disabled={submitting} {...props} ref={ref} />
</label>
{touched && normalizedError && (
{touched && (error || submitError) && (
<div role="alert" style={{color: "red"}}>
{normalizedError}
{error || submitError}
</div>
)}

View File

@@ -1,16 +1,16 @@
import React, { useState, ReactNode, PropsWithoutRef } from "react"
import { Formik, FormikProps } from "formik"
import * as z from "zod"
import React, { useState, ReactNode, PropsWithoutRef } from "react";
import { Formik, FormikProps, FormikErrors } from "formik";
import * as z from "zod";
type FormProps<S extends z.ZodType<any, any>> = {
type FormProps<FormValues> = {
/** All your form fields */
children: ReactNode
children: ReactNode;
/** Text to display in the submit button */
submitText: string
schema?: S
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
initialValues?: FormikProps<z.infer<S>>["initialValues"]
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
submitText: string;
onSubmit: (values: FormValues) => Promise<void | OnSubmitResult>;
initialValues?: FormikProps<FormValues>["initialValues"];
schema?: z.ZodType<any, any>;
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">;
type OnSubmitResult = {
FORM_ERROR?: string
@@ -19,40 +19,44 @@ type OnSubmitResult = {
export const FORM_ERROR = "FORM_ERROR"
export function Form<S extends z.ZodType<any, any>>({
export function Form<FormValues extends Record<string, unknown>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<S>) {
const [formError, setFormError] = useState<string | null>(null)
}: FormProps<FormValues>) {
const [formError, setFormError] = useState<string | null>(null);
return (
<Formik
initialValues={initialValues || {}}
<Formik<FormValues>
initialValues={initialValues || {} as FormValues}
validate={(values) => {
if (!schema) return
if (!schema) return;
try {
schema.parse(values)
schema.parse(values);
} catch (error) {
return error.formErrors.fieldErrors
return error.formErrors.fieldErrors;
}
}}
onSubmit={async (values, { setErrors }) => {
const { FORM_ERROR, ...otherErrors } = (await onSubmit(values)) || {}
onSubmit={async (values, {setErrors}) => {
const {FORM_ERROR, ...otherErrors} = (await onSubmit(values as FormValues)) || {}
if (FORM_ERROR) {
setFormError(FORM_ERROR)
}
if(FORM_ERROR) {
setFormError(FORM_ERROR);
}
if (Object.keys(otherErrors).length > 0) {
setErrors(otherErrors)
}
if(Object.keys(otherErrors).length > 0) {
setErrors(otherErrors as FormikErrors<FormValues>)
}
}}
>
{({ handleSubmit, isSubmitting }) => (
<form onSubmit={handleSubmit} className="form" {...props}>
{({ handleSubmit, isSubmitting, }) => (
<form
onSubmit={handleSubmit}
className="form"
{...props}
>
{/* Form fields supplied as children are rendered here */}
{children}
@@ -74,7 +78,7 @@ export function Form<S extends z.ZodType<any, any>>({
</form>
)}
</Formik>
)
);
}
export default Form
export default Form;

View File

@@ -2,14 +2,14 @@ import React, { useState, ReactNode, PropsWithoutRef } from "react"
import { FormProvider, useForm, UseFormOptions } from "react-hook-form"
import * as z from "zod"
type FormProps<S extends z.ZodType<any, any>> = {
type FormProps<FormValues> = {
/** All your form fields */
children: ReactNode
/** Text to display in the submit button */
submitText: string
schema?: S
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
initialValues?: UseFormOptions<z.infer<S>>["defaultValues"]
onSubmit: (values: FormValues) => Promise<void | OnSubmitResult>
initialValues?: UseFormOptions<FormValues>["defaultValues"]
schema?: z.ZodType<any, any>
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
type OnSubmitResult = {
@@ -19,15 +19,15 @@ type OnSubmitResult = {
export const FORM_ERROR = "FORM_ERROR"
export function Form<S extends z.ZodType<any, any>>({
export function Form<FormValues extends Record<string, unknown>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<S>) {
const ctx = useForm<z.infer<S>>({
}: FormProps<FormValues>) {
const ctx = useForm<FormValues>({
mode: "onBlur",
resolver: async (values) => {
try {
@@ -36,7 +36,7 @@ export function Form<S extends z.ZodType<any, any>>({
}
return { values, errors: {} }
} catch (error) {
return { values: {}, errors: error.formErrors?.fieldErrors }
return { values: {}, errors: error.formErrors?.fieldErrors } as any
}
},
defaultValues: initialValues,
@@ -47,7 +47,7 @@ export function Form<S extends z.ZodType<any, any>>({
<FormProvider {...ctx}>
<form
onSubmit={ctx.handleSubmit(async (values) => {
const result = (await onSubmit(values)) || {}
const result = (await onSubmit(values as FormValues)) || {}
for (const [key, value] of Object.entries(result)) {
if (key === FORM_ERROR) {
setFormError(value)

View File

@@ -18,7 +18,7 @@ export const verifyPassword = async (hashedPassword: string, password: string) =
}
export const authenticateUser = async (email: string, password: string) => {
const user = await db.user.findOne({ where: { email: email.toLowerCase() } })
const user = await db.user.findOne({ where: { email } })
if (!user || !user.hashedPassword) throw new AuthenticationError()

View File

@@ -1,9 +1,8 @@
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 } from "app/auth/validations"
import { LoginInput, LoginInputType } from "app/auth/validations"
type LoginFormProps = {
onSuccess?: () => void
@@ -14,14 +13,14 @@ export const LoginForm = (props: LoginFormProps) => {
<div>
<h1>Login</h1>
<Form
<Form<LoginInputType>
submitText="Log In"
schema={LoginInput}
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
try {
await login({ email: values.email, password: values.password })
props.onSuccess?.()
props.onSuccess && props.onSuccess()
} catch (error) {
if (error.name === "AuthenticationError") {
return { [FORM_ERROR]: "Sorry, those credentials are invalid" }
@@ -37,10 +36,6 @@ export const LoginForm = (props: LoginFormProps) => {
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
</Form>
<div style={{ marginTop: "1rem" }}>
Or <Link href="/signup">Sign Up</Link>
</div>
</div>
)
}

View File

@@ -2,7 +2,7 @@ import React from "react"
import { LabeledTextField } from "app/components/LabeledTextField"
import { Form, FORM_ERROR } from "app/components/Form"
import signup from "app/auth/mutations/signup"
import { SignupInput } from "app/auth/validations"
import { SignupInput, SignupInputType } from "app/auth/validations"
type SignupFormProps = {
onSuccess?: () => void
@@ -13,14 +13,14 @@ export const SignupForm = (props: SignupFormProps) => {
<div>
<h1>Create an Account</h1>
<Form
<Form<SignupInputType>
submitText="Create Account"
schema={SignupInput}
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
try {
await signup({ email: values.email, password: values.password })
props.onSuccess?.()
props.onSuccess && props.onSuccess()
} catch (error) {
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
// This error comes from Prisma

View File

@@ -12,7 +12,7 @@ export default async function signup(
const hashedPassword = await hashPassword(password)
const user = await db.user.create({
data: { email: email.toLowerCase(), hashedPassword, role: "user" },
data: { email, hashedPassword, role: "user" },
select: { id: true, name: true, email: true, role: true },
})

View File

@@ -6,17 +6,15 @@ type LayoutProps = {
children: ReactNode
}
const Layout = ({ title, children }: LayoutProps) => {
return (
<>
<Head>
<title>{title || "__name__"}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
const Layout = ({ title, children }: LayoutProps) => (
<>
<Head>
<title>{title || "__name__"}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
{children}
</>
)
}
{children}
</>
)
export default Layout

View File

@@ -1,16 +1,14 @@
import { AppProps, ErrorComponent, useRouter } from "blitz"
import { AppProps, ErrorComponent } from "blitz"
import { ErrorBoundary, FallbackProps } from "react-error-boundary"
import { queryCache } from "react-query"
import LoginForm from "app/auth/components/LoginForm"
export default function App({ Component, pageProps }: AppProps) {
const getLayout = Component.getLayout || ((page) => page)
const router = useRouter()
return (
<ErrorBoundary
FallbackComponent={RootErrorFallback}
resetKeys={[router.asPath]}
onReset={() => {
// This ensures the Blitz useQuery hooks will automatically refetch
// data any time you reset the error boundary

View File

@@ -2,7 +2,7 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "sqlite"
provider = ["sqlite", "postgres"]
url = env("DATABASE_URL")
}

View File

@@ -1,16 +0,0 @@
// import db from "./index"
/*
* This seed function is executed when you run `blitz db seed`.
*
* Probably you want to use a library like https://chancejs.com
* or https://github.com/Marak/Faker.js to easily generate
* realistic data.
*/
const seed = async () => {
// for (let i = 0; i < 5; i++) {
// await db.project.create({ data: { name: "Project " + i } })
// }
}
export default seed

View File

@@ -12,17 +12,14 @@
"browserslist": [
"defaults"
],
"prisma": {
"schema": "db/schema.prisma"
},
"prettier": {
"semi": false,
"printWidth": 100
},
"husky": {
"hooks": {
"pre-commit": "tsc && lint-staged && pretty-quick --staged",
"pre-push": "yarn lint && yarn test"
"pre-commit": "lint-staged && pretty-quick --staged",
"pre-push": "blitz test"
}
},
"lint-staged": {
@@ -42,13 +39,13 @@
},
"devDependencies": {
"@testing-library/jest-dom": "5.x",
"@testing-library/react": "11.x",
"@testing-library/react": "10.x",
"@testing-library/react-hooks": "3.x",
"@types/jest": "26.x",
"@types/react": "16.x",
"@types/secure-password": "3.x",
"@typescript-eslint/eslint-plugin": "4.x",
"@typescript-eslint/parser": "4.x",
"@typescript-eslint/eslint-plugin": "3.x",
"@typescript-eslint/parser": "3.x",
"babel-eslint": "10.x",
"eslint": "7.x",
"eslint-config-react-app": "5.x",
@@ -64,8 +61,8 @@
"react-test-renderer": "16.x",
"lint-staged": "10.x",
"prettier": "2.x",
"pretty-quick": "3.x",
"typescript": "4.x",
"pretty-quick": "2.x",
"typescript": "3.x",
"ts-jest": "26.x"
},
"private": true

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