Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5e5ca87ef | ||
|
|
03f860ee6f | ||
|
|
7bc8a249b4 | ||
|
|
1e3d306eb5 | ||
|
|
742ff71a97 | ||
|
|
9291ae3b38 | ||
|
|
5a5656078b | ||
|
|
0656c94885 | ||
|
|
d1f2e624e9 | ||
|
|
e4d646a643 | ||
|
|
a8a8325176 | ||
|
|
ea815e83fa | ||
|
|
869c00c950 | ||
|
|
a670693e9d | ||
|
|
5de91ad57b | ||
|
|
31899458de | ||
|
|
dce462ba53 | ||
|
|
5ebed4b05d | ||
|
|
13353793af | ||
|
|
3583a59aa8 | ||
|
|
1c5aee7c67 | ||
|
|
c87883dbe8 | ||
|
|
7d84561690 | ||
|
|
3f43ffd4fe | ||
|
|
1ac2092129 | ||
|
|
579807ff20 | ||
|
|
566e8be3c3 | ||
|
|
1b9eb77964 | ||
|
|
9f24ba10b2 | ||
|
|
f5237c31c4 | ||
|
|
3ddb3870b9 | ||
|
|
763252a5ed | ||
|
|
6a37f32322 | ||
|
|
48e27be1a7 | ||
|
|
90df4e8409 | ||
|
|
3b46d96ec8 | ||
|
|
13c5a9b802 | ||
|
|
e6ddebadf5 | ||
|
|
1bb4cf33ff | ||
|
|
36dfbe42f5 | ||
|
|
6c06f0b62c | ||
|
|
a83536be21 | ||
|
|
07f9e26827 | ||
|
|
c5e6221ebb | ||
|
|
2b0fe98cf5 | ||
|
|
58386ffe2c | ||
|
|
23fc27027a | ||
|
|
e4c00094e5 | ||
|
|
a357fd0445 | ||
|
|
c43967984b | ||
|
|
e47d947dc0 | ||
|
|
ffb54ec064 | ||
|
|
08abc33494 | ||
|
|
34722f952c | ||
|
|
4003b8ac01 | ||
|
|
160b5fc062 | ||
|
|
b722c39f79 | ||
|
|
8da7bd7cd4 | ||
|
|
712cb172eb |
@@ -353,7 +353,8 @@
|
||||
"profile": "https://github.com/ntgussoni",
|
||||
"contributions": [
|
||||
"test",
|
||||
"code"
|
||||
"code",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -971,7 +972,8 @@
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/37571416?v=4",
|
||||
"profile": "https://github.com/clgeoio",
|
||||
"contributions": [
|
||||
"code"
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1009,8 +1011,8 @@
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/36962022?v=4",
|
||||
"profile": "https://github.com/engelkes-finstreet",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1168,6 +1170,60 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jorisre",
|
||||
"name": "Joris",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/7545547?v=4",
|
||||
"profile": "https://github.com/jorisre",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Kamshak",
|
||||
"name": "Valentin Funk",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/337968?v=4",
|
||||
"profile": "https://github.com/Kamshak",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lukebennett",
|
||||
"name": "Luke Bennett",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/135390?v=4",
|
||||
"profile": "https://lukebennett.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hmajid2301",
|
||||
"name": "Haseeb Majid",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/998807?v=4",
|
||||
"profile": "https://haseebmajid.dev",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "phillippschmedt",
|
||||
"name": "Phillipp Schmedt",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/16028406?v=4",
|
||||
"profile": "https://github.com/phillippschmedt",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hasparus",
|
||||
"name": "Piotr Monwid-Olechnowicz",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/15332326?v=4",
|
||||
"profile": "https://haspar.us",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -24,7 +24,12 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"no-use-before-define": ["error", {functions: false, classes: false}],
|
||||
// note you must disable the base rule as it can report incorrect errors
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": ["error"],
|
||||
// note you must disable the base rule as it can report incorrect errors
|
||||
"no-redeclare": "off",
|
||||
"@typescript-eslint/no-redeclare": ["error"],
|
||||
},
|
||||
ignorePatterns: ["packages/cli/", "packages/generator/templates", ".eslintrc.js"],
|
||||
overrides: [
|
||||
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
12.16.1
|
||||
17
README.md
17
README.md
@@ -6,7 +6,7 @@
|
||||
<img alt="" src="https://img.shields.io/badge/Join%20our%20community-6700EB.svg?style=for-the-badge&labelColor=000000&logoWidth=20&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ9SURBVHgB7d3dVdtAEIbhcSpICUoH0IEogQqSVBBSAU4FSSpIOoAORAfQgSghHXzZ1U/YcMD4R9rZmf2ec3y448LyiNf27iLiGIAmPLrweC9Un3DhrzG6EarLNP09nlwJ1SOZ/lQr5N80/S/p2QMVCBf5N17XCfm1Y/rBHqjAG9PPHvBsz+mf9WAP+HLA9M/YA14cOP2payH7jpj+VCtk1wnTP+vj7xCy6cTpn7EHLMLp059iD1iD8eveJbVCNsSLheX1YA/YgOWnf8YeKB3Wmf7Ud6Fy4f/FHmtpxbl3YlC4MJ/Cj0bWdwPnPbARg+L0S54XQHS32WwuxClzd4CM0z9rPfeAuTtA5ulPXYQ7wZ04Y+oOoDD9KZc9YOoOoDj9s4dwFzgXR6w1wIPoOvPWA9buAHEJ173o3gWiy3AnuBUHLEbgmYwvAk1/wuM8vAgexThzbwPDkx7/DHwVXfFOxP2GmsKd4Ab6zPeAyU8CI7AHFmH2BRCBPXAyk18GzUrqAXCTiR4ssyj0VFw/oCU8+e+RZ33AWz6KMaYbIIWxB+JSLs1bsbkeMN0AqakHvoku9oA2sAfqBvbAQdw0QArsgb25aYBUQT3QgT2gB+yBuqGcHij2UCqXDZACe2Anlw2QYg/QAOyBuoE98CL3DZDCuK4/rh/Q7oGL6U+TOvcNkJoijN8X1C48+T+g75eQDrAH/qmqAVJgDwyqaoAUe4AGYA/UDZX3QLUNkEIZPRCd5+6BahsgVUgPROwBTSijB7jpVAvGHriHvmw9wAZ4BpX1ABvgmakHtPcbRuwBTWAPULgAV9D/jKDY9YRvwvgEaurD44uQHvAol7qBW7WKluVtIHiUS7GyvA0s6CiXDnxrpQfsgbqBS7GKk/2jYHCrVlGyfxTMrVo0ALdq1Q3sgSKofh0M9oA61a+D2QM0AHugbmAPqClmSRjK2apVVQ8UsySsoK1aHdgDesCtWnUDeyCrIpeFg1u3sylyWTi3btMA7IG6gT2wuuK3hoE9sKrit4YVslWLPaAN7IG6ocKt2zmY2h4O9sDiTG0PZw/QANy6XTewBxZj9ogYVHy025LMHhEz9cBn0We6B0yfERReBLfhx0/R1YQHPx/QBPbA0VwcEwf2wNFcHBPHHjiem3MC2QPHcXdSaJjA+KfgTPQ8hhfjBzHC40mhlzJ+Xq9lK4a4PCs43AVaGTed5mZq+iOXZwWHi3AnOj2wFWNcnxYe7gTxLtBKHuamP/J+Wnh8a5irB7ZC5Yk9gPX1QuXC+usHWqGyhYvUYR0a7zboUOFCNVhnk0krZAOW7wFOvzXhom2xnEbIHizTA1wEYhWW6YFGyC6c1gOcfg9wfA80Qj7g8B7g9HuCww+haIR8wf49wOn3Cvv9k8tGyC/s7gFOv3fY3QONkH+v9MBWqB7PeqDn9FcIT//kcitUn6kHOu/T/xfWzlQy3dEHhwAAAABJRU5ErkJggg==">
|
||||
</a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-122-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
|
||||
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-128-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
<a aria-label="License" href="https://github.com/blitz-js/blitz/blob/canary/LICENSE">
|
||||
<img alt="" src="https://img.shields.io/npm/l/blitz.svg?style=for-the-badge&labelColor=000000&color=blue">
|
||||
@@ -260,7 +260,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
|
||||
<td align="center"><a href="https://mikeattara.com"><img src="https://avatars1.githubusercontent.com/u/31483629?v=4" width="100px;" alt=""/><br /><sub><b>Mike Perry Y Attara</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mikeattara" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://devanthe.dev"><img src="https://avatars0.githubusercontent.com/u/354652?v=4" width="100px;" alt=""/><br /><sub><b>Devan</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=DevanB" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/jclancy93"><img src="https://avatars2.githubusercontent.com/u/7850202?v=4" width="100px;" alt=""/><br /><sub><b>Jack Clancy</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jclancy93" title="Code">💻</a> <a href="#maintenance-jclancy93" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/ntgussoni"><img src="https://avatars0.githubusercontent.com/u/10161067?v=4" width="100px;" alt=""/><br /><sub><b>Nicolas Torres</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ntgussoni"><img src="https://avatars0.githubusercontent.com/u/10161067?v=4" width="100px;" alt=""/><br /><sub><b>Nicolas Torres</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Antgussoni" title="Reviewed Pull Requests">👀</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Tests">⚠️</a> <a href="#maintenance-Skn0tt" title="Maintenance">🚧</a></td>
|
||||
@@ -346,11 +346,11 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/jschepmans"><img src="https://avatars2.githubusercontent.com/u/5782977?v=4" width="100px;" alt=""/><br /><sub><b>Johan Schepmans</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jschepmans" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://twitter.com/dillonraphael"><img src="https://avatars0.githubusercontent.com/u/3496193?v=4" width="100px;" alt=""/><br /><sub><b>Dillon Raphael</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dillonraphael" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/clgeoio"><img src="https://avatars2.githubusercontent.com/u/37571416?v=4" width="100px;" alt=""/><br /><sub><b>Cody G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/clgeoio"><img src="https://avatars2.githubusercontent.com/u/37571416?v=4" width="100px;" alt=""/><br /><sub><b>Cody G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/madflow"><img src="https://avatars0.githubusercontent.com/u/183248?v=4" width="100px;" alt=""/><br /><sub><b>madflow</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=madflow" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://twitter.com/nitaking_"><img src="https://avatars2.githubusercontent.com/u/10850034?v=4" width="100px;" alt=""/><br /><sub><b>Satoshi Nitawaki</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nitaking" title="Code">💻</a> <a href="#maintenance-nitaking" title="Maintenance">🚧</a> <a href="#question-nitaking" title="Answering Questions">💬</a></td>
|
||||
<td align="center"><a href="https://github.com/sirmyron"><img src="https://avatars2.githubusercontent.com/u/1430136?v=4" width="100px;" alt=""/><br /><sub><b>sirmyron</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sirmyron" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://twitter.com/pixelscommander"><img src="https://avatars2.githubusercontent.com/u/810671?v=4" width="100px;" alt=""/><br /><sub><b>Denis Radin</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3APixelsCommander" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Documentation">📖</a></td>
|
||||
@@ -374,11 +374,20 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
|
||||
<td align="center"><a href="http://enricoschaaf.com"><img src="https://avatars1.githubusercontent.com/u/54645197?v=4" width="100px;" alt=""/><br /><sub><b>Enrico Schaaf</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=enricoschaaf" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://kitze.io"><img src="https://avatars0.githubusercontent.com/u/1160594?v=4" width="100px;" alt=""/><br /><sub><b>Kitze</b></sub></a><br /><a href="#ideas-kitze" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/drmas"><img src="https://avatars3.githubusercontent.com/u/644440?v=4" width="100px;" alt=""/><br /><sub><b>Mohamed Shaban</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=drmas" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jorisre"><img src="https://avatars1.githubusercontent.com/u/7545547?v=4" width="100px;" alt=""/><br /><sub><b>Joris</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jorisre" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Kamshak"><img src="https://avatars3.githubusercontent.com/u/337968?v=4" width="100px;" alt=""/><br /><sub><b>Valentin Funk</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Kamshak" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://lukebennett.com"><img src="https://avatars1.githubusercontent.com/u/135390?v=4" width="100px;" alt=""/><br /><sub><b>Luke Bennett</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=lukebennett" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://haseebmajid.dev"><img src="https://avatars0.githubusercontent.com/u/998807?v=4" width="100px;" alt=""/><br /><sub><b>Haseeb Majid</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hmajid2301" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/phillippschmedt"><img src="https://avatars0.githubusercontent.com/u/16028406?v=4" width="100px;" alt=""/><br /><sub><b>Phillipp Schmedt</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=phillippschmedt" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://haspar.us"><img src="https://avatars0.githubusercontent.com/u/15332326?v=4" width="100px;" alt=""/><br /><sub><b>Piotr Monwid-Olechnowicz</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hasparus" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react"
|
||||
import {Link} from "blitz"
|
||||
import {Link, useMutation} from "blitz"
|
||||
import {LabeledTextField} from "app/components/LabeledTextField"
|
||||
import {Form, FORM_ERROR} from "app/components/Form"
|
||||
import login from "app/auth/mutations/login"
|
||||
@@ -10,6 +9,7 @@ type LoginFormProps = {
|
||||
}
|
||||
|
||||
export const LoginForm = (props: LoginFormProps) => {
|
||||
const [loginMutation] = useMutation(login)
|
||||
return (
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
@@ -19,7 +19,7 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
initialValues={{email: undefined, password: undefined}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await login({email: values.email, password: values.password})
|
||||
await loginMutation(values)
|
||||
props.onSuccess && props.onSuccess()
|
||||
} catch (error) {
|
||||
if (error.name === "AuthenticationError") {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import {Ctx} from "blitz"
|
||||
import {authenticateUser} from "app/auth/auth-utils"
|
||||
import {LoginInput, LoginInputType} from "../validations"
|
||||
|
||||
export default async function login(input: LoginInputType, ctx: {session?: SessionContext} = {}) {
|
||||
export default async function login(input: LoginInputType, {session}: Ctx) {
|
||||
// This throws an error if input is invalid
|
||||
const {email, password} = LoginInput.parse(input)
|
||||
|
||||
// This throws an error if credentials are invalid
|
||||
const user = await authenticateUser(email, password)
|
||||
|
||||
await ctx.session!.create({userId: user.id, roles: [user.role]})
|
||||
await session.create({userId: user.id, roles: [user.role]})
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import {Ctx} from "blitz"
|
||||
|
||||
export default async function logout(_ = null, ctx: {session?: SessionContext} = {}) {
|
||||
return await ctx.session!.revoke()
|
||||
export default async function logout(_: any, {session}: Ctx) {
|
||||
return await session.revoke()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Ctx} from "blitz"
|
||||
import db from "db"
|
||||
import {SessionContext} from "blitz"
|
||||
import {hashPassword} from "app/auth/auth-utils"
|
||||
import {SignupInput, SignupInputType} from "app/auth/validations"
|
||||
|
||||
export default async function signup(input: SignupInputType, ctx: {session?: SessionContext} = {}) {
|
||||
export default async function signup(input: SignupInputType, {session}: Ctx) {
|
||||
// This throws an error if input is invalid
|
||||
const {email, password} = SignupInput.parse(input)
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async function signup(input: SignupInputType, ctx: {session?: Ses
|
||||
select: {id: true, name: true, email: true, role: true},
|
||||
})
|
||||
|
||||
await ctx.session!.create({userId: user.id, roles: [user.role]})
|
||||
await session.create({userId: user.id, roles: [user.role]})
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react"
|
||||
import {Head, useRouter, BlitzPage} from "blitz"
|
||||
import {LoginForm} from "app/auth/components/LoginForm"
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react"
|
||||
import {Head, useRouter, BlitzPage} from "blitz"
|
||||
import {Head, useRouter, BlitzPage, useMutation} from "blitz"
|
||||
import {Form, FORM_ERROR} from "app/components/Form"
|
||||
import {LabeledTextField} from "app/components/LabeledTextField"
|
||||
import signup from "app/auth/mutations/signup"
|
||||
@@ -7,6 +6,7 @@ import {SignupInput} from "app/auth/validations"
|
||||
|
||||
const SignupPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
const [signupMutation] = useMutation(signup)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -23,7 +23,7 @@ const SignupPage: BlitzPage = () => {
|
||||
schema={SignupInput}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await signup({email: values.email, password: values.password})
|
||||
await signupMutation(values)
|
||||
router.push("/")
|
||||
} catch (error) {
|
||||
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {ReactNode, PropsWithoutRef} from "react"
|
||||
import {ReactNode, PropsWithoutRef} from "react"
|
||||
import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form"
|
||||
import * as z from "zod"
|
||||
export {FORM_ERROR} from "final-form"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {PropsWithoutRef} from "react"
|
||||
import {forwardRef, PropsWithoutRef} from "react"
|
||||
import {useField} from "react-final-form"
|
||||
|
||||
export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
|
||||
@@ -11,7 +11,7 @@ export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElem
|
||||
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
|
||||
}
|
||||
|
||||
export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFieldProps>(
|
||||
export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
|
||||
({name, label, outerProps, ...props}, ref) => {
|
||||
const {
|
||||
input,
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import {useSession, useRouter} from "blitz"
|
||||
import {useSession, useRouter, useMutation} from "blitz"
|
||||
import logout from "app/auth/mutations/logout"
|
||||
|
||||
export default function Layout({children}: {children: React.ReactNode}) {
|
||||
const session = useSession()
|
||||
const router = useRouter()
|
||||
const [logoutMutation] = useMutation(logout)
|
||||
return (
|
||||
<div>
|
||||
{session.userId && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
router.push("/")
|
||||
await logout()
|
||||
await logoutMutation()
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
|
||||
@@ -3,6 +3,10 @@ import {ErrorBoundary} from "react-error-boundary"
|
||||
import {queryCache} from "react-query"
|
||||
import LoginForm from "app/auth/components/LoginForm"
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window["DEBUG_BLITZ"] = 1
|
||||
}
|
||||
|
||||
export default function App({Component, pageProps}: AppProps) {
|
||||
const router = useRouter()
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Suspense} from "react"
|
||||
import {Head, Link, useSession, useRouterQuery} from "blitz"
|
||||
import getUser from "app/users/queries/getUser"
|
||||
import {Head, Link, useSession, useRouterQuery, useMutation, invoke} from "blitz"
|
||||
import trackView from "app/users/mutations/trackView"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import {useCurrentUser} from "app/hooks/useCurrentUser"
|
||||
// import getUsers from "app/users/queries/getUsers"
|
||||
|
||||
const CurrentUserInfo = () => {
|
||||
const currentUser = useCurrentUser()
|
||||
@@ -11,12 +11,21 @@ const CurrentUserInfo = () => {
|
||||
return <pre>{JSON.stringify(currentUser, null, 2)}</pre>
|
||||
}
|
||||
|
||||
// const Users = () => {
|
||||
// const [users] = useQuery(getUsers, {})
|
||||
//
|
||||
// return <pre style={{maxWidth: "30rem"}}>{JSON.stringify(users, null, 2)}</pre>
|
||||
// }
|
||||
|
||||
const UserStuff = () => {
|
||||
const session = useSession()
|
||||
const query = useRouterQuery()
|
||||
const [trackViewMutation] = useMutation(trackView)
|
||||
|
||||
if (session.isLoading) return <div>Loading...</div>
|
||||
|
||||
console.log(session.views)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!session.userId && (
|
||||
@@ -40,10 +49,15 @@ const UserStuff = () => {
|
||||
<Suspense fallback="Loading...">
|
||||
<CurrentUserInfo />
|
||||
</Suspense>
|
||||
{/*
|
||||
<Suspense fallback="Loading...">
|
||||
<Users />
|
||||
</Suspense>
|
||||
*/}
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const user = await getUser({where: {id: session.userId as number}})
|
||||
const user = await invoke(getUser, {where: {id: session.userId as number}})
|
||||
alert(JSON.stringify(user))
|
||||
} catch (error) {
|
||||
alert("error: " + JSON.stringify(error))
|
||||
@@ -55,7 +69,7 @@ const UserStuff = () => {
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await trackView()
|
||||
await trackViewMutation()
|
||||
} catch (error) {
|
||||
alert("error: " + error)
|
||||
console.log(error)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as React from "react"
|
||||
import {FC} from "react"
|
||||
import {getSessionContext} from "@blitzjs/server"
|
||||
import {
|
||||
ssrQuery,
|
||||
invokeWithMiddleware,
|
||||
useRouter,
|
||||
GetServerSideProps,
|
||||
PromiseReturnType,
|
||||
ErrorComponent as ErrorPage,
|
||||
useMutation,
|
||||
} from "blitz"
|
||||
import getUser from "app/users/queries/getUser"
|
||||
import logout from "app/auth/mutations/logout"
|
||||
@@ -30,9 +31,9 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, re
|
||||
const session = await getSessionContext(req, res)
|
||||
console.log("Session id:", session.userId)
|
||||
try {
|
||||
const user = await ssrQuery(
|
||||
const user = await invokeWithMiddleware(
|
||||
getUser,
|
||||
{where: {id: Number(session.userId)}, select: {id: true}},
|
||||
{where: {id: Number(session.userId)}},
|
||||
{res, req},
|
||||
)
|
||||
return {props: {user}}
|
||||
@@ -42,8 +43,7 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, re
|
||||
res.end()
|
||||
return {props: {}}
|
||||
} else if (error.name === "AuthenticationError") {
|
||||
res.writeHead(302, {location: "/login"})
|
||||
res.end()
|
||||
res.writeHead(302, {location: "/login"}).end()
|
||||
return {props: {}}
|
||||
} else if (error.name === "AuthorizationError") {
|
||||
return {
|
||||
@@ -60,8 +60,9 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, re
|
||||
}
|
||||
}
|
||||
|
||||
const Test: React.FC<PageProps> = ({user, error}: PageProps) => {
|
||||
const Test: FC<PageProps> = ({user, error}: PageProps) => {
|
||||
const router = useRouter()
|
||||
const [logoutMutation] = useMutation(logout)
|
||||
|
||||
if (error) {
|
||||
return <ErrorPage statusCode={error.statusCode} title={error.message} />
|
||||
@@ -72,7 +73,7 @@ const Test: React.FC<PageProps> = ({user, error}: PageProps) => {
|
||||
<div>Logged in user id: {user?.id}</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await logout()
|
||||
await logoutMutation()
|
||||
router.push("/")
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import db, {UserCreateArgs} from "db"
|
||||
import {protect} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
type CreateUserInput = {
|
||||
data: UserCreateArgs["data"]
|
||||
}
|
||||
export default async function createUser({data}: CreateUserInput, ctx: Record<any, any> = {}) {
|
||||
const user = await db.user.create({data})
|
||||
export default protect(
|
||||
{
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
},
|
||||
async function createUser(input, {session}) {
|
||||
const user = await db.user.create({data: input})
|
||||
|
||||
return user
|
||||
}
|
||||
return user
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import db, {UserDeleteArgs} from "db"
|
||||
import {protect} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
type DeleteUserInput = {
|
||||
where: UserDeleteArgs["where"]
|
||||
}
|
||||
export default protect(
|
||||
{
|
||||
schema: z.object({
|
||||
id: z.number(),
|
||||
}),
|
||||
},
|
||||
async function deleteUser({id}, {session}) {
|
||||
const user = await db.user.delete({where: {id}})
|
||||
|
||||
export default async function deleteUser({where}: DeleteUserInput, ctx: Record<any, any> = {}) {
|
||||
const user = await db.user.delete({where})
|
||||
|
||||
return user
|
||||
}
|
||||
return user
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import {Ctx} from "blitz"
|
||||
|
||||
export default async function trackView(_ = null, ctx: {session?: SessionContext} = {}) {
|
||||
const currentViews = ctx.session!.publicData.views || 0
|
||||
await ctx.session!.setPublicData({views: currentViews + 1})
|
||||
await ctx.session!.setPrivateData({views: currentViews + 1})
|
||||
export default async function trackView(_ = null, {session}: Ctx) {
|
||||
const currentViews = session.publicData.views || 0
|
||||
await session.setPublicData({views: currentViews + 1})
|
||||
await session.setPrivateData({views: currentViews + 1})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import db, {UserUpdateArgs} from "db"
|
||||
import {protect} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
type UpdateUserInput = {
|
||||
where: UserUpdateArgs["where"]
|
||||
data: UserUpdateArgs["data"]
|
||||
}
|
||||
export default protect(
|
||||
{
|
||||
schema: z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
}),
|
||||
},
|
||||
async function updateUser({id, ...data}, {session}) {
|
||||
const user = await db.user.update({where: {id}, data})
|
||||
|
||||
export default async function updateUser(
|
||||
{where, data}: UpdateUserInput,
|
||||
ctx: Record<any, any> = {},
|
||||
) {
|
||||
const user = await db.user.update({where, data})
|
||||
|
||||
return user
|
||||
}
|
||||
return user
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import React, {Suspense} from "react"
|
||||
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import {Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
|
||||
import getUser from "app/users/queries/getUser"
|
||||
import deleteUser from "app/users/mutations/deleteUser"
|
||||
|
||||
export const User = () => {
|
||||
const router = useRouter()
|
||||
const userId = useParam("userId", "number")
|
||||
const [user] = useQuery(getUser, {where: {id: userId}})
|
||||
const [user] = useQuery(getUser, {id: userId})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>User {user.id}</h1>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
|
||||
{
|
||||
<Link href="/users/[userId]/edit" as={`/users/${user.id}/edit`}>
|
||||
<a>Edit</a>
|
||||
</Link>
|
||||
}
|
||||
<Link href="/users/[userId]/edit" as={`/users/${user.id}/edit`}>
|
||||
<a>Edit</a>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (window.confirm("This will be deleted")) {
|
||||
await deleteUser({where: {id: user.id}})
|
||||
await deleteUser({id: user.id})
|
||||
router.push("/users")
|
||||
}
|
||||
}}
|
||||
@@ -37,26 +36,19 @@ export const User = () => {
|
||||
const ShowUserPage: BlitzPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>User</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<p>
|
||||
<Link href="/users">
|
||||
<a>Users</a>
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<main>
|
||||
<p>
|
||||
{
|
||||
<Link href="/users">
|
||||
<a>Users</a>
|
||||
</Link>
|
||||
}
|
||||
</p>
|
||||
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<User />
|
||||
</Suspense>
|
||||
</main>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<User />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ShowUserPage.getLayout = (page) => <Layout title={"User"}>{page}</Layout>
|
||||
|
||||
export default ShowUserPage
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {Suspense} from "react"
|
||||
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import {Link, useRouter, useQuery, useMutation, useParam, BlitzPage} from "blitz"
|
||||
import getUser from "app/users/queries/getUser"
|
||||
import updateUser from "app/users/mutations/updateUser"
|
||||
import UserForm from "app/users/components/UserForm"
|
||||
@@ -7,7 +8,8 @@ import UserForm from "app/users/components/UserForm"
|
||||
export const EditUser = () => {
|
||||
const router = useRouter()
|
||||
const userId = useParam("userId", "number")
|
||||
const [user, {mutate}] = useQuery(getUser, {where: {id: userId}})
|
||||
const [user, {mutate}] = useQuery(getUser, {id: userId})
|
||||
const [updateUserMutation] = useMutation(updateUser)
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -18,11 +20,11 @@ export const EditUser = () => {
|
||||
initialValues={user}
|
||||
onSubmit={async () => {
|
||||
try {
|
||||
const updated = await updateUser({
|
||||
where: {id: user.id},
|
||||
data: {name: "MyNewName"},
|
||||
const updated = await updateUserMutation({
|
||||
id: user.id,
|
||||
name: "MyNewName",
|
||||
})
|
||||
mutate(updated)
|
||||
await mutate(updated)
|
||||
alert("Success!" + JSON.stringify(updated))
|
||||
router.push("/users/[userId]", `/users/${updated.id}`)
|
||||
} catch (error) {
|
||||
@@ -38,26 +40,19 @@ export const EditUser = () => {
|
||||
const EditUserPage: BlitzPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Edit User</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<EditUser />
|
||||
</Suspense>
|
||||
|
||||
<main>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<EditUser />
|
||||
</Suspense>
|
||||
|
||||
<p>
|
||||
{
|
||||
<Link href="/users">
|
||||
<a>Users</a>
|
||||
</Link>
|
||||
}
|
||||
</p>
|
||||
</main>
|
||||
<p>
|
||||
<Link href="/users">
|
||||
<a>Users</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
EditUserPage.getLayout = (page) => <Layout title={"Edit User"}>{page}</Layout>
|
||||
|
||||
export default EditUserPage
|
||||
|
||||
@@ -1,49 +1,60 @@
|
||||
import React, {Suspense} from "react"
|
||||
import {Head, Link, useQuery, BlitzPage} from "blitz"
|
||||
import getUsers from "app/users/queries/getUsers"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import {Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
|
||||
import getUsers from "app/users/queries/getUsers"
|
||||
|
||||
const ITEMS_PER_PAGE = 100
|
||||
|
||||
export const UsersList = () => {
|
||||
const [users] = useQuery(getUsers, {orderBy: {id: "desc"}})
|
||||
const router = useRouter()
|
||||
const page = Number(router.query.page) || 0
|
||||
const [{users, hasMore}] = usePaginatedQuery(getUsers, {
|
||||
orderBy: {id: "asc"},
|
||||
skip: ITEMS_PER_PAGE * page,
|
||||
take: ITEMS_PER_PAGE,
|
||||
})
|
||||
|
||||
const goToPreviousPage = () => router.push({query: {page: page - 1}})
|
||||
const goToNextPage = () => router.push({query: {page: page + 1}})
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{users?.map((user) => (
|
||||
<li key={user.id}>
|
||||
<Link href="/users/[userId]" as={`/users/${user.id}`}>
|
||||
<a>{user.email}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<ul>
|
||||
{users.map((user) => (
|
||||
<li key={user.id}>
|
||||
<Link href="/users/[userId]" as={`/users/${user.id}`}>
|
||||
<a>{user.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button disabled={page === 0} onClick={goToPreviousPage}>
|
||||
Previous
|
||||
</button>
|
||||
<button disabled={!hasMore} onClick={goToNextPage}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const UsersPage: BlitzPage = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Users</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div>
|
||||
<p>
|
||||
<Link href="/users/new">
|
||||
<a>Create User</a>
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<main>
|
||||
<h1>Users</h1>
|
||||
|
||||
<p>
|
||||
{
|
||||
<Link href="/users/new">
|
||||
<a>Create User</a>
|
||||
</Link>
|
||||
}
|
||||
</p>
|
||||
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<UsersList />
|
||||
</Suspense>
|
||||
</main>
|
||||
</Layout>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<UsersList />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
UsersPage.getLayout = (page) => <Layout title={"Users"}>{page}</Layout>
|
||||
|
||||
export default UsersPage
|
||||
|
||||
@@ -1,44 +1,39 @@
|
||||
import React from "react"
|
||||
import {Head, Link, useRouter, BlitzPage} from "blitz"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import {Link, useRouter, useMutation, BlitzPage} from "blitz"
|
||||
import createUser from "app/users/mutations/createUser"
|
||||
import UserForm from "app/users/components/UserForm"
|
||||
|
||||
const NewUserPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
const [createUserMutation] = useMutation(createUser)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>New User</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<h1>Create New User</h1>
|
||||
|
||||
<main>
|
||||
<h1>Create New User </h1>
|
||||
|
||||
<UserForm
|
||||
initialValues={{}}
|
||||
onSubmit={async () => {
|
||||
try {
|
||||
const user = await createUser({data: {name: "MyName"}})
|
||||
alert("Success!" + JSON.stringify(user))
|
||||
router.push("/users/[userId]", `/users/${user.id}`)
|
||||
} catch (error) {
|
||||
alert("Error creating user " + JSON.stringify(error, null, 2))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<p>
|
||||
{
|
||||
<Link href="/users">
|
||||
<a>Users</a>
|
||||
</Link>
|
||||
<UserForm
|
||||
initialValues={{}}
|
||||
onSubmit={async () => {
|
||||
try {
|
||||
const user = await createUserMutation({name: "MyName"})
|
||||
alert("Success!" + JSON.stringify(user))
|
||||
router.push("/users/[userId]", `/users/${user.id}`)
|
||||
} catch (error) {
|
||||
alert("Error creating user " + JSON.stringify(error, null, 2))
|
||||
}
|
||||
</p>
|
||||
</main>
|
||||
}}
|
||||
/>
|
||||
|
||||
<p>
|
||||
<Link href="/users">
|
||||
<a>Users</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
NewUserPage.getLayout = (page) => <Layout title={"Create New User"}>{page}</Layout>
|
||||
|
||||
export default NewUserPage
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Ctx} from "blitz"
|
||||
import db from "db"
|
||||
import {SessionContext} from "blitz"
|
||||
|
||||
export default async function getCurrentUser(_ = null, ctx: {session?: SessionContext} = {}) {
|
||||
if (!ctx.session?.userId) return null
|
||||
export default async function getCurrentUser(_ = null, ctx: Ctx) {
|
||||
if (!ctx.session.userId) return null
|
||||
|
||||
const user = await db.user.findOne({
|
||||
where: {id: ctx.session!.userId},
|
||||
where: {id: ctx.session.userId},
|
||||
select: {id: true, name: true, email: true, role: true},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import db, {FindOneUserArgs} from "db"
|
||||
import {SessionContext, NotFoundError} from "blitz"
|
||||
import {protect, NotFoundError} from "blitz"
|
||||
import db, {FindFirstUserArgs} from "db"
|
||||
|
||||
type GetUserInput = {
|
||||
where: FindOneUserArgs["where"]
|
||||
select?: FindOneUserArgs["select"]
|
||||
// Only available if a model relationship exists
|
||||
// include?: FindOneUserArgs['include']
|
||||
}
|
||||
type GetUserInput = FindFirstUserArgs["where"]
|
||||
|
||||
export default async function getUser(
|
||||
{where, select}: GetUserInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
) {
|
||||
ctx.session?.authorize(["admin", "user"])
|
||||
export default protect({}, async function getUser(input: GetUserInput, {session}) {
|
||||
const user = await db.user.findFirst({where: input})
|
||||
|
||||
const user = await db.user.findOne({where, select})
|
||||
|
||||
if (!user) throw new NotFoundError(`User with id ${where.id} does not exist`)
|
||||
if (!user) throw new NotFoundError()
|
||||
|
||||
return user
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import {protect} from "blitz"
|
||||
import db, {FindManyUserArgs} from "db"
|
||||
import {SessionContext} from "blitz"
|
||||
|
||||
type GetUsersInput = {
|
||||
where?: FindManyUserArgs["where"]
|
||||
orderBy?: FindManyUserArgs["orderBy"]
|
||||
cursor?: FindManyUserArgs["cursor"]
|
||||
take?: FindManyUserArgs["take"]
|
||||
skip?: FindManyUserArgs["skip"]
|
||||
// Only available if a model relationship exists
|
||||
// include?: FindManyUserArgs['include']
|
||||
}
|
||||
type GetUsersInput = Pick<FindManyUserArgs, "orderBy" | "skip" | "take">
|
||||
|
||||
export default async function getUsers(
|
||||
{where, orderBy, cursor, take, skip}: GetUsersInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
export default protect({}, async function getUsers(
|
||||
{orderBy, skip = 0, take}: GetUsersInput,
|
||||
{session},
|
||||
) {
|
||||
ctx.session!.authorize(["admin"])
|
||||
|
||||
const users = await db.user.findMany({
|
||||
where,
|
||||
where: {
|
||||
// add your selection criteria here
|
||||
},
|
||||
orderBy,
|
||||
cursor,
|
||||
take,
|
||||
skip,
|
||||
})
|
||||
|
||||
return users
|
||||
}
|
||||
const count = await db.user.count()
|
||||
const hasMore = typeof take === "number" ? skip + take < count : false
|
||||
const nextPage = hasMore ? {take, skip: skip + take!} : null
|
||||
|
||||
return {
|
||||
users,
|
||||
nextPage,
|
||||
hasMore,
|
||||
count,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = withBundleAnalyzer({
|
||||
middleware: [
|
||||
sessionMiddleware({
|
||||
unstable_isAuthorized: unstable_simpleRolesIsAuthorized,
|
||||
// sessionExpiryMinutes: 1,
|
||||
sessionExpiryMinutes: 4,
|
||||
}),
|
||||
],
|
||||
/*
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
//@ts-ignore
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
||||
@@ -43,3 +43,27 @@ model Session {
|
||||
publicData String?
|
||||
privateData String?
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@examples/auth",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"studio": "blitz db studio",
|
||||
@@ -15,6 +15,9 @@
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"prisma": {
|
||||
"schema": "db/schema.prisma"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 100,
|
||||
@@ -33,9 +36,9 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"@prisma/cli": "2.8.0",
|
||||
"@prisma/client": "2.8.0",
|
||||
"blitz": "0.24.0-canary.0",
|
||||
"final-form": "4.20.1",
|
||||
"passport-auth0": "1.3.3",
|
||||
"passport-github2": "0.1.11",
|
||||
@@ -45,7 +48,7 @@
|
||||
"react-error-boundary": "2.3.1",
|
||||
"react-final-form": "6.5.1",
|
||||
"secure-password": "4.0.0",
|
||||
"zod": "1.10.0"
|
||||
"zod": "1.11.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/skip-test": "2.5.0",
|
||||
|
||||
12
examples/auth/types.ts
Normal file
12
examples/auth/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {DefaultCtx, SessionContext, DefaultPublicData} from "blitz"
|
||||
import {User} from "db"
|
||||
|
||||
declare module "blitz" {
|
||||
export interface Ctx extends DefaultCtx {
|
||||
session: SessionContext
|
||||
}
|
||||
export interface PublicData extends DefaultPublicData {
|
||||
userId: User["id"]
|
||||
views?: number
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-prisma",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"build": "blitz build",
|
||||
@@ -26,7 +26,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.24.0-canary.0",
|
||||
"knex": "0.21.2",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@examples/plain-js",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"build": "blitz db migrate && blitz build",
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.24.0-canary.0",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import {Suspense} from "react"
|
||||
import {Link, useRouter, useQuery, useParam} from "blitz"
|
||||
import getProduct from "app/products/queries/getProduct"
|
||||
import ProductForm from "app/products/components/ProductForm"
|
||||
import {queryCache} from "react-query"
|
||||
|
||||
function Product() {
|
||||
const router = useRouter()
|
||||
@@ -12,9 +11,8 @@ function Product() {
|
||||
return (
|
||||
<ProductForm
|
||||
product={product}
|
||||
onSuccess={() => {
|
||||
queryCache.invalidateQueries("/api/products/queries/getProducts")
|
||||
router.push("/admin/products")
|
||||
onSuccess={async () => {
|
||||
await router.push("/admin/products")
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Suspense} from "react"
|
||||
import {useQuery, Link, useRouterQuery} from "blitz"
|
||||
import {useQuery, Link, useRouterQuery, invalidateQuery} from "blitz"
|
||||
import getProducts from "app/products/queries/getProducts"
|
||||
import getProduct from "app/products/queries/getProduct"
|
||||
// import getProduct from "app/products/queries/getProduct"
|
||||
|
||||
function ProductsList() {
|
||||
const {orderby = "id", order = "desc"} = useRouterQuery()
|
||||
@@ -17,7 +17,12 @@ function ProductsList() {
|
||||
{products.map((product) => (
|
||||
<li key={product.id}>
|
||||
<Link href="/admin/products/[id]" as={`/admin/products/${product.id}`}>
|
||||
<a onMouseEnter={() => getProduct({where: {id: product.id}})}>{product.name}</a>
|
||||
<a
|
||||
// Disable until prefetch api added
|
||||
//onMouseEnter={() => getProduct({where: {id: product.id}})}
|
||||
>
|
||||
{product.name}
|
||||
</a>
|
||||
</Link>{" "}
|
||||
- Created: {product.createdAt.toISOString()}
|
||||
</li>
|
||||
@@ -31,6 +36,8 @@ function AdminProducts() {
|
||||
<div>
|
||||
<h1>Products</h1>
|
||||
|
||||
<button onClick={() => invalidateQuery(getProducts)}>Invalidate query</button>
|
||||
|
||||
<p>
|
||||
<Link href="/admin/products/new">
|
||||
<a>Create Product</a>
|
||||
|
||||
@@ -2,6 +2,7 @@ import {Form, Field} from "react-final-form"
|
||||
import {Product, ProductCreateInput, ProductUpdateInput} from "db"
|
||||
import createProduct from "../mutations/createProduct"
|
||||
import updateProduct from "../mutations/updateProduct"
|
||||
import {useMutation} from "blitz"
|
||||
|
||||
type ProductInput = ProductCreateInput | Product
|
||||
|
||||
@@ -16,13 +17,15 @@ type ProductFormProps = {
|
||||
}
|
||||
|
||||
function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
|
||||
const [createProductMutation] = useMutation(createProduct)
|
||||
const [updateProductMutation] = useMutation(updateProduct)
|
||||
return (
|
||||
<Form
|
||||
initialValues={product || {name: null, handle: null, description: null, price: null}}
|
||||
onSubmit={async (data: any) => {
|
||||
if (isNew(data)) {
|
||||
try {
|
||||
const product = await createProduct({data})
|
||||
const product = await createProductMutation({data})
|
||||
onSuccess(product)
|
||||
} catch (error) {
|
||||
alert("Error creating product " + JSON.stringify(error, null, 2))
|
||||
@@ -32,7 +35,7 @@ function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
|
||||
// Can't update id
|
||||
const id = data.id
|
||||
delete data.id
|
||||
const product = await updateProduct({where: {id}, data})
|
||||
const product = await updateProductMutation({where: {id}, data})
|
||||
onSuccess(product)
|
||||
} catch (error) {
|
||||
alert("Error updating product " + JSON.stringify(error, null, 2))
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import db, {ProductUpdateArgs} from "db"
|
||||
import {Ctx} from "blitz"
|
||||
|
||||
type UpdateProductInput = {
|
||||
where: ProductUpdateArgs["where"]
|
||||
data: ProductUpdateArgs["data"]
|
||||
}
|
||||
|
||||
export default async function updateProduct({where, data}: UpdateProductInput) {
|
||||
export default async function updateProduct({where, data}: UpdateProductInput, _ctx: Ctx) {
|
||||
const product = await db.product.update({where, data})
|
||||
|
||||
return product
|
||||
|
||||
@@ -10,7 +10,7 @@ type StaticProps = {
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<StaticProps> = async (ctx) => {
|
||||
const product = await getProduct({where: {handle: ctx.params!.handle as string}})
|
||||
const product = await getProduct({where: {handle: ctx.params!.handle as string}}, {} as any)
|
||||
const dataString = superjson.stringify(product)
|
||||
return {
|
||||
props: {dataString},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useMemo} from "react"
|
||||
import {ssrQuery, GetServerSideProps, Link, BlitzPage, PromiseReturnType} from "blitz"
|
||||
import {invokeWithMiddleware, GetServerSideProps, Link, BlitzPage, PromiseReturnType} from "blitz"
|
||||
import getProducts from "app/products/queries/getProducts"
|
||||
import superjson from "superjson"
|
||||
|
||||
@@ -10,7 +10,7 @@ type PageProps = {
|
||||
type Products = PromiseReturnType<typeof getProducts>
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({req, res}) => {
|
||||
const products = await ssrQuery(getProducts, {orderBy: {id: "desc"}}, {req, res})
|
||||
const products = await invokeWithMiddleware(getProducts, {orderBy: {id: "desc"}}, {req, res})
|
||||
const dataString = superjson.stringify(products)
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {NotFoundError} from "blitz"
|
||||
import {NotFoundError, Ctx} from "blitz"
|
||||
import db, {FindOneProductArgs} from "db"
|
||||
|
||||
type GetProductInput = {
|
||||
@@ -7,7 +7,7 @@ type GetProductInput = {
|
||||
// include?: FindOneProductArgs['include']
|
||||
}
|
||||
|
||||
export default async function getProduct({where}: GetProductInput) {
|
||||
export default async function getProduct({where}: GetProductInput, _ctx: Ctx) {
|
||||
const product = await db.product.findOne({where})
|
||||
|
||||
if (!product) throw new NotFoundError()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@examples/store",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "blitz db migrate && blitz build",
|
||||
@@ -18,7 +18,7 @@
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.24.0-canary.0",
|
||||
"final-form": "4.19.1",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"scripts": {
|
||||
"build": "blitz db migrate && blitz build",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
@@ -30,7 +30,7 @@
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.24.0-canary.0",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
"typescript": "3.8.3"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"packages": ["packages/*"],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
"@types/vinyl": "2.0.4",
|
||||
"@types/vinyl-fs": "2.4.11",
|
||||
"@types/webpack": "4.41.13",
|
||||
"@typescript-eslint/eslint-plugin": "2.x",
|
||||
"@typescript-eslint/parser": "2.x",
|
||||
"@typescript-eslint/eslint-plugin": "4.3.1-alpha.1",
|
||||
"@typescript-eslint/parser": "4.3.1-alpha.1",
|
||||
"@wessberg/cjs-to-esm-transformer": "0.0.22",
|
||||
"@wessberg/rollup-plugin-ts": "1.3.3",
|
||||
"babel-eslint": "10.x",
|
||||
@@ -133,8 +133,9 @@
|
||||
"ts-jest": "24.3.0",
|
||||
"tsdx": "0.13.3",
|
||||
"tslib": "1.11.1",
|
||||
"typescript": "3.8.3",
|
||||
"wait-on": "4.0.2"
|
||||
"typescript": "4.0.3",
|
||||
"wait-on": "4.0.2",
|
||||
"zod": "1.11.9"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "blitz",
|
||||
"description": "Blitz is a Rails-like framework for monolithic, full-stack React apps — built on Next.js",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
@@ -39,11 +39,11 @@
|
||||
"url": "https://github.com/blitz-js/blitz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blitzjs/cli": "0.23.1-canary.0",
|
||||
"@blitzjs/core": "0.23.1-canary.0",
|
||||
"@blitzjs/generator": "0.23.1-canary.0",
|
||||
"@blitzjs/installer": "0.23.1-canary.0",
|
||||
"@blitzjs/server": "0.23.1-canary.0",
|
||||
"@blitzjs/cli": "0.24.0-canary.0",
|
||||
"@blitzjs/core": "0.24.0-canary.0",
|
||||
"@blitzjs/generator": "0.24.0-canary.0",
|
||||
"@blitzjs/installer": "0.24.0-canary.0",
|
||||
"@blitzjs/server": "0.24.0-canary.0",
|
||||
"envinfo": "7.7.2",
|
||||
"os-name": "3.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
|
||||
@@ -19,9 +19,10 @@ async function main() {
|
||||
if (parseSemver(process.version).major < 12) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`You are using an unsupported version of Node.js. Consider switching to v12 or newer.\n`,
|
||||
`You are using an unsupported version of Node.js. Please switch to v12 or newer.\n`,
|
||||
),
|
||||
)
|
||||
process.exit()
|
||||
}
|
||||
|
||||
const globalBlitzPath = resolveFrom(__dirname, "blitz")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@blitzjs/cli",
|
||||
"description": "Blitz.js CLI",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"b": "./bin/run",
|
||||
@@ -30,8 +30,8 @@
|
||||
"/lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@blitzjs/display": "0.23.1-canary.0",
|
||||
"@blitzjs/repl": "0.23.1-canary.0",
|
||||
"@blitzjs/display": "0.24.0-canary.0",
|
||||
"@blitzjs/repl": "0.24.0-canary.0",
|
||||
"@oclif/command": "1.5.20",
|
||||
"@oclif/config": "1.15.1",
|
||||
"@oclif/plugin-autocomplete": "0.2.0",
|
||||
@@ -59,9 +59,9 @@
|
||||
"v8-compile-cache": "2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blitzjs/generator": "0.23.1-canary.0",
|
||||
"@blitzjs/installer": "0.23.1-canary.0",
|
||||
"@blitzjs/server": "0.23.1-canary.0",
|
||||
"@blitzjs/generator": "0.24.0-canary.0",
|
||||
"@blitzjs/installer": "0.24.0-canary.0",
|
||||
"@blitzjs/server": "0.24.0-canary.0",
|
||||
"@oclif/dev-cli": "1.22.2",
|
||||
"@oclif/test": "1.2.5",
|
||||
"@prisma/cli": "2.4.1",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"config"
|
||||
],
|
||||
"author": "Fran Zekan <zekan.fran369@gmail.com>",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@blitzjs/core",
|
||||
"description": "Blitz.js core functionality",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
@@ -40,8 +40,8 @@
|
||||
"url": "https://github.com/blitz-js/blitz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blitzjs/config": "0.23.1-canary.0",
|
||||
"@blitzjs/display": "0.23.1-canary.0",
|
||||
"@blitzjs/config": "0.24.0-canary.0",
|
||||
"@blitzjs/display": "0.24.0-canary.0",
|
||||
"bad-behavior": "1.0.1",
|
||||
"cookie-session": "1.4.0",
|
||||
"deepmerge": "4.2.2",
|
||||
@@ -51,7 +51,7 @@
|
||||
"pretty-ms": "6.0.1",
|
||||
"react-query": "2.23.0",
|
||||
"serialize-error": "6.0.0",
|
||||
"superjson": "1.2.2",
|
||||
"superjson": "1.2.3",
|
||||
"url": "0.11.0"
|
||||
},
|
||||
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"
|
||||
|
||||
28
packages/core/src/authorization.ts
Normal file
28
packages/core/src/authorization.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {Ctx} from "./middleware"
|
||||
import {AuthenticatedSessionContext} from "./supertokens"
|
||||
import {ZodSchema, infer as zInfer} from "zod"
|
||||
|
||||
export type ProtectArgs<T> = {schema?: T; authorize?: boolean | unknown}
|
||||
|
||||
interface AuthenticatedCtx extends Ctx {
|
||||
session: AuthenticatedSessionContext
|
||||
}
|
||||
|
||||
export const protect = <T extends ZodSchema<any, any>, U = zInfer<T>>(
|
||||
{schema, authorize = true}: ProtectArgs<T>,
|
||||
resolver: (args: U, ctx: AuthenticatedCtx) => any,
|
||||
) => {
|
||||
return (rawInput: U, ctx: Ctx) => {
|
||||
if (authorize) {
|
||||
const authorizeInput: any[] = ["superadmin", "SUPERADMIN"]
|
||||
if (Array.isArray(authorize)) {
|
||||
authorizeInput.push(...authorize)
|
||||
} else if (typeof authorize !== "boolean") {
|
||||
authorizeInput.push(authorize)
|
||||
}
|
||||
;(ctx as any).session.authorize(authorizeInput)
|
||||
}
|
||||
const input = schema ? schema.parse(rawInput) : rawInput
|
||||
return resolver(input, ctx as AuthenticatedCtx)
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,17 @@ export class AuthenticationError extends Error {
|
||||
constructor(message = "You must be logged in to access this") {
|
||||
super(message)
|
||||
}
|
||||
get _clearStack() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export class CSRFTokenMismatchError extends AuthenticationError {
|
||||
export class CSRFTokenMismatchError extends Error {
|
||||
name = "CSRFTokenMismatchError"
|
||||
statusCode = 401
|
||||
get _clearStack() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthorizationError extends Error {
|
||||
@@ -16,6 +23,9 @@ export class AuthorizationError extends Error {
|
||||
constructor(message = "You are not authorized to access this") {
|
||||
super(message)
|
||||
}
|
||||
get _clearStack() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Error {
|
||||
@@ -24,4 +34,7 @@ export class NotFoundError extends Error {
|
||||
constructor(message = "This could not be found") {
|
||||
super(message)
|
||||
}
|
||||
get _clearStack() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {NextPage, NextComponentType, NextPageContext} from "next"
|
||||
import {AppProps as NextAppProps} from "next/app"
|
||||
|
||||
export * from "./use-query"
|
||||
export * from "./use-paginated-query"
|
||||
export * from "./use-query-hooks"
|
||||
export {useMutation} from "./use-mutation"
|
||||
export {invoke, invokeWithMiddleware} from "./invoke"
|
||||
export {getQueryKey, invalidateQuery} from "./utils/react-query-utils"
|
||||
export {protect} from "./authorization"
|
||||
export * from "./use-params"
|
||||
export * from "./use-infinite-query"
|
||||
export * from "./ssr-query"
|
||||
export * from "./rpc"
|
||||
export * from "./with-router"
|
||||
export * from "./use-router"
|
||||
|
||||
@@ -3,26 +3,26 @@ import listen from "test-listen"
|
||||
import fetch from "isomorphic-unfetch"
|
||||
import delay from "delay"
|
||||
|
||||
import {ssrQuery} from "./ssr-query"
|
||||
import {EnhancedResolverModule} from "./rpc"
|
||||
import {invokeWithMiddleware} from "./invoke"
|
||||
import {EnhancedResolver} from "./types"
|
||||
|
||||
describe("ssrQuery", () => {
|
||||
describe("invokeWithMiddleware", () => {
|
||||
it("works without middleware", async () => {
|
||||
console.log = jest.fn()
|
||||
const resolverModule = (jest.fn().mockImplementation(async (input) => {
|
||||
await delay(1)
|
||||
return input
|
||||
}) as unknown) as EnhancedResolverModule
|
||||
}) as unknown) as EnhancedResolver<unknown, unknown>
|
||||
resolverModule._meta = {
|
||||
name: "getTest",
|
||||
type: "query",
|
||||
path: "some/test/path",
|
||||
filePath: "some/test/path",
|
||||
apiUrl: "some/test/path",
|
||||
}
|
||||
|
||||
await mockServer(
|
||||
async (req, res) => {
|
||||
const result = await ssrQuery(resolverModule as any, "test", {req, res})
|
||||
const result = await invokeWithMiddleware(resolverModule as any, "test", {req, res})
|
||||
|
||||
expect(result).toBe("test")
|
||||
},
|
||||
@@ -38,11 +38,11 @@ describe("ssrQuery", () => {
|
||||
const resolverModule = (jest.fn().mockImplementation(async (input) => {
|
||||
await delay(1)
|
||||
return input
|
||||
}) as unknown) as EnhancedResolverModule
|
||||
}) as unknown) as EnhancedResolver<unknown, unknown>
|
||||
resolverModule._meta = {
|
||||
name: "getTest",
|
||||
type: "query",
|
||||
path: "some/test/path",
|
||||
filePath: "some/test/path",
|
||||
apiUrl: "some/test/path",
|
||||
}
|
||||
resolverModule.middleware = [
|
||||
@@ -58,7 +58,49 @@ describe("ssrQuery", () => {
|
||||
|
||||
await mockServer(
|
||||
async (req, res) => {
|
||||
const result = await ssrQuery(resolverModule as any, "test", {req, res})
|
||||
const result = await invokeWithMiddleware(resolverModule as any, "test", {req, res})
|
||||
|
||||
expect(result).toBe("test")
|
||||
},
|
||||
async (url) => {
|
||||
const res = await fetch(url)
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.headers.get("test")).toBe("works")
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("works with extra middleware in config", async () => {
|
||||
console.log = jest.fn()
|
||||
const resolverModule = (jest.fn().mockImplementation(async (input) => {
|
||||
await delay(1)
|
||||
return input
|
||||
}) as unknown) as EnhancedResolver<unknown, unknown>
|
||||
resolverModule._meta = {
|
||||
name: "getTest",
|
||||
type: "query",
|
||||
filePath: "some/test/path",
|
||||
apiUrl: "some/test/path",
|
||||
}
|
||||
resolverModule.middleware = [
|
||||
(_req, res, next) => {
|
||||
res.statusCode = 201
|
||||
return next()
|
||||
},
|
||||
]
|
||||
|
||||
await mockServer(
|
||||
async (req, res) => {
|
||||
const result = await invokeWithMiddleware(resolverModule as any, "test", {
|
||||
req,
|
||||
res,
|
||||
middleware: [
|
||||
(_req, res, next) => {
|
||||
res.setHeader("test", "works")
|
||||
return next()
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(result).toBe("test")
|
||||
},
|
||||
88
packages/core/src/invoke.ts
Normal file
88
packages/core/src/invoke.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
QueryFn,
|
||||
FirstParam,
|
||||
PromiseReturnType,
|
||||
Resolver,
|
||||
EnhancedResolver,
|
||||
EnhancedResolverRpcClient,
|
||||
} from "./types"
|
||||
import {isClient} from "./utils"
|
||||
import {IncomingMessage, ServerResponse} from "http"
|
||||
import {baseLogger, log as displayLog, chalk} from "@blitzjs/display"
|
||||
import prettyMs from "pretty-ms"
|
||||
import {
|
||||
getAllMiddlewareForModule,
|
||||
handleRequestWithMiddleware,
|
||||
MiddlewareResponse,
|
||||
Middleware,
|
||||
} from "./middleware"
|
||||
|
||||
export function invoke<T extends QueryFn, TInput = FirstParam<T>, TResult = PromiseReturnType<T>>(
|
||||
queryFn: T,
|
||||
params: TInput,
|
||||
) {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error(
|
||||
"invoke is missing the first argument - it must be a query or mutation function",
|
||||
)
|
||||
}
|
||||
|
||||
if (isClient) {
|
||||
const fn = (queryFn as unknown) as EnhancedResolverRpcClient<TInput, TResult>
|
||||
return fn(params, {fromInvoke: true})
|
||||
} else {
|
||||
const fn = (queryFn as unknown) as EnhancedResolver<TInput, TResult>
|
||||
return fn(params) as ReturnType<T>
|
||||
}
|
||||
}
|
||||
|
||||
export type InvokeWithMiddlewareConfig = {
|
||||
req: IncomingMessage
|
||||
res: ServerResponse
|
||||
middleware?: Middleware[]
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export async function invokeWithMiddleware<TInput, TResult>(
|
||||
resolver: Resolver<TInput, TResult>,
|
||||
params: TInput,
|
||||
ctx: InvokeWithMiddlewareConfig,
|
||||
): Promise<TResult> {
|
||||
if (!ctx.req) {
|
||||
throw new Error("You must provide `req` in third argument of invokeWithMiddleware()")
|
||||
}
|
||||
if (!ctx.res) {
|
||||
throw new Error("You must provide `res` in third argument of invokeWithMiddleware()")
|
||||
}
|
||||
const enhancedResolver = (resolver as unknown) as EnhancedResolver<TInput, TResult>
|
||||
|
||||
const middleware = getAllMiddlewareForModule(enhancedResolver)
|
||||
|
||||
if (ctx.middleware) {
|
||||
middleware.push(...ctx.middleware)
|
||||
}
|
||||
|
||||
middleware.push(async (_req, res, next) => {
|
||||
const log = baseLogger.getChildLogger({prefix: [enhancedResolver._meta.name + "()"]})
|
||||
displayLog.newline()
|
||||
try {
|
||||
log.info(chalk.dim("Starting with input:"), params)
|
||||
const startTime = new Date().getTime()
|
||||
|
||||
const result = await enhancedResolver(params, res.blitzCtx)
|
||||
|
||||
const duration = prettyMs(new Date().getTime() - startTime)
|
||||
log.info(chalk.dim("Finished", "in", duration))
|
||||
displayLog.newline()
|
||||
|
||||
res.blitzResult = result
|
||||
return next()
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
await handleRequestWithMiddleware(ctx.req, ctx.res, middleware)
|
||||
|
||||
return (ctx.res as MiddlewareResponse).blitzResult as TResult
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import {apiResolver} from "next/dist/next-server/server/api-utils"
|
||||
import {BlitzApiRequest, BlitzApiResponse} from "."
|
||||
import {Middleware, handleRequestWithMiddleware} from "./middleware"
|
||||
|
||||
const testIfNotWindows = process.platform === "win32" ? it.skip : it
|
||||
|
||||
describe("handleRequestWithMiddleware", () => {
|
||||
it("works without await", async () => {
|
||||
const middleware: Middleware[] = [
|
||||
@@ -21,7 +23,7 @@ describe("handleRequestWithMiddleware", () => {
|
||||
]
|
||||
|
||||
await mockServer(middleware, async (url) => {
|
||||
const res = await fetch(url)
|
||||
const res = await fetch(url, {method: "POST"})
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.headers.get("test")).toBe("works")
|
||||
})
|
||||
@@ -40,7 +42,7 @@ describe("handleRequestWithMiddleware", () => {
|
||||
]
|
||||
|
||||
await mockServer(middleware, async (url) => {
|
||||
const res = await fetch(url)
|
||||
const res = await fetch(url, {method: "POST"})
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.headers.get("test")).toBe("works")
|
||||
})
|
||||
@@ -59,13 +61,14 @@ describe("handleRequestWithMiddleware", () => {
|
||||
]
|
||||
|
||||
await mockServer(middleware, async (url) => {
|
||||
const res = await fetch(url)
|
||||
const res = await fetch(url, {method: "POST"})
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.headers.get("test")).toBe("works")
|
||||
})
|
||||
})
|
||||
|
||||
it("middleware can throw", async () => {
|
||||
// Failing on windows for unknown reason
|
||||
testIfNotWindows("middleware can throw", async () => {
|
||||
console.log = jest.fn()
|
||||
console.error = jest.fn()
|
||||
const forbiddenMiddleware = jest.fn()
|
||||
@@ -77,13 +80,14 @@ describe("handleRequestWithMiddleware", () => {
|
||||
]
|
||||
|
||||
await mockServer(middleware, async (url) => {
|
||||
const res = await fetch(url)
|
||||
const res = await fetch(url, {method: "POST"})
|
||||
expect(forbiddenMiddleware).not.toBeCalled()
|
||||
expect(res.status).toBe(500)
|
||||
})
|
||||
})
|
||||
|
||||
it("middleware can return error", async () => {
|
||||
// Failing on windows for unknown reason
|
||||
testIfNotWindows("middleware can return error", async () => {
|
||||
console.log = jest.fn()
|
||||
const forbiddenMiddleware = jest.fn()
|
||||
const middleware: Middleware[] = [
|
||||
@@ -94,7 +98,7 @@ describe("handleRequestWithMiddleware", () => {
|
||||
]
|
||||
|
||||
await mockServer(middleware, async (url) => {
|
||||
const res = await fetch(url)
|
||||
const res = await fetch(url, {method: "POST"})
|
||||
expect(forbiddenMiddleware).not.toBeCalled()
|
||||
expect(res.status).toBe(500)
|
||||
})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable es5/no-for-of -- file only used on the server */
|
||||
/* eslint-disable es5/no-es6-methods -- file only used on the server */
|
||||
import {BlitzApiRequest, BlitzApiResponse} from "."
|
||||
import {IncomingMessage, ServerResponse} from "http"
|
||||
import {EnhancedResolverModule} from "./rpc"
|
||||
import {getConfig} from "@blitzjs/config"
|
||||
import {log} from "@blitzjs/display"
|
||||
import {log, baseLogger} from "@blitzjs/display"
|
||||
import {EnhancedResolver} from "./types"
|
||||
|
||||
export interface DefaultCtx {}
|
||||
export interface Ctx extends DefaultCtx {}
|
||||
|
||||
export interface MiddlewareRequest extends BlitzApiRequest {
|
||||
protocol?: string
|
||||
@@ -37,12 +39,9 @@ export type ConnectMiddleware = (
|
||||
next: (error?: Error) => void,
|
||||
) => void
|
||||
|
||||
export type ResolverModule = {
|
||||
default: (args: any, ctx: any) => Promise<unknown>
|
||||
middleware?: Middleware[]
|
||||
}
|
||||
|
||||
export function getAllMiddlewareForModule(resolverModule: EnhancedResolverModule) {
|
||||
export function getAllMiddlewareForModule<TInput, TResult>(
|
||||
resolverModule: EnhancedResolver<TInput, TResult>,
|
||||
) {
|
||||
const middleware: Middleware[] = []
|
||||
const config = getConfig()
|
||||
if (config.middleware) {
|
||||
@@ -64,6 +63,7 @@ export async function handleRequestWithMiddleware(
|
||||
req: BlitzApiRequest | IncomingMessage,
|
||||
res: BlitzApiResponse | ServerResponse,
|
||||
middleware: Middleware | Middleware[],
|
||||
{throwOnError = true}: {throwOnError?: boolean} = {},
|
||||
) {
|
||||
if (!(res as MiddlewareResponse).blitzCtx) {
|
||||
;(res as MiddlewareResponse).blitzCtx = {}
|
||||
@@ -89,20 +89,22 @@ export async function handleRequestWithMiddleware(
|
||||
log.newline()
|
||||
if (req.method === "GET") {
|
||||
// This GET method check is so we don't .end() the request for SSR requests
|
||||
log.error("Error while processing the request:\n")
|
||||
log.error(error)
|
||||
baseLogger.error("Error while processing the request")
|
||||
} else if (res.writableFinished) {
|
||||
baseLogger.error(
|
||||
"Error occured in middleware after the response was already sent to the browser",
|
||||
)
|
||||
} else {
|
||||
if (!res.writableFinished) {
|
||||
res.statusCode = (error as any).statusCode || (error as any).status || 500
|
||||
res.end(error.message || res.statusCode.toString())
|
||||
log.error("Error while processing the request:\n")
|
||||
} else {
|
||||
log.error(
|
||||
"Error occured in middleware after the response was already sent to the browser:\n",
|
||||
)
|
||||
}
|
||||
res.statusCode = (error as any).statusCode || (error as any).status || 500
|
||||
res.end(error.message || res.statusCode.toString())
|
||||
baseLogger.error("Error while processing the request")
|
||||
}
|
||||
throw error
|
||||
if (error._clearStack) {
|
||||
delete error.stack
|
||||
}
|
||||
baseLogger.prettyError(error)
|
||||
log.newline()
|
||||
if (throwOnError) throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export function passportAuth(config: BlitzPassportConfig) {
|
||||
middleware.push(async (req, res, next) => {
|
||||
const session = res.blitzCtx.session as SessionContext
|
||||
assert(session, "Missing Blitz sessionMiddleware!")
|
||||
await session.setPublicData({[INTERNAL_REDIRECT_URL_KEY]: req.query.redirectUrl})
|
||||
await session.setPublicData({[INTERNAL_REDIRECT_URL_KEY]: req.query.redirectUrl} as any)
|
||||
return next()
|
||||
})
|
||||
}
|
||||
@@ -113,9 +113,9 @@ export function passportAuth(config: BlitzPassportConfig) {
|
||||
|
||||
const redirectUrlFromVerifyResult =
|
||||
result && typeof result === "object" && (result as any).redirectUrl
|
||||
let redirectUrl =
|
||||
let redirectUrl: string =
|
||||
redirectUrlFromVerifyResult ||
|
||||
session.publicData[INTERNAL_REDIRECT_URL_KEY] ||
|
||||
(session.publicData as any)[INTERNAL_REDIRECT_URL_KEY] ||
|
||||
(error ? config.errorRedirectUrl : config.successRedirectUrl) ||
|
||||
"/"
|
||||
|
||||
@@ -129,10 +129,9 @@ export function passportAuth(config: BlitzPassportConfig) {
|
||||
|
||||
assert(isVerifyCallbackResult(result), "Passport verify callback is invalid")
|
||||
|
||||
await session.create(
|
||||
{...result.publicData, [INTERNAL_REDIRECT_URL_KEY]: undefined},
|
||||
result.privateData,
|
||||
)
|
||||
delete (result.publicData as any)[INTERNAL_REDIRECT_URL_KEY]
|
||||
|
||||
await session.create(result.publicData, result.privateData)
|
||||
|
||||
res.setHeader("Location", redirectUrl)
|
||||
res.statusCode = 302
|
||||
|
||||
95
packages/core/src/public-data-store.test.ts
Normal file
95
packages/core/src/public-data-store.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {publicDataStore} from "./public-data-store"
|
||||
import {COOKIE_PUBLIC_DATA_TOKEN, parsePublicDataToken} from "./supertokens"
|
||||
import {deleteCookie, readCookie} from "./utils/cookie"
|
||||
import {queryCache} from "react-query"
|
||||
jest.mock("./supertokens", () => ({
|
||||
parsePublicDataToken: jest.fn(),
|
||||
}))
|
||||
jest.mock("./utils/cookie", () => ({
|
||||
readCookie: jest.fn(),
|
||||
deleteCookie: jest.fn(),
|
||||
}))
|
||||
jest.mock("react-query")
|
||||
|
||||
describe("publicDataStore", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
it("calls readCookie token on init", () => {
|
||||
// note: As public-data-store has side effects, this test might be fickle
|
||||
expect(readCookie).toHaveBeenCalledWith(COOKIE_PUBLIC_DATA_TOKEN)
|
||||
})
|
||||
|
||||
describe("updateState", () => {
|
||||
let localStorageSpy: jest.SpyInstance
|
||||
|
||||
beforeAll(() => {
|
||||
localStorageSpy = jest.spyOn(Storage.prototype, "setItem")
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
localStorageSpy.mockRestore()
|
||||
})
|
||||
it("sets local storage", () => {
|
||||
publicDataStore.updateState()
|
||||
expect(localStorageSpy).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("publishes data on observable", () => {
|
||||
let ret: any = null
|
||||
publicDataStore.observable.subscribe((data) => {
|
||||
ret = data
|
||||
})
|
||||
publicDataStore.updateState()
|
||||
expect(ret).not.toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe("clear", () => {
|
||||
it("clears the cookie", () => {
|
||||
publicDataStore.clear()
|
||||
expect(deleteCookie).toHaveBeenCalledWith(COOKIE_PUBLIC_DATA_TOKEN)
|
||||
})
|
||||
it("clears the cache", () => {
|
||||
publicDataStore.clear()
|
||||
expect(queryCache.clear).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("publishes empty data", () => {
|
||||
let ret: any = null
|
||||
publicDataStore.observable.subscribe((data) => {
|
||||
ret = data
|
||||
})
|
||||
publicDataStore.clear()
|
||||
expect(ret).toEqual(publicDataStore.emptyPublicData)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getData", () => {
|
||||
const setPublicDataToken = (value: string) => {
|
||||
;(parsePublicDataToken as jest.MockedFunction<typeof parsePublicDataToken>).mockReturnValue({
|
||||
publicData: value as any,
|
||||
})
|
||||
}
|
||||
|
||||
xdescribe("when the cookie is falsy", () => {
|
||||
it("returns empty data if cookie is falsy", () => {
|
||||
const ret = publicDataStore.getData()
|
||||
|
||||
expect(ret).toEqual(publicDataStore.emptyPublicData)
|
||||
})
|
||||
})
|
||||
|
||||
describe("when the cookie has a value", () => {
|
||||
beforeEach(() => {
|
||||
;(readCookie as jest.MockedFunction<typeof readCookie>).mockReturnValue("readCookie")
|
||||
})
|
||||
it("returns publicData", () => {
|
||||
setPublicDataToken("foo")
|
||||
const ret = publicDataStore.getData()
|
||||
|
||||
expect(ret).toEqual("foo")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
55
packages/core/src/public-data-store.ts
Normal file
55
packages/core/src/public-data-store.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
LOCALSTORAGE_PREFIX,
|
||||
COOKIE_PUBLIC_DATA_TOKEN,
|
||||
PublicData,
|
||||
parsePublicDataToken,
|
||||
} from "./supertokens"
|
||||
import {readCookie, deleteCookie} from "./utils/cookie"
|
||||
import BadBehavior from "bad-behavior"
|
||||
import {queryCache} from "react-query"
|
||||
|
||||
class PublicDataStore {
|
||||
private eventKey = `${LOCALSTORAGE_PREFIX}publicDataUpdated`
|
||||
readonly emptyPublicData: PublicData = {userId: null, roles: []}
|
||||
readonly observable = BadBehavior<PublicData>()
|
||||
|
||||
constructor() {
|
||||
if (typeof window !== "undefined") {
|
||||
// Set default value
|
||||
this.updateState()
|
||||
window.addEventListener("storage", (event) => {
|
||||
if (event.key === this.eventKey) {
|
||||
this.updateState()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateState(value?: PublicData) {
|
||||
// We use localStorage as a message bus between tabs.
|
||||
// Setting the current time in ms will cause other tabs to receive the `storage` event
|
||||
localStorage.setItem(this.eventKey, Date.now().toString())
|
||||
this.observable.next(value ?? this.getData())
|
||||
}
|
||||
|
||||
clear() {
|
||||
deleteCookie(COOKIE_PUBLIC_DATA_TOKEN)
|
||||
queryCache.clear()
|
||||
this.updateState(this.emptyPublicData)
|
||||
}
|
||||
|
||||
getData() {
|
||||
const publicDataToken = this.getToken()
|
||||
if (!publicDataToken) {
|
||||
return this.emptyPublicData
|
||||
}
|
||||
|
||||
const {publicData} = parsePublicDataToken(publicDataToken)
|
||||
return publicData
|
||||
}
|
||||
|
||||
private getToken() {
|
||||
return readCookie(COOKIE_PUBLIC_DATA_TOKEN)
|
||||
}
|
||||
}
|
||||
export const publicDataStore = new PublicDataStore()
|
||||
@@ -1,26 +1,41 @@
|
||||
import {deserializeError} from "serialize-error"
|
||||
import {queryCache} from "react-query"
|
||||
import {getQueryKey} from "./utils"
|
||||
import {ResolverModule, Middleware} from "./middleware"
|
||||
import {isClient, isServer, clientDebug} from "./utils"
|
||||
import {
|
||||
getAntiCSRFToken,
|
||||
publicDataStore,
|
||||
HEADER_CSRF,
|
||||
HEADER_SESSION_REVOKED,
|
||||
HEADER_CSRF_ERROR,
|
||||
HEADER_PUBLIC_DATA_TOKEN,
|
||||
} from "./supertokens"
|
||||
import {publicDataStore} from "./public-data-store"
|
||||
import {CSRFTokenMismatchError} from "./errors"
|
||||
import {serialize, deserialize} from "superjson"
|
||||
import merge from "deepmerge"
|
||||
import {
|
||||
ResolverType,
|
||||
ResolverModule,
|
||||
EnhancedResolver,
|
||||
EnhancedResolverRpcClient,
|
||||
CancellablePromise,
|
||||
ResolverRpc,
|
||||
RpcOptions,
|
||||
} from "./types"
|
||||
import {SuperJSONResult} from "superjson/dist/types"
|
||||
import {getQueryKeyFromUrlAndParams} from "./utils/react-query-utils"
|
||||
|
||||
type Options = {
|
||||
fromQueryHook?: boolean
|
||||
resultOfGetFetchMore?: any
|
||||
}
|
||||
export const executeRpcCall = <TInput, TResult>(
|
||||
apiUrl: string,
|
||||
params: TInput,
|
||||
opts: RpcOptions = {},
|
||||
) => {
|
||||
if (!opts.fromQueryHook && !opts.fromInvoke) {
|
||||
console.warn(
|
||||
"[Deprecation] Directly calling queries/mutations is deprecated in favor of invoke(queryFn, params)",
|
||||
)
|
||||
}
|
||||
|
||||
export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
if (typeof window === "undefined") return
|
||||
if (isServer) return (Promise.resolve() as unknown) as CancellablePromise<TResult>
|
||||
clientDebug("Starting request for", apiUrl)
|
||||
|
||||
const headers: Record<string, any> = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -28,20 +43,19 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
|
||||
const antiCSRFToken = getAntiCSRFToken()
|
||||
if (antiCSRFToken) {
|
||||
clientDebug("Adding antiCSRFToken cookie header", antiCSRFToken)
|
||||
headers[HEADER_CSRF] = antiCSRFToken
|
||||
} else {
|
||||
clientDebug("No antiCSRFToken cookie found")
|
||||
}
|
||||
|
||||
let serialized
|
||||
if (opts.fromQueryHook) {
|
||||
// We have to serialize query arguments inside the hooks, otherwise react-query will use
|
||||
// JSON.parse(JSON.stringify) so by the time the arguments come here the real JS objects are lost
|
||||
serialized = params
|
||||
if (opts.resultOfGetFetchMore) {
|
||||
// useInfiniteQuery usually passes in extra pageParams here that come from getFetchMore()
|
||||
// This isn't serialized inside useInfiniteQuery because this data is provided separately
|
||||
// by react-query
|
||||
serialized = merge(params, serialize(opts.resultOfGetFetchMore))
|
||||
}
|
||||
let serialized: SuperJSONResult
|
||||
if (opts.alreadySerialized) {
|
||||
// params is already serialized with superjson when it gets here
|
||||
// We have to serialize the params before passing to react-query in the query key
|
||||
// because otherwise react-query will use JSON.parse(JSON.stringify)
|
||||
// so by the time the arguments come here the real JS objects are lost
|
||||
serialized = (params as unknown) as SuperJSONResult
|
||||
} else {
|
||||
serialized = serialize(params)
|
||||
}
|
||||
@@ -49,15 +63,14 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
// Create a new AbortController instance for this request
|
||||
const controller = new AbortController()
|
||||
|
||||
const promise: CancellablePromise<any> = window
|
||||
.fetch(url, {
|
||||
const promise = window
|
||||
.fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers,
|
||||
credentials: "include",
|
||||
redirect: "follow",
|
||||
body: JSON.stringify({
|
||||
// TODO remove `|| null` once superjson allows `undefined`
|
||||
params: serialized.json || null,
|
||||
params: serialized.json,
|
||||
meta: {
|
||||
params: serialized.meta,
|
||||
},
|
||||
@@ -65,15 +78,20 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then(async (result) => {
|
||||
clientDebug("Received request for", apiUrl)
|
||||
if (result.headers) {
|
||||
if (result.headers.get(HEADER_PUBLIC_DATA_TOKEN)) {
|
||||
publicDataStore.updateState()
|
||||
clientDebug("Public data updated")
|
||||
}
|
||||
if (result.headers.get(HEADER_SESSION_REVOKED)) {
|
||||
clientDebug("Sessin revoked")
|
||||
publicDataStore.clear()
|
||||
}
|
||||
if (result.headers.get(HEADER_CSRF_ERROR)) {
|
||||
throw new CSRFTokenMismatchError()
|
||||
const err = new CSRFTokenMismatchError()
|
||||
delete err.stack
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,15 +99,25 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
try {
|
||||
payload = await result.json()
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse json from request to ${url}`)
|
||||
throw new Error(`Failed to parse json from request to ${apiUrl}`)
|
||||
}
|
||||
|
||||
if (payload.error) {
|
||||
const error = deserializeError(payload.error)
|
||||
let error = deserializeError(payload.error) as any
|
||||
// We don't clear the publicDataStore for anonymous users
|
||||
if (error.name === "AuthenticationError" && publicDataStore.getData().userId) {
|
||||
publicDataStore.clear()
|
||||
}
|
||||
|
||||
const prismaError = error.message.match(/invalid.*prisma.*invocation/i)
|
||||
if (prismaError) {
|
||||
error = new Error(prismaError[0])
|
||||
error.statusCode = 500
|
||||
}
|
||||
|
||||
// Prevent client-side error popop from showing
|
||||
delete error.stack
|
||||
|
||||
throw error
|
||||
} else {
|
||||
const data =
|
||||
@@ -98,81 +126,95 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
: deserialize({json: payload.result, meta: payload.meta?.result})
|
||||
|
||||
if (!opts.fromQueryHook) {
|
||||
const queryKey = getQueryKey(url, params)
|
||||
const queryKey = getQueryKeyFromUrlAndParams(apiUrl, params)
|
||||
queryCache.setQueryData(queryKey, data)
|
||||
}
|
||||
return data
|
||||
return data as TResult
|
||||
}
|
||||
})
|
||||
}) as CancellablePromise<TResult>
|
||||
|
||||
promise.cancel = () => controller.abort()
|
||||
// Disable react-query request cancellation for now
|
||||
// Having too many weird bugs with it enabled
|
||||
// promise.cancel = () => controller.abort()
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
executeRpcCall.warm = (url: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
window.fetch(url, {method: "HEAD"})
|
||||
executeRpcCall.warm = (apiUrl: string) => {
|
||||
if (isClient) {
|
||||
return window.fetch(apiUrl, {method: "HEAD"})
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
interface ResolverEnhancement {
|
||||
_meta: {
|
||||
name: string
|
||||
type: string
|
||||
path: string
|
||||
apiUrl: string
|
||||
}
|
||||
}
|
||||
const getApiUrlFromResolverFilePath = (resolverFilePath: string) =>
|
||||
resolverFilePath.replace(/^app\/_resolvers/, "/api")
|
||||
|
||||
interface CancellablePromise<T> extends Promise<T> {
|
||||
cancel?: Function
|
||||
}
|
||||
|
||||
export interface RpcFunction {
|
||||
(params: any, opts: any): CancellablePromise<any>
|
||||
}
|
||||
export interface EnhancedRpcFunction extends RpcFunction, ResolverEnhancement {}
|
||||
|
||||
export interface EnhancedResolverModule extends ResolverEnhancement {
|
||||
(input: any, ctx: Record<string, any>): CancellablePromise<unknown>
|
||||
middleware?: Middleware[]
|
||||
}
|
||||
|
||||
export function getIsomorphicRpcHandler(
|
||||
resolver: ResolverModule,
|
||||
resolverPath: string,
|
||||
/*
|
||||
* Overloading signature so you can specify server/client and get the
|
||||
* correct return type
|
||||
*/
|
||||
export function getIsomorphicEnhancedResolver<TInput, TResult>(
|
||||
// resolver is undefined on the client
|
||||
resolver: ResolverModule<TInput, TResult> | undefined,
|
||||
resolverFilePath: string,
|
||||
resolverName: string,
|
||||
resolverType: string,
|
||||
) {
|
||||
const apiUrl = resolverPath.replace(/^app\/_resolvers/, "/api")
|
||||
const enhance = <T extends ResolverEnhancement>(fn: T): T => {
|
||||
fn._meta = {
|
||||
resolverType: ResolverType,
|
||||
): EnhancedResolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>
|
||||
export function getIsomorphicEnhancedResolver<TInput, TResult>(
|
||||
// resolver is undefined on the client
|
||||
resolver: ResolverModule<TInput, TResult> | undefined,
|
||||
resolverFilePath: string,
|
||||
resolverName: string,
|
||||
resolverType: ResolverType,
|
||||
target: "client",
|
||||
): EnhancedResolverRpcClient<TInput, TResult>
|
||||
export function getIsomorphicEnhancedResolver<TInput, TResult>(
|
||||
// resolver is undefined on the client
|
||||
resolver: ResolverModule<TInput, TResult> | undefined,
|
||||
resolverFilePath: string,
|
||||
resolverName: string,
|
||||
resolverType: ResolverType,
|
||||
target: "server",
|
||||
): EnhancedResolver<TInput, TResult>
|
||||
export function getIsomorphicEnhancedResolver<TInput, TResult>(
|
||||
// resolver is undefined on the client
|
||||
resolver: ResolverModule<TInput, TResult> | undefined,
|
||||
resolverFilePath: string,
|
||||
resolverName: string,
|
||||
resolverType: ResolverType,
|
||||
target: "server" | "client" = isClient ? "client" : "server",
|
||||
): EnhancedResolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult> {
|
||||
const apiUrl = getApiUrlFromResolverFilePath(resolverFilePath)
|
||||
|
||||
if (target === "client") {
|
||||
const resolverRpc: ResolverRpc<TInput, TResult> = (params, opts) =>
|
||||
executeRpcCall(apiUrl, params, opts)
|
||||
const enhancedResolverRpcClient = resolverRpc as EnhancedResolverRpcClient<TInput, TResult>
|
||||
|
||||
enhancedResolverRpcClient._meta = {
|
||||
name: resolverName,
|
||||
type: resolverType,
|
||||
path: resolverPath,
|
||||
filePath: resolverFilePath,
|
||||
apiUrl: apiUrl,
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
let rpcFn: EnhancedRpcFunction = ((params: any, opts = {}) =>
|
||||
executeRpcCall(apiUrl, params, opts)) as any
|
||||
|
||||
rpcFn = enhance(rpcFn)
|
||||
|
||||
// Warm the lambda
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
executeRpcCall.warm(apiUrl)
|
||||
|
||||
return rpcFn
|
||||
return enhancedResolverRpcClient
|
||||
} else {
|
||||
let handler: EnhancedResolverModule = resolver.default as any
|
||||
|
||||
handler.middleware = resolver.middleware
|
||||
handler = enhance(handler)
|
||||
|
||||
return handler
|
||||
if (!resolver) throw new Error("resolver is missing on the server")
|
||||
const enhancedResolver = (resolver.default as unknown) as EnhancedResolver<TInput, TResult>
|
||||
enhancedResolver.middleware = resolver.middleware
|
||||
enhancedResolver._meta = {
|
||||
name: resolverName,
|
||||
type: resolverType,
|
||||
filePath: resolverFilePath,
|
||||
apiUrl: apiUrl,
|
||||
}
|
||||
return enhancedResolver
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import {IncomingMessage, ServerResponse} from "http"
|
||||
import {log} from "@blitzjs/display"
|
||||
import {InferUnaryParam} from "./types"
|
||||
import {
|
||||
getAllMiddlewareForModule,
|
||||
handleRequestWithMiddleware,
|
||||
MiddlewareResponse,
|
||||
} from "./middleware"
|
||||
import {EnhancedResolverModule} from "./rpc"
|
||||
|
||||
type QueryFn = (...args: any) => Promise<any>
|
||||
|
||||
type SsrQueryContext = {
|
||||
req: IncomingMessage
|
||||
res: ServerResponse
|
||||
}
|
||||
|
||||
export async function ssrQuery<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T>,
|
||||
{req, res}: SsrQueryContext,
|
||||
): Promise<ReturnType<T>> {
|
||||
const handler = (queryFn as unknown) as EnhancedResolverModule
|
||||
|
||||
const middleware = getAllMiddlewareForModule(handler)
|
||||
|
||||
middleware.push(async (_req, res, next) => {
|
||||
const logPrefix = `${handler._meta.name}`
|
||||
log.newline()
|
||||
try {
|
||||
log.progress(`Running ${logPrefix}(${JSON.stringify(params, null, 2)})`)
|
||||
const result = await handler(params, res.blitzCtx)
|
||||
log.success(`${logPrefix} returned ${log.variable(JSON.stringify(result, null, 2))}\n`)
|
||||
res.blitzResult = result
|
||||
return next()
|
||||
} catch (error) {
|
||||
log.error(`${logPrefix} failed: ${error}\n`)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
await handleRequestWithMiddleware(req, res, middleware)
|
||||
|
||||
return (res as MiddlewareResponse).blitzResult as ReturnType<T>
|
||||
}
|
||||
31
packages/core/src/supertokens.test.ts
Normal file
31
packages/core/src/supertokens.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {parsePublicDataToken, TOKEN_SEPARATOR} from "./supertokens"
|
||||
|
||||
describe("supertokens", () => {
|
||||
describe("parsePublicDataToken", () => {
|
||||
it("throws if token is empty", () => {
|
||||
const ret = () => parsePublicDataToken("")
|
||||
expect(ret).toThrow("[parsePublicDataToken] Failed: token is empty")
|
||||
})
|
||||
|
||||
it("throws if the token cannot be parsed", () => {
|
||||
const invalidJSON = "{"
|
||||
const ret = () => parsePublicDataToken(btoa(invalidJSON))
|
||||
|
||||
expect(ret).toThrowError("[parsePublicDataToken] Failed to parse publicDataStr: {")
|
||||
})
|
||||
|
||||
it("parses the public data", () => {
|
||||
const validJSON = '"foo"'
|
||||
expect(parsePublicDataToken(btoa(validJSON))).toEqual({
|
||||
publicData: "foo",
|
||||
})
|
||||
})
|
||||
|
||||
it("only uses the first separated tokens", () => {
|
||||
const data = `"foo"${TOKEN_SEPARATOR}123`
|
||||
expect(parsePublicDataToken(btoa(data))).toEqual({
|
||||
publicData: "foo",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useState} from "react"
|
||||
import BadBehavior from "bad-behavior"
|
||||
import {publicDataStore} from "./public-data-store"
|
||||
import {useIsomorphicLayoutEffect} from "./utils/hooks"
|
||||
import {queryCache} from "react-query"
|
||||
import {readCookie} from "./utils/cookie"
|
||||
|
||||
export const TOKEN_SEPARATOR = ";"
|
||||
export const HANDLE_SEPARATOR = ":"
|
||||
@@ -27,14 +27,16 @@ function assert(condition: any, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
export interface PublicData extends Record<any, any> {
|
||||
export interface DefaultPublicData {
|
||||
userId: any
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export interface PublicData extends DefaultPublicData {}
|
||||
|
||||
export interface SessionModel extends Record<any, any> {
|
||||
handle: string
|
||||
userId?: any
|
||||
userId?: PublicData["userId"]
|
||||
expiresAt?: Date
|
||||
hashedSessionToken?: string
|
||||
antiCSRFToken?: string
|
||||
@@ -47,23 +49,20 @@ export type SessionConfig = {
|
||||
method?: "essential" | "advanced"
|
||||
sameSite?: "none" | "lax" | "strict"
|
||||
getSession: (handle: string) => Promise<SessionModel | null>
|
||||
getSessions: (userId: any) => Promise<SessionModel[]>
|
||||
getSessions: (userId: PublicData["userId"]) => Promise<SessionModel[]>
|
||||
createSession: (session: SessionModel) => Promise<SessionModel>
|
||||
updateSession: (handle: string, session: Partial<SessionModel>) => Promise<SessionModel>
|
||||
deleteSession: (handle: string) => Promise<SessionModel>
|
||||
unstable_isAuthorized: (userRoles: string[], input?: any) => boolean
|
||||
}
|
||||
|
||||
export interface SessionContext {
|
||||
/**
|
||||
* null if anonymous
|
||||
*/
|
||||
userId: any
|
||||
export interface SessionContextBase {
|
||||
userId: unknown
|
||||
roles: string[]
|
||||
handle: string | null
|
||||
publicData: PublicData
|
||||
authorize: (input?: any) => void
|
||||
isAuthorized: (input?: any) => boolean
|
||||
publicData: unknown
|
||||
authorize(input?: any): asserts this is AuthenticatedSessionContext
|
||||
isAuthorized(input?: any): boolean
|
||||
// authorize: (roleOrRoles?: string | string[]) => void
|
||||
// isAuthorized: (roleOrRoles?: string | string[]) => boolean
|
||||
create: (publicData: PublicData, privateData?: Record<any, any>) => Promise<void>
|
||||
@@ -71,97 +70,38 @@ export interface SessionContext {
|
||||
revokeAll: () => Promise<void>
|
||||
getPrivateData: () => Promise<Record<any, any>>
|
||||
setPrivateData: (data: Record<any, any>) => Promise<void>
|
||||
setPublicData: (data: Record<any, any>) => Promise<void>
|
||||
setPublicData: (data: Partial<Omit<PublicData, "userId">>) => Promise<void>
|
||||
}
|
||||
|
||||
// Taken from https://github.com/HenrikJoreteg/cookie-getter
|
||||
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
|
||||
export function readCookie(name: string) {
|
||||
if (typeof document === "undefined") return null
|
||||
const cookie = document.cookie
|
||||
const setPos = cookie.search(new RegExp("\\b" + name + "="))
|
||||
const stopPos = cookie.indexOf(";", setPos)
|
||||
let res
|
||||
if (!~setPos) return null
|
||||
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1])
|
||||
return res.charAt(0) === "{" ? JSON.parse(res) : res
|
||||
// Could be anonymous
|
||||
export interface SessionContext extends SessionContextBase {
|
||||
userId: PublicData["userId"] | null
|
||||
publicData: Partial<PublicData>
|
||||
}
|
||||
|
||||
export const setCookie = (name: string, value: string, expires: string) => {
|
||||
const result = `${name}=${value};path=/;expires=${expires}`
|
||||
document.cookie = result
|
||||
export interface AuthenticatedSessionContext extends SessionContextBase {
|
||||
userId: PublicData["userId"]
|
||||
publicData: PublicData
|
||||
}
|
||||
export const deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")
|
||||
|
||||
export const getAntiCSRFToken = () => readCookie(COOKIE_CSRF_TOKEN)
|
||||
export const getPublicDataToken = () => readCookie(COOKIE_PUBLIC_DATA_TOKEN)
|
||||
|
||||
export const parsePublicDataToken = (token: string) => {
|
||||
assert(token, "[parsePublicDataToken] Failed - token is empty")
|
||||
assert(token, "[parsePublicDataToken] Failed: token is empty")
|
||||
|
||||
const [publicDataStr, expireAt] = atob(token).split(TOKEN_SEPARATOR)
|
||||
let publicData: PublicData
|
||||
const [publicDataStr] = atob(token).split(TOKEN_SEPARATOR)
|
||||
try {
|
||||
publicData = JSON.parse(publicDataStr)
|
||||
const publicData: PublicData = JSON.parse(publicDataStr)
|
||||
return {
|
||||
publicData,
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error("Failed to parse publicDataToken: " + publicDataStr)
|
||||
}
|
||||
return {
|
||||
publicData,
|
||||
expireAt: expireAt && new Date(expireAt),
|
||||
throw new Error(`[parsePublicDataToken] Failed to parse publicDataStr: ${publicDataStr}`)
|
||||
}
|
||||
}
|
||||
|
||||
const emptyPublicData: PublicData = {userId: null, roles: []}
|
||||
|
||||
export const publicDataStore = {
|
||||
eventKey: LOCALSTORAGE_PREFIX + "publicDataUpdated",
|
||||
observable: BadBehavior<PublicData>(),
|
||||
initialize() {
|
||||
if (typeof window !== "undefined") {
|
||||
// Set default value
|
||||
publicDataStore.updateState()
|
||||
window.addEventListener("storage", (event) => {
|
||||
if (event.key === this.eventKey) {
|
||||
publicDataStore.updateState()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
getToken() {
|
||||
return getPublicDataToken()
|
||||
},
|
||||
getData() {
|
||||
const publicDataToken = this.getToken()
|
||||
|
||||
if (!publicDataToken) {
|
||||
return emptyPublicData
|
||||
}
|
||||
|
||||
const {publicData, expireAt} = parsePublicDataToken(publicDataToken)
|
||||
|
||||
if (expireAt < new Date()) {
|
||||
this.clear()
|
||||
return emptyPublicData
|
||||
}
|
||||
return publicData
|
||||
},
|
||||
updateState() {
|
||||
// We use localStorage as a message bus between tabs.
|
||||
// Setting the current time in ms will cause other tabs to receive the `storage` event
|
||||
localStorage.setItem(this.eventKey, Date.now().toString())
|
||||
publicDataStore.observable.next(this.getData())
|
||||
},
|
||||
clear() {
|
||||
deleteCookie(COOKIE_PUBLIC_DATA_TOKEN)
|
||||
queryCache.clear()
|
||||
this.updateState()
|
||||
},
|
||||
}
|
||||
publicDataStore.initialize()
|
||||
|
||||
export const useSession = () => {
|
||||
const [publicData, setPublicData] = useState(emptyPublicData)
|
||||
const [publicData, setPublicData] = useState(publicDataStore.emptyPublicData)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
@@ -172,7 +112,7 @@ export const useSession = () => {
|
||||
return subscription.unsubscribe
|
||||
}, [])
|
||||
|
||||
return {...publicData, isLoading}
|
||||
return {...publicData, isLoading} as PublicData & {isLoading: boolean}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {Middleware} from "./middleware"
|
||||
|
||||
/**
|
||||
* Infer the type of the parameter from function that takes a single argument
|
||||
*/
|
||||
export type InferUnaryParam<F extends Function> = F extends (args: infer A) => any ? A : never
|
||||
export type FirstParam<F extends QueryFn> = Parameters<F>[0]
|
||||
|
||||
/**
|
||||
* Get the type of the value, that the Promise holds.
|
||||
@@ -13,4 +15,73 @@ export type PromiseType<T extends PromiseLike<any>> = T extends PromiseLike<infe
|
||||
*/
|
||||
export type PromiseReturnType<T extends (...args: any) => Promise<any>> = PromiseType<ReturnType<T>>
|
||||
|
||||
export interface CancellablePromise<T> extends Promise<T> {
|
||||
cancel?: Function
|
||||
}
|
||||
|
||||
export type QueryFn = (...args: any) => Promise<any>
|
||||
|
||||
// The actual resolver source definition
|
||||
export type Resolver<TInput, TResult> = (input: TInput, ctx?: any) => Promise<TResult>
|
||||
|
||||
// Resolver type when imported with require()
|
||||
export type ResolverModule<TInput, TResult> = {
|
||||
default: Resolver<TInput, TResult>
|
||||
middleware?: Middleware[]
|
||||
}
|
||||
|
||||
export type RpcOptions = {
|
||||
fromQueryHook?: boolean
|
||||
fromInvoke?: boolean
|
||||
alreadySerialized?: boolean
|
||||
}
|
||||
|
||||
// The compiled rpc resolver available on client
|
||||
export type ResolverRpc<TInput, TResult> = (
|
||||
input?: TInput,
|
||||
opts?: RpcOptions,
|
||||
) => CancellablePromise<TResult>
|
||||
|
||||
export interface ResolverRpcExecutor<TInput, TResult> {
|
||||
(apiUrl: string, params: TInput, opts?: RpcOptions): CancellablePromise<TResult>
|
||||
warm: (apiUrl: string) => undefined | Promise<unknown>
|
||||
}
|
||||
|
||||
export type ResolverType = "query" | "mutation"
|
||||
|
||||
export interface ResolverEnhancement {
|
||||
_meta: {
|
||||
name: string
|
||||
type: ResolverType
|
||||
filePath: string
|
||||
apiUrl: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface EnhancedResolver<TInput, TResult>
|
||||
extends Resolver<TInput, TResult>,
|
||||
ResolverEnhancement {
|
||||
middleware?: Middleware[]
|
||||
}
|
||||
export interface EnhancedResolverRpcClient<TInput, TResult>
|
||||
extends ResolverRpc<TInput, TResult>,
|
||||
ResolverEnhancement {}
|
||||
|
||||
type RequestIdleCallbackHandle = any
|
||||
type RequestIdleCallbackOptions = {
|
||||
timeout: number
|
||||
}
|
||||
type RequestIdleCallbackDeadline = {
|
||||
readonly didTimeout: boolean
|
||||
timeRemaining: () => number
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
requestIdleCallback: (
|
||||
callback: (deadline: RequestIdleCallbackDeadline) => void,
|
||||
opts?: RequestIdleCallbackOptions,
|
||||
) => RequestIdleCallbackHandle
|
||||
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import {
|
||||
useInfiniteQuery as useInfiniteReactQuery,
|
||||
InfiniteQueryResult,
|
||||
InfiniteQueryConfig,
|
||||
} from "react-query"
|
||||
import {emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {getQueryCacheFunctions, QueryCacheFunctions, getInfiniteQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
|
||||
type RestQueryResult<T extends QueryFn> = Omit<
|
||||
InfiniteQueryResult<PromiseReturnType<T>, any>,
|
||||
"resolvedData"
|
||||
> &
|
||||
QueryCacheFunctions<PromiseReturnType<T>[]>
|
||||
|
||||
const isServer = typeof window === "undefined"
|
||||
|
||||
export function useInfiniteQuery<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
|
||||
options: InfiniteQueryConfig<PromiseReturnType<T>, any>,
|
||||
): [PromiseReturnType<T>[], RestQueryResult<T>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
|
||||
}
|
||||
|
||||
if (typeof params === "undefined") {
|
||||
throw new Error(
|
||||
"useInfiniteQuery is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getInfiniteQueryKey(queryFn, params)
|
||||
|
||||
const {data, ...queryRest} = useInfiniteReactQuery({
|
||||
queryKey,
|
||||
queryFn: (_infinite: boolean, _apiUrl: string, params: any, resultOfGetFetchMore?: any) =>
|
||||
queryRpcFn(params, {fromQueryHook: true, resultOfGetFetchMore}),
|
||||
config: {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
...options,
|
||||
},
|
||||
})
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
|
||||
}
|
||||
|
||||
return [data as PromiseReturnType<T>[], rest as RestQueryResult<T>]
|
||||
}
|
||||
47
packages/core/src/use-mutation.ts
Normal file
47
packages/core/src/use-mutation.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
useMutation as useReactQueryMutation,
|
||||
MutationResult,
|
||||
MutationConfig,
|
||||
MutateConfig,
|
||||
} from "react-query"
|
||||
import {validateQueryFn} from "./utils/react-query-utils"
|
||||
|
||||
/*
|
||||
* We have to override react-query's MutationFunction and MutationResultPair
|
||||
* types so because we have throwOnError:true by default. And by the RQ types
|
||||
* have the mutate function result typed as TResult|undefined which isn't typed
|
||||
* properly with throwOnError.
|
||||
*
|
||||
* So this fixes that.
|
||||
*/
|
||||
export declare type MutateFunction<
|
||||
TResult,
|
||||
TError = unknown,
|
||||
TVariables = unknown,
|
||||
TSnapshot = unknown
|
||||
> = (
|
||||
variables?: TVariables,
|
||||
config?: MutateConfig<TResult, TError, TVariables, TSnapshot>,
|
||||
) => Promise<TResult>
|
||||
|
||||
export declare type MutationResultPair<TResult, TError, TVariables, TSnapshot> = [
|
||||
MutateFunction<TResult, TError, TVariables, TSnapshot>,
|
||||
MutationResult<TResult, TError>,
|
||||
]
|
||||
|
||||
export declare type MutationFunction<TResult, TVariables = unknown> = (
|
||||
variables: TVariables,
|
||||
ctx?: any,
|
||||
) => Promise<TResult>
|
||||
|
||||
export function useMutation<TResult, TError = unknown, TVariables = undefined, TSnapshot = unknown>(
|
||||
mutationResolver: MutationFunction<TResult, TVariables>,
|
||||
config?: MutationConfig<TResult, TError, TVariables, TSnapshot>,
|
||||
) {
|
||||
validateQueryFn(mutationResolver)
|
||||
|
||||
return useReactQueryMutation(mutationResolver, {
|
||||
throwOnError: true,
|
||||
...config,
|
||||
}) as MutationResultPair<TResult, TError, TVariables, TSnapshot>
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import {
|
||||
usePaginatedQuery as usePaginatedReactQuery,
|
||||
PaginatedQueryResult,
|
||||
PaginatedQueryConfig,
|
||||
} from "react-query"
|
||||
import {emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
|
||||
type RestQueryResult<T extends QueryFn> = Omit<
|
||||
PaginatedQueryResult<PromiseReturnType<T>>,
|
||||
"resolvedData"
|
||||
> &
|
||||
QueryCacheFunctions<PromiseReturnType<T>>
|
||||
|
||||
const isServer = typeof window === "undefined"
|
||||
|
||||
export function usePaginatedQuery<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
|
||||
options?: PaginatedQueryConfig<PromiseReturnType<T>>,
|
||||
): [PromiseReturnType<T>, RestQueryResult<T>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
|
||||
}
|
||||
|
||||
if (typeof params === "undefined") {
|
||||
throw new Error(
|
||||
"usePaginatedQuery is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getQueryKey(queryFn, params)
|
||||
|
||||
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
|
||||
queryKey,
|
||||
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
config: {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
...options,
|
||||
},
|
||||
})
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
|
||||
}
|
||||
|
||||
return [resolvedData as PromiseReturnType<T>, rest as RestQueryResult<T>]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {useMemo} from "react"
|
||||
import {fromPairs} from "lodash"
|
||||
import {useRouter} from "next/router"
|
||||
import {useRouterQuery} from "./use-router-query"
|
||||
import {fromPairs} from "lodash"
|
||||
|
||||
type ParsedUrlQueryValue = string | string[] | undefined
|
||||
|
||||
@@ -48,39 +49,43 @@ export function useParams(returnType?: "string" | "number" | "array") {
|
||||
const router = useRouter()
|
||||
const query = useRouterQuery()
|
||||
|
||||
const rawParams = extractRouterParams(router.query, query)
|
||||
const params = useMemo(() => {
|
||||
const rawParams = extractRouterParams(router.query, query)
|
||||
|
||||
if (returnType === "string") {
|
||||
const params: Record<string, string> = {}
|
||||
for (const key in rawParams) {
|
||||
if (typeof rawParams[key] === "string") {
|
||||
params[key] = rawParams[key] as string
|
||||
if (returnType === "string") {
|
||||
const params: Record<string, string> = {}
|
||||
for (const key in rawParams) {
|
||||
if (typeof rawParams[key] === "string") {
|
||||
params[key] = rawParams[key] as string
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
if (returnType === "number") {
|
||||
const params: Record<string, number> = {}
|
||||
for (const key in rawParams) {
|
||||
if (rawParams[key]) {
|
||||
params[key] = Number(rawParams[key])
|
||||
if (returnType === "number") {
|
||||
const params: Record<string, number> = {}
|
||||
for (const key in rawParams) {
|
||||
if (rawParams[key]) {
|
||||
params[key] = Number(rawParams[key])
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
if (returnType === "array") {
|
||||
const params: Record<string, Array<string | undefined>> = {}
|
||||
for (const key in rawParams) {
|
||||
if (Array.isArray(rawParams[key])) {
|
||||
params[key] = rawParams[key] as Array<string | undefined>
|
||||
if (returnType === "array") {
|
||||
const params: Record<string, Array<string | undefined>> = {}
|
||||
for (const key in rawParams) {
|
||||
if (Array.isArray(rawParams[key])) {
|
||||
params[key] = rawParams[key] as Array<string | undefined>
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
return rawParams
|
||||
return rawParams
|
||||
}, [router.query, query, returnType])
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export function useParam(key: string): undefined | string | string[]
|
||||
|
||||
145
packages/core/src/use-query-hooks.ts
Normal file
145
packages/core/src/use-query-hooks.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
useQuery as useReactQuery,
|
||||
QueryResult,
|
||||
QueryConfig,
|
||||
usePaginatedQuery as usePaginatedReactQuery,
|
||||
PaginatedQueryResult,
|
||||
PaginatedQueryConfig,
|
||||
useInfiniteQuery as useInfiniteReactQuery,
|
||||
InfiniteQueryResult,
|
||||
InfiniteQueryConfig as RQInfiniteQueryConfig,
|
||||
queryCache,
|
||||
} from "react-query"
|
||||
import {FirstParam, QueryFn, PromiseReturnType} from "./types"
|
||||
import {
|
||||
QueryCacheFunctions,
|
||||
getQueryCacheFunctions,
|
||||
getQueryKey,
|
||||
sanitize,
|
||||
defaultQueryConfig,
|
||||
} from "./utils/react-query-utils"
|
||||
import Router from "next/router"
|
||||
|
||||
Router.events.on("routeChangeComplete", async () => {
|
||||
await queryCache.invalidateQueries()
|
||||
})
|
||||
|
||||
// -------------------------
|
||||
// useQuery
|
||||
// -------------------------
|
||||
type RestQueryResult<TResult> = Omit<QueryResult<TResult>, "data"> & QueryCacheFunctions<TResult>
|
||||
|
||||
export function useQuery<T extends QueryFn, TResult = PromiseReturnType<T>>(
|
||||
queryFn: T,
|
||||
params: FirstParam<T>,
|
||||
options?: QueryConfig<TResult>,
|
||||
): [TResult, RestQueryResult<TResult>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("useQuery is missing the first argument - it must be a query function")
|
||||
}
|
||||
|
||||
const enhancedResolverRpcClient = sanitize(queryFn)
|
||||
const queryKey = getQueryKey(queryFn, params)
|
||||
|
||||
const {data, ...queryRest} = useReactQuery({
|
||||
queryKey,
|
||||
queryFn: (_apiUrl: string, params: any) =>
|
||||
enhancedResolverRpcClient(params, {fromQueryHook: true, alreadySerialized: true}),
|
||||
config: {
|
||||
...defaultQueryConfig,
|
||||
...options,
|
||||
},
|
||||
})
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<TResult>(queryKey),
|
||||
}
|
||||
|
||||
return [data as TResult, rest as RestQueryResult<TResult>]
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// usePaginatedQuery
|
||||
// -------------------------
|
||||
type RestPaginatedResult<TResult> = Omit<PaginatedQueryResult<TResult>, "resolvedData"> &
|
||||
QueryCacheFunctions<TResult>
|
||||
|
||||
export function usePaginatedQuery<T extends QueryFn, TResult = PromiseReturnType<T>>(
|
||||
queryFn: T,
|
||||
params: FirstParam<T>,
|
||||
options?: PaginatedQueryConfig<TResult>,
|
||||
): [TResult, RestPaginatedResult<TResult>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
|
||||
}
|
||||
|
||||
const enhancedResolverRpcClient = sanitize(queryFn)
|
||||
const queryKey = getQueryKey(queryFn, params)
|
||||
|
||||
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
|
||||
queryKey,
|
||||
queryFn: (_apiUrl: string, params: any) =>
|
||||
enhancedResolverRpcClient(params, {fromQueryHook: true, alreadySerialized: true}),
|
||||
config: {
|
||||
...defaultQueryConfig,
|
||||
...options,
|
||||
},
|
||||
})
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<TResult>(queryKey),
|
||||
}
|
||||
|
||||
return [resolvedData as TResult, rest as RestPaginatedResult<TResult>]
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// useInfiniteQuery
|
||||
// -------------------------
|
||||
type RestInfiniteResult<TResult> = Omit<InfiniteQueryResult<TResult>, "resolvedData"> &
|
||||
QueryCacheFunctions<TResult>
|
||||
|
||||
interface InfiniteQueryConfig<TResult, TFetchMoreResult> extends RQInfiniteQueryConfig<TResult> {
|
||||
getFetchMore?: (lastPage: TResult, allPages: TResult[]) => TFetchMoreResult
|
||||
}
|
||||
|
||||
// TODO - Fix TFetchMoreResult not actually taking affect in apps.
|
||||
// It shows as 'unknown' in the params() input argumunt, but should show as TFetchMoreResult
|
||||
export function useInfiniteQuery<
|
||||
T extends QueryFn,
|
||||
TFetchMoreResult = any,
|
||||
TResult = PromiseReturnType<T>
|
||||
>(
|
||||
queryFn: T,
|
||||
params: (fetchMoreResult: TFetchMoreResult) => FirstParam<T>,
|
||||
options: InfiniteQueryConfig<TResult, TFetchMoreResult>,
|
||||
): [TResult[], RestInfiniteResult<TResult>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
|
||||
}
|
||||
|
||||
const enhancedResolverRpcClient = sanitize(queryFn)
|
||||
const queryKey = getQueryKey(queryFn)
|
||||
|
||||
const {data, ...queryRest} = useInfiniteReactQuery({
|
||||
// we need an extra cache key for infinite loading so that the cache for
|
||||
// for this query is stored separately since the hook result is an array of results.
|
||||
// Without this cache for usePaginatedQuery and this will conflict and break.
|
||||
queryKey: [...queryKey, "infinite"],
|
||||
queryFn: (_apiUrl: string, _infinite: string, resultOfGetFetchMore: TFetchMoreResult) =>
|
||||
enhancedResolverRpcClient(params(resultOfGetFetchMore), {fromQueryHook: true}),
|
||||
config: {
|
||||
...defaultQueryConfig,
|
||||
...options,
|
||||
},
|
||||
})
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<TResult>(queryKey),
|
||||
}
|
||||
|
||||
return [data as TResult[], rest as RestInfiniteResult<TResult>]
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import {useQuery as useReactQuery, QueryResult, QueryConfig} from "react-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
|
||||
type RestQueryResult<T extends QueryFn> = Omit<QueryResult<PromiseReturnType<T>>, "data"> &
|
||||
QueryCacheFunctions<PromiseReturnType<T>>
|
||||
|
||||
export const emptyQueryFn: EnhancedRpcFunction = (() => {
|
||||
const fn = () => new Promise(() => {})
|
||||
fn._meta = {
|
||||
name: "emptyQueryFn",
|
||||
type: "n/a",
|
||||
path: "n/a",
|
||||
apiUrl: "",
|
||||
}
|
||||
return fn
|
||||
})()
|
||||
|
||||
const isServer = typeof window === "undefined"
|
||||
|
||||
export const retryFunction = (failureCount: number, error: any) => {
|
||||
if (process.env.NODE_ENV !== "production") return false
|
||||
|
||||
// Retry (max. 3 times) only if network error detected
|
||||
if (error.message === "Network request failed" && failureCount <= 3) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function useQuery<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
|
||||
options?: QueryConfig<PromiseReturnType<T>>,
|
||||
): [PromiseReturnType<T>, RestQueryResult<T>] {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("useQuery is missing the first argument - it must be a query function")
|
||||
}
|
||||
|
||||
if (typeof params === "undefined") {
|
||||
throw new Error(
|
||||
"useQuery is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getQueryKey(queryFn, params)
|
||||
|
||||
const {data, ...queryRest} = useReactQuery({
|
||||
queryKey,
|
||||
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
config: {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
...options,
|
||||
},
|
||||
})
|
||||
|
||||
const rest = {
|
||||
...queryRest,
|
||||
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
|
||||
}
|
||||
|
||||
return [data as PromiseReturnType<T>, rest as RestQueryResult<T>]
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import {useMemo} from "react"
|
||||
import {useRouter} from "next/router"
|
||||
import {parse} from "url"
|
||||
|
||||
export function useRouterQuery() {
|
||||
const router = useRouter()
|
||||
const {query} = parse(router.asPath, true)
|
||||
|
||||
const query = useMemo(() => {
|
||||
const {query} = parse(router.asPath, true)
|
||||
|
||||
return query
|
||||
}, [router.asPath])
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
18
packages/core/src/utils/cookie.ts
Normal file
18
packages/core/src/utils/cookie.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// Taken from https://github.com/HenrikJoreteg/cookie-getter
|
||||
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
|
||||
export function readCookie(name: string) {
|
||||
if (typeof document === "undefined") return null
|
||||
const cookie = document.cookie
|
||||
const setPos = cookie.search(new RegExp("\\b" + name + "="))
|
||||
const stopPos = cookie.indexOf(";", setPos)
|
||||
let res
|
||||
if (!~setPos) return null
|
||||
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1])
|
||||
return res.charAt(0) === "{" ? JSON.parse(res) : res
|
||||
}
|
||||
|
||||
export const setCookie = (name: string, value: string, expires: string) => {
|
||||
const result = `${name}=${value};path=/;expires=${expires}`
|
||||
document.cookie = result
|
||||
}
|
||||
export const deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")
|
||||
@@ -1,12 +1,8 @@
|
||||
import {QueryKey} from "react-query"
|
||||
import {BlitzApiRequest} from "../"
|
||||
import {IncomingMessage} from "http"
|
||||
|
||||
export const isServer = typeof window === "undefined"
|
||||
|
||||
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKey[]] {
|
||||
return [cacheKey, typeof params === "function" ? (params as Function)() : params]
|
||||
}
|
||||
export const isClient = typeof window !== "undefined"
|
||||
|
||||
export function isLocalhost(req: BlitzApiRequest | IncomingMessage): boolean {
|
||||
let {host} = req.headers
|
||||
@@ -17,3 +13,9 @@ export function isLocalhost(req: BlitzApiRequest | IncomingMessage): boolean {
|
||||
}
|
||||
return localhost
|
||||
}
|
||||
|
||||
export function clientDebug(...args: any) {
|
||||
if (typeof window !== "undefined" && (window as any)["DEBUG_BLITZ"]) {
|
||||
console.log("[BLITZ]", ...args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import {queryCache, QueryKey} from "react-query"
|
||||
import {serialize} from "superjson"
|
||||
import {InferUnaryParam, QueryFn} from "../types"
|
||||
import {EnhancedRpcFunction} from "rpc"
|
||||
|
||||
type MutateOptions = {
|
||||
refetch?: boolean
|
||||
}
|
||||
|
||||
export interface QueryCacheFunctions<T> {
|
||||
mutate: (newData: T | ((oldData: T | undefined) => T), opts?: MutateOptions) => void
|
||||
}
|
||||
|
||||
export const getQueryCacheFunctions = <T>(queryKey: QueryKey): QueryCacheFunctions<T> => ({
|
||||
mutate: (newData, opts = {refetch: true}) => {
|
||||
queryCache.setQueryData(queryKey, newData)
|
||||
if (opts.refetch) {
|
||||
return queryCache.invalidateQueries(queryKey, {refetchActive: true})
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
|
||||
export function getQueryKey<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
|
||||
) {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("getQueryKey is missing the first argument - it must be a query function")
|
||||
}
|
||||
if (typeof params === "undefined") {
|
||||
throw new Error(
|
||||
"getQueryKey is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
|
||||
)
|
||||
}
|
||||
|
||||
const queryKey: [string, Record<string, any>] = [
|
||||
((queryFn as unknown) as EnhancedRpcFunction)._meta.apiUrl,
|
||||
serialize(typeof params === "function" ? (params as Function)() : params),
|
||||
]
|
||||
return queryKey
|
||||
}
|
||||
|
||||
export function getInfiniteQueryKey<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
|
||||
) {
|
||||
if (typeof queryFn === "undefined") {
|
||||
throw new Error("getQueryKey is missing the first argument - it must be a query function")
|
||||
}
|
||||
if (typeof params === "undefined") {
|
||||
throw new Error(
|
||||
"getQueryKey is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
|
||||
)
|
||||
}
|
||||
|
||||
const queryKey: ["infinite", string, Record<string, any>] = [
|
||||
// we need an extra cache key for infinite loading so that the cache for
|
||||
// for this query is stored separately since the hook result is an array of results. Without this cache for usePaginatedQuery and this will conflict and break.
|
||||
"infinite",
|
||||
((queryFn as unknown) as EnhancedRpcFunction)._meta.apiUrl,
|
||||
serialize(typeof params === "function" ? (params as Function)() : params),
|
||||
]
|
||||
return queryKey
|
||||
}
|
||||
128
packages/core/src/utils/react-query-utils.ts
Normal file
128
packages/core/src/utils/react-query-utils.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {queryCache, QueryKey} from "react-query"
|
||||
import {serialize} from "superjson"
|
||||
import {Resolver, EnhancedResolverRpcClient, QueryFn} from "../types"
|
||||
import {isServer, isClient} from "."
|
||||
|
||||
type MutateOptions = {
|
||||
refetch?: boolean
|
||||
}
|
||||
|
||||
function isEnhancedResolverRpcClient(f: any): f is EnhancedResolverRpcClient<any, any> {
|
||||
return !!f._meta
|
||||
}
|
||||
|
||||
export interface QueryCacheFunctions<T> {
|
||||
mutate: (
|
||||
newData: T | ((oldData: T | undefined) => T),
|
||||
opts?: MutateOptions,
|
||||
) => Promise<void | ReturnType<typeof queryCache.invalidateQueries>>
|
||||
}
|
||||
|
||||
export const getQueryCacheFunctions = <T>(queryKey: QueryKey): QueryCacheFunctions<T> => ({
|
||||
mutate: (newData, opts = {refetch: true}) => {
|
||||
return new Promise((res) => {
|
||||
queryCache.setQueryData(queryKey, newData)
|
||||
let result: void | ReturnType<typeof queryCache.invalidateQueries>
|
||||
if (opts.refetch) {
|
||||
result = res(queryCache.invalidateQueries(queryKey, {refetchActive: true}))
|
||||
}
|
||||
if (isClient) {
|
||||
// Fix for https://github.com/blitz-js/blitz/issues/1174
|
||||
window.requestIdleCallback(() => {
|
||||
res(result)
|
||||
})
|
||||
} else {
|
||||
res(result)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const emptyQueryFn: EnhancedResolverRpcClient<unknown, unknown> = (() => {
|
||||
const fn = () => new Promise(() => {})
|
||||
fn._meta = {
|
||||
name: "emptyQueryFn",
|
||||
type: "n/a" as any,
|
||||
filePath: "n/a",
|
||||
apiUrl: "",
|
||||
}
|
||||
return fn
|
||||
})()
|
||||
|
||||
export const validateQueryFn = <TInput, TResult>(
|
||||
queryFn: Resolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>,
|
||||
) => {
|
||||
if (!isEnhancedResolverRpcClient(queryFn)) {
|
||||
throw new Error(
|
||||
`It looks like you are trying to use Blitz's useQuery to fetch from third-party APIs. To do that, import useQuery directly from "react-query"`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const sanitize = <TInput, TResult>(
|
||||
queryFn: Resolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>,
|
||||
) => {
|
||||
if (isServer) {
|
||||
// Prevents logging garbage during static pre-rendering
|
||||
return emptyQueryFn
|
||||
}
|
||||
|
||||
validateQueryFn(queryFn)
|
||||
|
||||
return queryFn as EnhancedResolverRpcClient<TInput, TResult>
|
||||
}
|
||||
|
||||
export const getQueryKeyFromUrlAndParams = (url: string, params: unknown) => {
|
||||
const queryKey = [url]
|
||||
|
||||
const args = typeof params === "function" ? (params as Function)() : params
|
||||
queryKey.push(serialize(args) as any)
|
||||
|
||||
return queryKey as [string, any]
|
||||
}
|
||||
|
||||
export function getQueryKey<TInput, TResult, T extends QueryFn>(
|
||||
resolver: T | Resolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>,
|
||||
params?: TInput,
|
||||
) {
|
||||
if (typeof resolver === "undefined") {
|
||||
throw new Error("getQueryKey is missing the first argument - it must be a resolver function")
|
||||
}
|
||||
|
||||
return getQueryKeyFromUrlAndParams(sanitize(resolver)._meta.apiUrl, params)
|
||||
}
|
||||
|
||||
export function invalidateQuery<TInput, TResult, T extends QueryFn>(
|
||||
resolver: T | Resolver<TInput, TResult> | EnhancedResolverRpcClient<TInput, TResult>,
|
||||
params?: TInput,
|
||||
) {
|
||||
if (typeof resolver === "undefined") {
|
||||
throw new Error(
|
||||
"invalidateQuery is missing the first argument - it must be a resolver function",
|
||||
)
|
||||
}
|
||||
|
||||
const fullQueryKey = getQueryKey(resolver, params)
|
||||
let queryKey: any
|
||||
if (params) {
|
||||
queryKey = fullQueryKey
|
||||
} else {
|
||||
// Params not provided, only use first query key item (url)
|
||||
queryKey = fullQueryKey[0]
|
||||
}
|
||||
return queryCache.invalidateQueries(queryKey)
|
||||
}
|
||||
|
||||
export const retryFunction = (failureCount: number, error: any) => {
|
||||
if (process.env.NODE_ENV !== "production") return false
|
||||
|
||||
// Retry (max. 3 times) only if network error detected
|
||||
if (error.message === "Network request failed" && failureCount <= 3) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const defaultQueryConfig = {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
}
|
||||
3
packages/core/test/__snapshots__/use-query.test.tsx.snap
Normal file
3
packages/core/test/__snapshots__/use-query.test.tsx.snap
Normal file
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`useQuery a "query" that converts the string parameter to uppercase shouldn't work with regular functions 1`] = `"It looks like you are trying to use Blitz's useQuery to fetch from third-party APIs. To do that, import useQuery directly from \\"react-query\\""`;
|
||||
@@ -1,18 +1,21 @@
|
||||
import {getQueryCacheFunctions} from "../src/utils/query-cache"
|
||||
import {getQueryCacheFunctions} from "../src/utils/react-query-utils"
|
||||
import {queryCache} from "react-query"
|
||||
|
||||
jest.mock("react-query")
|
||||
|
||||
describe("getQueryCacheFunctions", () => {
|
||||
it("returns a mutate function with working options", () => {
|
||||
it("returns a mutate function with working options", async () => {
|
||||
window.requestIdleCallback = jest.fn((fn) => {
|
||||
fn({} as any)
|
||||
})
|
||||
const spyRefetchQueries = jest.spyOn(queryCache, "invalidateQueries")
|
||||
const {mutate} = getQueryCacheFunctions("testQueryKey")
|
||||
expect(mutate).toBeTruthy()
|
||||
mutate({newData: true})
|
||||
await mutate({newData: true})
|
||||
expect(spyRefetchQueries).toBeCalledTimes(1)
|
||||
mutate({newData: true}, {refetch: false})
|
||||
await mutate({newData: true}, {refetch: false})
|
||||
expect(spyRefetchQueries).toBeCalledTimes(1)
|
||||
mutate({newData: true}, {refetch: true})
|
||||
await mutate({newData: true}, {refetch: true})
|
||||
expect(spyRefetchQueries).toBeCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,4 @@
|
||||
import {executeRpcCall, getIsomorphicRpcHandler} from "@blitzjs/core"
|
||||
|
||||
global.fetch = jest.fn(() => Promise.resolve({json: () => ({result: null, error: null})}))
|
||||
import {executeRpcCall, getIsomorphicEnhancedResolver} from "@blitzjs/core"
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
@@ -10,11 +8,13 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
global.fetch = jest.fn(() => Promise.resolve({json: () => ({result: null, error: null})}))
|
||||
|
||||
describe("RPC", () => {
|
||||
describe("HEAD", () => {
|
||||
it("warms the endpoint", () => {
|
||||
it("warms the endpoint", async () => {
|
||||
expect.assertions(1)
|
||||
executeRpcCall.warm("/api/endpoint")
|
||||
await executeRpcCall.warm("/api/endpoint")
|
||||
expect(global.fetch).toBeCalled()
|
||||
})
|
||||
})
|
||||
@@ -32,15 +32,16 @@ describe("RPC", () => {
|
||||
const resolverModule = {
|
||||
default: jest.fn(),
|
||||
}
|
||||
const rpcFn = getIsomorphicRpcHandler(
|
||||
const rpcFn = getIsomorphicEnhancedResolver(
|
||||
resolverModule,
|
||||
"app/_resolvers/queries/getProduct",
|
||||
"testResolver",
|
||||
"query",
|
||||
"client",
|
||||
)
|
||||
|
||||
try {
|
||||
const result = await rpcFn("/api/endpoint", {paramOne: 1234})
|
||||
const result = await rpcFn({paramOne: 1234}, {fromQueryHook: true})
|
||||
expect(result).toBe("result")
|
||||
expect(fetchMock).toBeCalled()
|
||||
} finally {
|
||||
@@ -59,15 +60,16 @@ describe("RPC", () => {
|
||||
const resolverModule = {
|
||||
default: jest.fn(),
|
||||
}
|
||||
const rpcFn = getIsomorphicRpcHandler(
|
||||
const rpcFn = getIsomorphicEnhancedResolver(
|
||||
resolverModule,
|
||||
"app/_resolvers/queries/getProduct",
|
||||
"testResolver",
|
||||
"query",
|
||||
"client",
|
||||
)
|
||||
|
||||
try {
|
||||
await expect(rpcFn("/api/endpoint", {paramOne: 1234})).rejects.toThrowError(
|
||||
await expect(rpcFn({paramOne: 1234}, {fromQueryHook: true})).rejects.toThrowError(
|
||||
/something broke/,
|
||||
)
|
||||
} finally {
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import React from "react"
|
||||
import {act, render, waitForElementToBeRemoved, screen} from "./test-utils"
|
||||
import {useQuery} from "../src/use-query"
|
||||
import {useQuery} from "../src/use-query-hooks"
|
||||
import {deserialize} from "superjson"
|
||||
|
||||
// This enhance fn does what getIsomorphicEnhancedResolver does during build time
|
||||
const enhance = (fn: any) => {
|
||||
const newFn = (...args: any) => {
|
||||
const [data, ...rest] = args
|
||||
return fn(deserialize(data), ...rest)
|
||||
}
|
||||
newFn._meta = {
|
||||
name: "testResolver",
|
||||
type: "query",
|
||||
path: "app/test",
|
||||
apiUrl: "test/url",
|
||||
}
|
||||
return newFn
|
||||
}
|
||||
|
||||
describe("useQuery", () => {
|
||||
const setupHook = (
|
||||
params: any,
|
||||
queryFn: (...args: any) => Promise<any>,
|
||||
): [{data?: any}, Function] => {
|
||||
// This enhance fn does what getIsomorphicRpcHandler does during build time
|
||||
const enhance = (fn: any) => {
|
||||
const newFn = (...args: any) => {
|
||||
const [data, ...rest] = args
|
||||
return fn(deserialize(data), ...rest)
|
||||
}
|
||||
newFn._meta = {
|
||||
name: "testResolver",
|
||||
type: "query",
|
||||
path: "app/test",
|
||||
apiUrl: "test/url",
|
||||
}
|
||||
return newFn
|
||||
}
|
||||
let res = {}
|
||||
function TestHarness() {
|
||||
useQuery(
|
||||
enhance((num: number) => num),
|
||||
1,
|
||||
)
|
||||
const [data] = useQuery(enhance(queryFn), params)
|
||||
const [data] = useQuery(queryFn, params)
|
||||
Object.assign(res, {data})
|
||||
return <div id="harness">{data ? "Ready" : "Missing Dependency"}</div>
|
||||
}
|
||||
@@ -48,13 +45,17 @@ describe("useQuery", () => {
|
||||
const upcase = async (args: string): Promise<string> => {
|
||||
return args.toUpperCase()
|
||||
}
|
||||
it("should work", async () => {
|
||||
const [res] = setupHook("test", upcase)
|
||||
it("should work with Blitz queries", async () => {
|
||||
const [res] = setupHook("test", enhance(upcase))
|
||||
await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
|
||||
await act(async () => {
|
||||
await screen.findByText("Ready")
|
||||
expect(res.data).toBe("TEST")
|
||||
})
|
||||
})
|
||||
|
||||
it("shouldn't work with regular functions", () => {
|
||||
expect(() => setupHook("test", upcase)).toThrowErrorMatchingSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/display",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"description": "Display package for the Blitz CLI",
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"license": "MIT",
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "4.0.0",
|
||||
"ora": "4.0.4"
|
||||
"ora": "4.0.4",
|
||||
"tslog": "2.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import chalk from "chalk"
|
||||
import c from "chalk"
|
||||
import ora from "ora"
|
||||
import readline from "readline"
|
||||
import {Logger} from "tslog"
|
||||
|
||||
export const chalk = c
|
||||
|
||||
// const blitzTrueBrandColor = '6700AB'
|
||||
const blitzBrightBrandColor = "8a3df0"
|
||||
@@ -9,23 +12,27 @@ const blitzBrightBrandColor = "8a3df0"
|
||||
const brandColor = blitzBrightBrandColor
|
||||
|
||||
const withBrand = (str: string) => {
|
||||
return chalk.hex(brandColor).bold(str)
|
||||
return c.hex(brandColor).bold(str)
|
||||
}
|
||||
|
||||
const withWarning = (str: string) => {
|
||||
return `⚠️ ${chalk.yellow(str)}`
|
||||
return `⚠️ ${c.yellow(str)}`
|
||||
}
|
||||
|
||||
const withCaret = (str: string) => {
|
||||
return `${chalk.gray(">")} ${str}`
|
||||
return `${c.gray(">")} ${str}`
|
||||
}
|
||||
|
||||
const withCheck = (str: string) => {
|
||||
return `${chalk.green("✔")} ${str}`
|
||||
return `${c.green("✔")} ${str}`
|
||||
}
|
||||
|
||||
const withX = (str: string) => {
|
||||
return `${chalk.red.bold("✕")} ${str}`
|
||||
return `${c.red.bold("✕")} ${str}`
|
||||
}
|
||||
|
||||
const withProgress = (str: string) => {
|
||||
return withCaret(c.bold(str))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,7 +41,7 @@ const withX = (str: string) => {
|
||||
* @param {string} msg
|
||||
*/
|
||||
const branded = (msg: string) => {
|
||||
console.log(chalk.hex(brandColor).bold(msg))
|
||||
console.log(c.hex(brandColor).bold(msg))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +70,7 @@ const warning = (msg: string) => {
|
||||
* @param {string} msg
|
||||
*/
|
||||
const error = (msg: string) => {
|
||||
console.error(withX(chalk.red.bold(msg)))
|
||||
console.error(withX(c.red.bold(msg)))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +79,7 @@ const error = (msg: string) => {
|
||||
* @param {string} msg
|
||||
*/
|
||||
const meta = (msg: string) => {
|
||||
console.log(withCaret(chalk.gray(msg)))
|
||||
console.log(withCaret(c.gray(msg)))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,11 +88,11 @@ const meta = (msg: string) => {
|
||||
* @param {string} msg
|
||||
*/
|
||||
const progress = (msg: string) => {
|
||||
console.log(withCaret(chalk.bold(msg)))
|
||||
console.log(withCaret(c.bold(msg)))
|
||||
}
|
||||
|
||||
const info = (msg: string) => {
|
||||
console.log(chalk.bold(msg))
|
||||
console.log(c.bold(msg))
|
||||
}
|
||||
|
||||
const spinner = (str: string) => {
|
||||
@@ -105,7 +112,7 @@ const spinner = (str: string) => {
|
||||
* @param {string} msg
|
||||
*/
|
||||
const success = (msg: string) => {
|
||||
console.log(withCheck(chalk.green(msg)))
|
||||
console.log(withCheck(c.green(msg)))
|
||||
}
|
||||
|
||||
const newline = () => {
|
||||
@@ -118,7 +125,7 @@ const newline = () => {
|
||||
* @param {string} val
|
||||
*/
|
||||
const variable = (val: any) => {
|
||||
return chalk.cyan.bold(`${val}`)
|
||||
return c.cyan.bold(`${val}`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +142,7 @@ export const log = {
|
||||
withCaret,
|
||||
withCheck,
|
||||
withX,
|
||||
withProgress,
|
||||
branded,
|
||||
clearLine,
|
||||
error,
|
||||
@@ -148,3 +156,20 @@ export const log = {
|
||||
info,
|
||||
debug,
|
||||
}
|
||||
|
||||
export const baseLogger = new Logger({
|
||||
dateTimePattern:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "year-month-day hour:minute:second.millisecond"
|
||||
: "hour:minute:second.millisecond",
|
||||
displayFunctionName: false,
|
||||
displayFilePath: "hidden",
|
||||
displayRequestId: false,
|
||||
dateTimeTimezone:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "utc"
|
||||
: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
prettyInspectHighlightStyles: {name: "black"},
|
||||
maskValuesOfKeys: ["password", "passwordConfirmation"],
|
||||
exposeErrorCodeFrame: process.env.NODE_ENV !== "production",
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/file-pipeline",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"description": "Display package for the Blitz CLI",
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import {unlink as unlinkFile, pathExists} from "fs-extra"
|
||||
import * as fs from "fs-extra"
|
||||
|
||||
import {relative, resolve} from "path"
|
||||
import {transform} from "../transform"
|
||||
import {transform} from "../../transform"
|
||||
import {EventedFile} from "types"
|
||||
|
||||
function getDestPath(folder: string, file: EventedFile) {
|
||||
const {history, cwd} = file
|
||||
const [firstPath] = history
|
||||
return resolve(folder, relative(cwd, firstPath))
|
||||
return resolve(folder, relative(file.cwd, file.path))
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file in the stream from the filesystem
|
||||
* @param folder The destination folder
|
||||
*/
|
||||
export function unlink(folder: string) {
|
||||
export function unlink(folder: string, unlinkFile = fs.unlink, pathExists = fs.pathExists) {
|
||||
return transform.file(async (file) => {
|
||||
if (file.event === "unlink" || file.event === "unlinkDir") {
|
||||
if (await pathExists(getDestPath(folder, file))) await unlinkFile(getDestPath(folder, file))
|
||||
const destPath = getDestPath(folder, file)
|
||||
if (await pathExists(destPath)) await unlinkFile(destPath)
|
||||
}
|
||||
|
||||
return file
|
||||
30
packages/file-pipeline/src/helpers/unlink/unlink.test.ts
Normal file
30
packages/file-pipeline/src/helpers/unlink/unlink.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {unlink} from "."
|
||||
import {normalize, resolve} from "path"
|
||||
import {take} from "../../test-utils"
|
||||
import File from "vinyl"
|
||||
|
||||
describe("unlink", () => {
|
||||
it("should unlink the correct path", async () => {
|
||||
const unlinkFile = jest.fn(() => Promise.resolve())
|
||||
const pathExists = jest.fn(() => Promise.resolve(true))
|
||||
|
||||
const unlinkStream = unlink(normalize("/dest"), unlinkFile, pathExists)
|
||||
|
||||
unlinkStream.write(
|
||||
new File({
|
||||
cwd: normalize("/src"),
|
||||
path: normalize("/src/bar/baz.tz"),
|
||||
content: null,
|
||||
event: "unlink",
|
||||
}),
|
||||
)
|
||||
|
||||
await take(unlinkStream, 1)
|
||||
|
||||
// Test the file exists before attempting to unlink it
|
||||
expect(pathExists).toHaveBeenCalledWith(resolve(normalize("/dest/bar/baz.tz")))
|
||||
|
||||
// Remove the correct file
|
||||
expect(unlinkFile).toHaveBeenCalledWith(resolve(normalize("/dest/bar/baz.tz")))
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/generator",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.24.0-canary.0",
|
||||
"description": "File generation for the Blitz CLI",
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"license": "MIT",
|
||||
@@ -36,7 +36,7 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/plugin-transform-typescript": "7.9.4",
|
||||
"@blitzjs/display": "0.23.1-canary.0",
|
||||
"@blitzjs/display": "0.24.0-canary.0",
|
||||
"@types/jscodeshift": "0.7.1",
|
||||
"chalk": "4.0.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
|
||||
@@ -22,6 +22,7 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
|
||||
sourceRoot: string = resolve(__dirname, "./templates/app")
|
||||
// Disable file-level prettier because we manually run prettier at the end
|
||||
prettierDisabled = true
|
||||
packageInstallSuccess: boolean = false
|
||||
|
||||
filesToIgnore() {
|
||||
if (!this.options.useTs) {
|
||||
@@ -45,43 +46,33 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
|
||||
async preCommit() {
|
||||
this.fs.move(this.destinationPath("gitignore"), this.destinationPath(".gitignore"))
|
||||
const pkg = this.fs.readJSON(this.destinationPath("package.json"))
|
||||
const ext = this.options.useTs ? "tsx" : "js"
|
||||
let type: string
|
||||
|
||||
switch (this.options.form) {
|
||||
case "React Final Form":
|
||||
this.fs.move(
|
||||
this.destinationPath("_forms/finalform/Form.tsx"),
|
||||
this.destinationPath("app/components/Form.tsx"),
|
||||
)
|
||||
this.fs.move(
|
||||
this.destinationPath("_forms/finalform/LabeledTextField.tsx"),
|
||||
this.destinationPath("app/components/LabeledTextField.tsx"),
|
||||
)
|
||||
type = "finalform"
|
||||
pkg.dependencies["final-form"] = "4.x"
|
||||
pkg.dependencies["react-final-form"] = "6.x"
|
||||
break
|
||||
case "React Hook Form":
|
||||
this.fs.move(
|
||||
this.destinationPath("_forms/hookform/Form.tsx"),
|
||||
this.destinationPath("app/components/Form.tsx"),
|
||||
)
|
||||
this.fs.move(
|
||||
this.destinationPath("_forms/hookform/LabeledTextField.tsx"),
|
||||
this.destinationPath("app/components/LabeledTextField.tsx"),
|
||||
)
|
||||
type = "hookform"
|
||||
pkg.dependencies["react-hook-form"] = "6.x"
|
||||
break
|
||||
case "Formik":
|
||||
this.fs.move(
|
||||
this.destinationPath("_forms/formik/Form.tsx"),
|
||||
this.destinationPath("app/components/Form.tsx"),
|
||||
)
|
||||
this.fs.move(
|
||||
this.destinationPath("_forms/formik/LabeledTextField.tsx"),
|
||||
this.destinationPath("app/components/LabeledTextField.tsx"),
|
||||
)
|
||||
type = "formik"
|
||||
pkg.dependencies["formik"] = "2.x"
|
||||
break
|
||||
}
|
||||
this.fs.move(
|
||||
this.destinationPath(`_forms/${type}/Form.${ext}`),
|
||||
this.destinationPath(`app/components/Form.${ext}`),
|
||||
)
|
||||
this.fs.move(
|
||||
this.destinationPath(`_forms/${type}/LabeledTextField.${ext}`),
|
||||
this.destinationPath(`app/components/LabeledTextField.${ext}`),
|
||||
)
|
||||
|
||||
this.fs.delete(this.destinationPath("_forms"))
|
||||
|
||||
this.fs.writeJSON(this.destinationPath("package.json"), pkg)
|
||||
@@ -186,6 +177,7 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
|
||||
if (code !== 0) spinners[spinners.length - 1].fail()
|
||||
else {
|
||||
spinners[spinners.length - 1].succeed()
|
||||
this.packageInstallSuccess = true
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
@@ -201,16 +193,18 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
|
||||
}
|
||||
|
||||
// Ensure the generated files are formatted with the installed prettier version
|
||||
const formattingSpinner = log.spinner(log.withBrand("Formatting your code")).start()
|
||||
const prettierResult = runLocalNodeCLI("prettier --loglevel silent --write .")
|
||||
if (prettierResult.status !== 0) {
|
||||
formattingSpinner.fail(
|
||||
chalk.yellow.bold(
|
||||
"We had an error running Prettier, but don't worry your app will still run fine :)",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
formattingSpinner.succeed()
|
||||
if (this.packageInstallSuccess) {
|
||||
const formattingSpinner = log.spinner(log.withBrand("Formatting your code")).start()
|
||||
const prettierResult = runLocalNodeCLI("prettier --loglevel silent --write .")
|
||||
if (prettierResult.status !== 0) {
|
||||
formattingSpinner.fail(
|
||||
chalk.yellow.bold(
|
||||
"We had an error running Prettier, but don't worry your app will still run fine :)",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
formattingSpinner.succeed()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("") // New line needed
|
||||
|
||||
@@ -47,8 +47,11 @@ export class PageGenerator extends Generator<PageGeneratorOptions> {
|
||||
}
|
||||
|
||||
getModelNamesPath() {
|
||||
const context = this.options.context ? `${this.options.context}/` : ""
|
||||
return context + this.options.modelNames
|
||||
const kebabCaseContext = this.options.context
|
||||
? `${camelCaseToKebabCase(this.options.context)}/`
|
||||
: ""
|
||||
const kebabCaseModelNames = camelCaseToKebabCase(this.options.modelNames)
|
||||
return kebabCaseContext + kebabCaseModelNames
|
||||
}
|
||||
|
||||
getTargetDirectory() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { Link } from "blitz"
|
||||
import { Link, useMutation } from "blitz"
|
||||
import { LabeledTextField } from "app/components/LabeledTextField"
|
||||
import { Form, FORM_ERROR } from "app/components/Form"
|
||||
import login from "app/auth/mutations/login"
|
||||
@@ -10,6 +10,8 @@ type LoginFormProps = {
|
||||
}
|
||||
|
||||
export const LoginForm = (props: LoginFormProps) => {
|
||||
const [loginMutation] = useMutation(login)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
@@ -20,7 +22,7 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
initialValues={{ email: "", password: "" }}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await login({ email: values.email, password: values.password })
|
||||
await loginMutation(values)
|
||||
props.onSuccess?.()
|
||||
} catch (error) {
|
||||
if (error.name === "AuthenticationError") {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react"
|
||||
import { useMutation } from "blitz"
|
||||
import { LabeledTextField } from "app/components/LabeledTextField"
|
||||
import { Form, FORM_ERROR } from "app/components/Form"
|
||||
import signup from "app/auth/mutations/signup"
|
||||
@@ -9,6 +10,8 @@ type SignupFormProps = {
|
||||
}
|
||||
|
||||
export const SignupForm = (props: SignupFormProps) => {
|
||||
const [signupMutation] = useMutation(signup)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Create an Account</h1>
|
||||
@@ -19,7 +22,7 @@ export const SignupForm = (props: SignupFormProps) => {
|
||||
initialValues={{ email: "", password: "" }}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await signup({ email: values.email, password: values.password })
|
||||
await signupMutation(values)
|
||||
props.onSuccess?.()
|
||||
} catch (error) {
|
||||
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { SessionContext } from "blitz"
|
||||
import { protect } from "blitz"
|
||||
import { authenticateUser } from "app/auth/auth-utils"
|
||||
import { LoginInput, LoginInputType } from "../validations"
|
||||
|
||||
export default async function login(input: LoginInputType, ctx: { session?: SessionContext } = {}) {
|
||||
// This throws an error if input is invalid
|
||||
const { email, password } = LoginInput.parse(input)
|
||||
import { LoginInput } from "../validations"
|
||||
|
||||
export default protect({ schema: LoginInput, authorize: false }, async function login(
|
||||
{ email, password },
|
||||
{ session }
|
||||
) {
|
||||
// This throws an error if credentials are invalid
|
||||
const user = await authenticateUser(email, password)
|
||||
|
||||
await ctx.session!.create({ userId: user.id, roles: [user.role] })
|
||||
await session.create({ userId: user.id, roles: [user.role] })
|
||||
|
||||
return user
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {SessionContext} from "blitz"
|
||||
import { Ctx } from "blitz"
|
||||
|
||||
export default async function logout(_ = null, ctx: {session?: SessionContext} = {}) {
|
||||
return await ctx.session!.revoke()
|
||||
export default async function logout(_: any, { session }: Ctx) {
|
||||
return await session.revoke()
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { protect } from "blitz"
|
||||
import db from "db"
|
||||
import { SessionContext } from "blitz"
|
||||
import { hashPassword } from "app/auth/auth-utils"
|
||||
import { SignupInput, SignupInputType } from "app/auth/validations"
|
||||
import { SignupInput } from "app/auth/validations"
|
||||
|
||||
export default async function signup(
|
||||
input: SignupInputType,
|
||||
ctx: { session?: SessionContext } = {}
|
||||
export default protect({ schema: SignupInput, authorize: false }, async function signup(
|
||||
{ email, password },
|
||||
{ session }
|
||||
) {
|
||||
// This throws an error if input is invalid
|
||||
const { email, password } = SignupInput.parse(input)
|
||||
|
||||
const hashedPassword = await hashPassword(password)
|
||||
const user = await db.user.create({
|
||||
data: { email: email.toLowerCase(), hashedPassword, role: "user" },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
})
|
||||
|
||||
await ctx.session!.create({ userId: user.id, roles: [user.role] })
|
||||
await session.create({ userId: user.id, roles: [user.role] })
|
||||
|
||||
return user
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,10 +4,8 @@ export const SignupInput = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(10).max(100),
|
||||
})
|
||||
export type SignupInputType = z.infer<typeof SignupInput>
|
||||
|
||||
export const LoginInput = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
})
|
||||
export type LoginInputType = z.infer<typeof LoginInput>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link, BlitzPage } from "blitz"
|
||||
import { Link, BlitzPage, useMutation } from "blitz"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import logout from "app/auth/mutations/logout"
|
||||
import { useCurrentUser } from "app/hooks/useCurrentUser"
|
||||
@@ -11,6 +11,7 @@ import { Suspense } from "react"
|
||||
|
||||
const UserInfo = () => {
|
||||
const currentUser = useCurrentUser()
|
||||
const [logoutMutation] = useMutation(logout)
|
||||
|
||||
if (currentUser) {
|
||||
return (
|
||||
@@ -18,7 +19,7 @@ const UserInfo = () => {
|
||||
<button
|
||||
className="button small"
|
||||
onClick={async () => {
|
||||
await logout()
|
||||
await logoutMutation()
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
@@ -58,7 +59,7 @@ const Home: BlitzPage = () => {
|
||||
<p>
|
||||
<strong>Congrats!</strong> Your app is ready, including user sign-up and log-in.
|
||||
</p>
|
||||
<div className="buttons" style={{ marginTop: "1rem", marginBottom: "5rem" }}>
|
||||
<div className="buttons" style={{ marginTop: "1rem", marginBottom: "1rem" }}>
|
||||
<Suspense fallback="Loading...">
|
||||
<UserInfo />
|
||||
</Suspense>
|
||||
@@ -77,7 +78,10 @@ const Home: BlitzPage = () => {
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Then go to{" "}
|
||||
Then <strong>restart the server</strong>
|
||||
<pre><code>Ctrl + c</code></pre>
|
||||
<pre><code>blitz start</code></pre>
|
||||
and go to{" "}
|
||||
<Link href="/projects">
|
||||
<a>/projects</a>
|
||||
</Link>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Ctx } from "blitz"
|
||||
import db from "db"
|
||||
import { SessionContext } from "blitz"
|
||||
|
||||
export default async function getCurrentUser(_ = null, ctx: { session?: SessionContext } = {}) {
|
||||
if (!ctx.session?.userId) return null
|
||||
export default async function getCurrentUser(_ = null, { session }: Ctx) {
|
||||
if (!session.userId) return null
|
||||
|
||||
const user = await db.user.findOne({
|
||||
where: { id: ctx.session!.userId },
|
||||
where: { id: session.userId },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ model User {
|
||||
name String?
|
||||
email String @unique
|
||||
hashedPassword String?
|
||||
role String @default("user")
|
||||
role String
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ module.exports = {
|
||||
"^.+\\.(ts|tsx)$": "babel-jest",
|
||||
},
|
||||
// This makes absolute imports work
|
||||
moduleDirectories: ["node_modules", "<rootDir>/node_modules", "."],
|
||||
moduleDirectories: ["node_modules", "."],
|
||||
modulePathIgnorePatterns: [".blitz"],
|
||||
moduleNameMapper: {
|
||||
// This ensures any path aliases in tsconfig also work in jest
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "tsc && lint-staged && pretty-quick --staged",
|
||||
"pre-push": "yarn lint && yarn test"
|
||||
"pre-push": "npm run lint && npm run test"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user