1
0
mirror of synced 2026-02-07 21:00:08 -05:00

Compare commits

..

1 Commits
protect ... bug

Author SHA1 Message Date
Brandon Bayer
c8700453a4 add pages 2020-09-24 12:16:46 -04:00
135 changed files with 1592 additions and 2170 deletions

View File

@@ -353,8 +353,7 @@
"profile": "https://github.com/ntgussoni",
"contributions": [
"test",
"code",
"review"
"code"
]
},
{
@@ -972,8 +971,7 @@
"avatar_url": "https://avatars2.githubusercontent.com/u/37571416?v=4",
"profile": "https://github.com/clgeoio",
"contributions": [
"code",
"test"
"code"
]
},
{
@@ -1011,8 +1009,8 @@
"avatar_url": "https://avatars1.githubusercontent.com/u/36962022?v=4",
"profile": "https://github.com/engelkes-finstreet",
"contributions": [
"doc",
"code"
"code",
"doc"
]
},
{
@@ -1170,60 +1168,6 @@
"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,

View File

@@ -24,12 +24,7 @@ module.exports = {
},
],
"@typescript-eslint/no-floating-promises": "error",
// 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"],
"no-use-before-define": ["error", {functions: false, classes: false}],
},
ignorePatterns: ["packages/cli/", "packages/generator/templates", ".eslintrc.js"],
overrides: [

View File

@@ -1 +0,0 @@
12.16.1

View File

@@ -6,7 +6,7 @@
<img alt="" src="https://img.shields.io/badge/Join%20our%20community-6700EB.svg?style=for-the-badge&labelColor=000000&logoWidth=20&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ9SURBVHgB7d3dVdtAEIbhcSpICUoH0IEogQqSVBBSAU4FSSpIOoAORAfQgSghHXzZ1U/YcMD4R9rZmf2ec3y448LyiNf27iLiGIAmPLrweC9Un3DhrzG6EarLNP09nlwJ1SOZ/lQr5N80/S/p2QMVCBf5N17XCfm1Y/rBHqjAG9PPHvBsz+mf9WAP+HLA9M/YA14cOP2payH7jpj+VCtk1wnTP+vj7xCy6cTpn7EHLMLp059iD1iD8eveJbVCNsSLheX1YA/YgOWnf8YeKB3Wmf7Ud6Fy4f/FHmtpxbl3YlC4MJ/Cj0bWdwPnPbARg+L0S54XQHS32WwuxClzd4CM0z9rPfeAuTtA5ulPXYQ7wZ04Y+oOoDD9KZc9YOoOoDj9s4dwFzgXR6w1wIPoOvPWA9buAHEJ173o3gWiy3AnuBUHLEbgmYwvAk1/wuM8vAgexThzbwPDkx7/DHwVXfFOxP2GmsKd4Ab6zPeAyU8CI7AHFmH2BRCBPXAyk18GzUrqAXCTiR4ssyj0VFw/oCU8+e+RZ33AWz6KMaYbIIWxB+JSLs1bsbkeMN0AqakHvoku9oA2sAfqBvbAQdw0QArsgb25aYBUQT3QgT2gB+yBuqGcHij2UCqXDZACe2Anlw2QYg/QAOyBuoE98CL3DZDCuK4/rh/Q7oGL6U+TOvcNkJoijN8X1C48+T+g75eQDrAH/qmqAVJgDwyqaoAUe4AGYA/UDZX3QLUNkEIZPRCd5+6BahsgVUgPROwBTSijB7jpVAvGHriHvmw9wAZ4BpX1ABvgmakHtPcbRuwBTWAPULgAV9D/jKDY9YRvwvgEaurD44uQHvAol7qBW7WKluVtIHiUS7GyvA0s6CiXDnxrpQfsgbqBS7GKk/2jYHCrVlGyfxTMrVo0ALdq1Q3sgSKofh0M9oA61a+D2QM0AHugbmAPqClmSRjK2apVVQ8UsySsoK1aHdgDesCtWnUDeyCrIpeFg1u3sylyWTi3btMA7IG6gT2wuuK3hoE9sKrit4YVslWLPaAN7IG6ocKt2zmY2h4O9sDiTG0PZw/QANy6XTewBxZj9ogYVHy025LMHhEz9cBn0We6B0yfERReBLfhx0/R1YQHPx/QBPbA0VwcEwf2wNFcHBPHHjiem3MC2QPHcXdSaJjA+KfgTPQ8hhfjBzHC40mhlzJ+Xq9lK4a4PCs43AVaGTed5mZq+iOXZwWHi3AnOj2wFWNcnxYe7gTxLtBKHuamP/J+Wnh8a5irB7ZC5Yk9gPX1QuXC+usHWqGyhYvUYR0a7zboUOFCNVhnk0krZAOW7wFOvzXhom2xnEbIHizTA1wEYhWW6YFGyC6c1gOcfg9wfA80Qj7g8B7g9HuCww+haIR8wf49wOn3Cvv9k8tGyC/s7gFOv3fY3QONkH+v9MBWqB7PeqDn9FcIT//kcitUn6kHOu/T/xfWzlQy3dEHhwAAAABJRU5ErkJggg==">
</a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-128-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-122-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<a aria-label="License" href="https://github.com/blitz-js/blitz/blob/canary/LICENSE">
<img alt="" src="https://img.shields.io/npm/l/blitz.svg?style=for-the-badge&labelColor=000000&color=blue">
@@ -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> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Antgussoni" title="Reviewed Pull Requests">👀</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>
</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> <a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/clgeoio"><img src="https://avatars2.githubusercontent.com/u/37571416?v=4" width="100px;" alt=""/><br /><sub><b>Cody G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/madflow"><img src="https://avatars0.githubusercontent.com/u/183248?v=4" width="100px;" alt=""/><br /><sub><b>madflow</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=madflow" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/nitaking_"><img src="https://avatars2.githubusercontent.com/u/10850034?v=4" width="100px;" alt=""/><br /><sub><b>Satoshi Nitawaki</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nitaking" title="Code">💻</a> <a href="#maintenance-nitaking" title="Maintenance">🚧</a> <a href="#question-nitaking" title="Answering Questions">💬</a></td>
<td align="center"><a href="https://github.com/sirmyron"><img src="https://avatars2.githubusercontent.com/u/1430136?v=4" width="100px;" alt=""/><br /><sub><b>sirmyron</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sirmyron" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/pixelscommander"><img src="https://avatars2.githubusercontent.com/u/810671?v=4" width="100px;" alt=""/><br /><sub><b>Denis Radin</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3APixelsCommander" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Documentation">📖</a></td>
@@ -374,20 +374,11 @@ 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!

View File

@@ -1,4 +1,5 @@
import {Link, useMutation} from "blitz"
import React from "react"
import {Link} from "blitz"
import {LabeledTextField} from "app/components/LabeledTextField"
import {Form, FORM_ERROR} from "app/components/Form"
import login from "app/auth/mutations/login"
@@ -9,7 +10,6 @@ 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 loginMutation(values)
await login({email: values.email, password: values.password})
props.onSuccess && props.onSuccess()
} catch (error) {
if (error.name === "AuthenticationError") {

View File

@@ -1,15 +1,15 @@
import {Ctx} from "blitz"
import {SessionContext} from "blitz"
import {authenticateUser} from "app/auth/auth-utils"
import {LoginInput, LoginInputType} from "../validations"
export default async function login(input: LoginInputType, {session}: Ctx) {
export default async function login(input: LoginInputType, ctx: {session?: SessionContext} = {}) {
// 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 session.create({userId: user.id, roles: [user.role]})
await ctx.session!.create({userId: user.id, roles: [user.role]})
return user
}

View File

@@ -1,5 +1,5 @@
import {Ctx} from "blitz"
import {SessionContext} from "blitz"
export default async function logout(_: any, {session}: Ctx) {
return await session.revoke()
export default async function logout(_ = null, ctx: {session?: SessionContext} = {}) {
return await ctx.session!.revoke()
}

View File

@@ -1,9 +1,9 @@
import {Ctx} from "blitz"
import db from "db"
import {SessionContext} from "blitz"
import {hashPassword} from "app/auth/auth-utils"
import {SignupInput, SignupInputType} from "app/auth/validations"
export default async function signup(input: SignupInputType, {session}: Ctx) {
export default async function signup(input: SignupInputType, ctx: {session?: SessionContext} = {}) {
// 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, {session}: Ctx) {
select: {id: true, name: true, email: true, role: true},
})
await session.create({userId: user.id, roles: [user.role]})
await ctx.session!.create({userId: user.id, roles: [user.role]})
return user
}

View File

@@ -1,3 +1,4 @@
import React from "react"
import {Head, useRouter, BlitzPage} from "blitz"
import {LoginForm} from "app/auth/components/LoginForm"

View File

@@ -1,4 +1,5 @@
import {Head, useRouter, BlitzPage, useMutation} from "blitz"
import React from "react"
import {Head, useRouter, BlitzPage} from "blitz"
import {Form, FORM_ERROR} from "app/components/Form"
import {LabeledTextField} from "app/components/LabeledTextField"
import signup from "app/auth/mutations/signup"
@@ -6,7 +7,6 @@ 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 signupMutation(values)
await signup({email: values.email, password: values.password})
router.push("/")
} catch (error) {
if (error.code === "P2002" && error.meta?.target?.includes("email")) {

View File

@@ -1,4 +1,4 @@
import {ReactNode, PropsWithoutRef} from "react"
import React, {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"

View File

@@ -1,4 +1,4 @@
import {forwardRef, PropsWithoutRef} from "react"
import React, {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 = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFieldProps>(
({name, label, outerProps, ...props}, ref) => {
const {
input,

View File

@@ -1,17 +1,16 @@
import {useSession, useRouter, useMutation} from "blitz"
import {useSession, useRouter} 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 logoutMutation()
await logout()
}}
>
Logout

View File

@@ -3,10 +3,6 @@ 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 (

View File

@@ -1,9 +1,9 @@
import {Suspense} from "react"
import {Head, Link, useSession, useRouterQuery, useMutation, invoke} from "blitz"
import {Head, Link, useSession, useRouterQuery} from "blitz"
import getUser from "app/users/queries/getUser"
import trackView from "app/users/mutations/trackView"
import Layout from "app/layouts/Layout"
import {useCurrentUser} from "app/hooks/useCurrentUser"
// import getUsers from "app/users/queries/getUsers"
const CurrentUserInfo = () => {
const currentUser = useCurrentUser()
@@ -11,21 +11,12 @@ 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 && (
@@ -49,15 +40,10 @@ const UserStuff = () => {
<Suspense fallback="Loading...">
<CurrentUserInfo />
</Suspense>
{/*
<Suspense fallback="Loading...">
<Users />
</Suspense>
*/}
<button
onClick={async () => {
try {
const user = await invoke(getUser, {where: {id: session.userId as number}})
const user = await getUser({where: {id: session.userId as number}})
alert(JSON.stringify(user))
} catch (error) {
alert("error: " + JSON.stringify(error))
@@ -69,7 +55,7 @@ const UserStuff = () => {
<button
onClick={async () => {
try {
await trackViewMutation()
await trackView()
} catch (error) {
alert("error: " + error)
console.log(error)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,10 @@
import {protect} from "blitz"
import db from "db"
import * as z from "zod"
import db, {UserCreateArgs} from "db"
export default protect(
{
schema: z.object({
name: z.string(),
}),
},
async function createUser(input, {session}) {
const user = await db.user.create({data: input})
type CreateUserInput = {
data: UserCreateArgs["data"]
}
export default async function createUser({data}: CreateUserInput, ctx: Record<any, any> = {}) {
const user = await db.user.create({data})
return user
},
)
return user
}

View File

@@ -1,16 +1,11 @@
import {protect} from "blitz"
import db from "db"
import * as z from "zod"
import db, {UserDeleteArgs} from "db"
export default protect(
{
schema: z.object({
id: z.number(),
}),
},
async function deleteUser({id}, {session}) {
const user = await db.user.delete({where: {id}})
type DeleteUserInput = {
where: UserDeleteArgs["where"]
}
return user
},
)
export default async function deleteUser({where}: DeleteUserInput, ctx: Record<any, any> = {}) {
const user = await db.user.delete({where})
return user
}

View File

@@ -1,9 +1,9 @@
import {Ctx} from "blitz"
import {SessionContext} from "blitz"
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})
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})
return
}

View File

@@ -1,17 +1,15 @@
import {protect} from "blitz"
import db from "db"
import * as z from "zod"
import db, {UserUpdateArgs} from "db"
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})
type UpdateUserInput = {
where: UserUpdateArgs["where"]
data: UserUpdateArgs["data"]
}
return user
},
)
export default async function updateUser(
{where, data}: UpdateUserInput,
ctx: Record<any, any> = {},
) {
const user = await db.user.update({where, data})
return user
}

View File

@@ -1,28 +1,29 @@
import React, {Suspense} from "react"
import Layout from "app/layouts/Layout"
import {Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import getUser from "app/users/queries/getUser"
import deleteUser from "app/users/mutations/deleteUser"
export const User = () => {
const router = useRouter()
const userId = useParam("userId", "number")
const [user] = useQuery(getUser, {id: userId})
const [user] = useQuery(getUser, {where: {id: userId}})
return (
<div>
<h1>User {user.id}</h1>
<pre>{JSON.stringify(user, null, 2)}</pre>
<Link href="/users/[userId]/edit" as={`/users/${user.id}/edit`}>
<a>Edit</a>
</Link>
{
<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({id: user.id})
await deleteUser({where: {id: user.id}})
router.push("/users")
}
}}
@@ -36,19 +37,26 @@ export const User = () => {
const ShowUserPage: BlitzPage = () => {
return (
<div>
<p>
<Link href="/users">
<a>Users</a>
</Link>
</p>
<Head>
<title>User</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Suspense fallback={<div>Loading...</div>}>
<User />
</Suspense>
<main>
<p>
{
<Link href="/users">
<a>Users</a>
</Link>
}
</p>
<Suspense fallback={<div>Loading...</div>}>
<User />
</Suspense>
</main>
</div>
)
}
ShowUserPage.getLayout = (page) => <Layout title={"User"}>{page}</Layout>
export default ShowUserPage

View File

@@ -1,6 +1,5 @@
import React, {Suspense} from "react"
import Layout from "app/layouts/Layout"
import {Link, useRouter, useQuery, useMutation, useParam, BlitzPage} from "blitz"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import getUser from "app/users/queries/getUser"
import updateUser from "app/users/mutations/updateUser"
import UserForm from "app/users/components/UserForm"
@@ -8,8 +7,7 @@ import UserForm from "app/users/components/UserForm"
export const EditUser = () => {
const router = useRouter()
const userId = useParam("userId", "number")
const [user, {mutate}] = useQuery(getUser, {id: userId})
const [updateUserMutation] = useMutation(updateUser)
const [user, {mutate}] = useQuery(getUser, {where: {id: userId}})
return (
<div>
@@ -20,11 +18,11 @@ export const EditUser = () => {
initialValues={user}
onSubmit={async () => {
try {
const updated = await updateUserMutation({
id: user.id,
name: "MyNewName",
const updated = await updateUser({
where: {id: user.id},
data: {name: "MyNewName"},
})
await mutate(updated)
mutate(updated)
alert("Success!" + JSON.stringify(updated))
router.push("/users/[userId]", `/users/${updated.id}`)
} catch (error) {
@@ -40,19 +38,26 @@ export const EditUser = () => {
const EditUserPage: BlitzPage = () => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<EditUser />
</Suspense>
<Head>
<title>Edit User</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<p>
<Link href="/users">
<a>Users</a>
</Link>
</p>
<main>
<Suspense fallback={<div>Loading...</div>}>
<EditUser />
</Suspense>
<p>
{
<Link href="/users">
<a>Users</a>
</Link>
}
</p>
</main>
</div>
)
}
EditUserPage.getLayout = (page) => <Layout title={"Edit User"}>{page}</Layout>
export default EditUserPage

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,29 @@
import {protect} from "blitz"
import db, {FindManyUserArgs} from "db"
import {SessionContext} from "blitz"
type GetUsersInput = Pick<FindManyUserArgs, "orderBy" | "skip" | "take">
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']
}
export default protect({}, async function getUsers(
{orderBy, skip = 0, take}: GetUsersInput,
{session},
export default async function getUsers(
{where, orderBy, cursor, take, skip}: GetUsersInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize(["admin"])
const users = await db.user.findMany({
where: {
// add your selection criteria here
},
where,
orderBy,
cursor,
take,
skip,
})
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,
}
})
return users
}

View File

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

View File

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

View File

@@ -43,27 +43,3 @@ 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
}

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/auth",
"version": "0.24.0-canary.0",
"version": "0.23.1-canary.0",
"scripts": {
"start": "blitz start",
"studio": "blitz db studio",
@@ -15,9 +15,6 @@
"browserslist": [
"defaults"
],
"prisma": {
"schema": "db/schema.prisma"
},
"prettier": {
"semi": false,
"printWidth": 100,
@@ -36,9 +33,9 @@
]
},
"dependencies": {
"@prisma/cli": "2.8.0",
"@prisma/client": "2.8.0",
"blitz": "0.24.0-canary.0",
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"final-form": "4.20.1",
"passport-auth0": "1.3.3",
"passport-github2": "0.1.11",
@@ -48,7 +45,7 @@
"react-error-boundary": "2.3.1",
"react-final-form": "6.5.1",
"secure-password": "4.0.0",
"zod": "1.11.9"
"zod": "1.10.0"
},
"devDependencies": {
"@cypress/skip-test": "2.5.0",

View File

@@ -1,12 +0,0 @@
import {DefaultCtx, SessionContext, DefaultPublicData} from "blitz"
import {User} from "db"
declare module "blitz" {
export interface Ctx extends DefaultCtx {
session: SessionContext
}
export interface PublicData extends DefaultPublicData {
userId: User["id"]
views?: number
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/plain-js",
"version": "0.24.0-canary.0",
"version": "0.23.1-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.24.0-canary.0",
"blitz": "0.23.1-canary.0",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8"
},

View File

@@ -2,6 +2,7 @@ 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()
@@ -11,8 +12,9 @@ function Product() {
return (
<ProductForm
product={product}
onSuccess={async () => {
await router.push("/admin/products")
onSuccess={() => {
queryCache.invalidateQueries("/api/products/queries/getProducts")
router.push("/admin/products")
}}
/>
)

View File

@@ -1,7 +1,7 @@
import {Suspense} from "react"
import {useQuery, Link, useRouterQuery, invalidateQuery} from "blitz"
import {useQuery, Link, useRouterQuery} 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,12 +17,7 @@ function ProductsList() {
{products.map((product) => (
<li key={product.id}>
<Link href="/admin/products/[id]" as={`/admin/products/${product.id}`}>
<a
// Disable until prefetch api added
//onMouseEnter={() => getProduct({where: {id: product.id}})}
>
{product.name}
</a>
<a onMouseEnter={() => getProduct({where: {id: product.id}})}>{product.name}</a>
</Link>{" "}
- Created: {product.createdAt.toISOString()}
</li>
@@ -36,8 +31,6 @@ function AdminProducts() {
<div>
<h1>Products</h1>
<button onClick={() => invalidateQuery(getProducts)}>Invalidate query</button>
<p>
<Link href="/admin/products/new">
<a>Create Product</a>

View File

@@ -2,7 +2,6 @@ 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
@@ -17,15 +16,13 @@ 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 createProductMutation({data})
const product = await createProduct({data})
onSuccess(product)
} catch (error) {
alert("Error creating product " + JSON.stringify(error, null, 2))
@@ -35,7 +32,7 @@ function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
// Can't update id
const id = data.id
delete data.id
const product = await updateProductMutation({where: {id}, data})
const product = await updateProduct({where: {id}, data})
onSuccess(product)
} catch (error) {
alert("Error updating product " + JSON.stringify(error, null, 2))

View File

@@ -1,12 +1,11 @@
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, _ctx: Ctx) {
export default async function updateProduct({where, data}: UpdateProductInput) {
const product = await db.product.update({where, data})
return product

View File

@@ -10,7 +10,7 @@ type StaticProps = {
}
export const getStaticProps: GetStaticProps<StaticProps> = async (ctx) => {
const product = await getProduct({where: {handle: ctx.params!.handle as string}}, {} as any)
const product = await getProduct({where: {handle: ctx.params!.handle as string}})
const dataString = superjson.stringify(product)
return {
props: {dataString},

View File

@@ -1,5 +1,5 @@
import {useMemo} from "react"
import {invokeWithMiddleware, GetServerSideProps, Link, BlitzPage, PromiseReturnType} from "blitz"
import {ssrQuery, 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 invokeWithMiddleware(getProducts, {orderBy: {id: "desc"}}, {req, res})
const products = await ssrQuery(getProducts, {orderBy: {id: "desc"}}, {req, res})
const dataString = superjson.stringify(products)
return {
props: {

View File

@@ -1,4 +1,4 @@
import {NotFoundError, Ctx} from "blitz"
import {NotFoundError} 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, _ctx: Ctx) {
export default async function getProduct({where}: GetProductInput) {
const product = await db.product.findOne({where})
if (!product) throw new NotFoundError()

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/store",
"version": "0.24.0-canary.0",
"version": "0.23.1-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.24.0-canary.0",
"blitz": "0.23.1-canary.0",
"final-form": "4.19.1",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",

View File

@@ -1,6 +1,6 @@
{
"name": "tailwind",
"version": "0.24.0-canary.0",
"version": "0.23.1-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.24.0-canary.0",
"blitz": "0.23.1-canary.0",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",
"typescript": "3.8.3"

View File

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

View File

@@ -81,8 +81,8 @@
"@types/vinyl": "2.0.4",
"@types/vinyl-fs": "2.4.11",
"@types/webpack": "4.41.13",
"@typescript-eslint/eslint-plugin": "4.3.1-alpha.1",
"@typescript-eslint/parser": "4.3.1-alpha.1",
"@typescript-eslint/eslint-plugin": "2.x",
"@typescript-eslint/parser": "2.x",
"@wessberg/cjs-to-esm-transformer": "0.0.22",
"@wessberg/rollup-plugin-ts": "1.3.3",
"babel-eslint": "10.x",
@@ -133,9 +133,8 @@
"ts-jest": "24.3.0",
"tsdx": "0.13.3",
"tslib": "1.11.1",
"typescript": "4.0.3",
"wait-on": "4.0.2",
"zod": "1.11.9"
"typescript": "3.8.3",
"wait-on": "4.0.2"
},
"husky": {
"hooks": {

View File

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

View File

@@ -19,10 +19,9 @@ async function main() {
if (parseSemver(process.version).major < 12) {
console.log(
chalk.yellow(
`You are using an unsupported version of Node.js. Please switch to v12 or newer.\n`,
`You are using an unsupported version of Node.js. Consider switching to v12 or newer.\n`,
),
)
process.exit()
}
const globalBlitzPath = resolveFrom(__dirname, "blitz")

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/cli",
"description": "Blitz.js CLI",
"version": "0.24.0-canary.0",
"version": "0.23.1-canary.0",
"license": "MIT",
"scripts": {
"b": "./bin/run",
@@ -30,8 +30,8 @@
"/lib"
],
"dependencies": {
"@blitzjs/display": "0.24.0-canary.0",
"@blitzjs/repl": "0.24.0-canary.0",
"@blitzjs/display": "0.23.1-canary.0",
"@blitzjs/repl": "0.23.1-canary.0",
"@oclif/command": "1.5.20",
"@oclif/config": "1.15.1",
"@oclif/plugin-autocomplete": "0.2.0",
@@ -59,9 +59,9 @@
"v8-compile-cache": "2.1.1"
},
"devDependencies": {
"@blitzjs/generator": "0.24.0-canary.0",
"@blitzjs/installer": "0.24.0-canary.0",
"@blitzjs/server": "0.24.0-canary.0",
"@blitzjs/generator": "0.23.1-canary.0",
"@blitzjs/installer": "0.23.1-canary.0",
"@blitzjs/server": "0.23.1-canary.0",
"@oclif/dev-cli": "1.22.2",
"@oclif/test": "1.2.5",
"@prisma/cli": "2.4.1",

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/core",
"description": "Blitz.js core functionality",
"version": "0.24.0-canary.0",
"version": "0.23.1-canary.0",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
@@ -40,8 +40,8 @@
"url": "https://github.com/blitz-js/blitz"
},
"dependencies": {
"@blitzjs/config": "0.24.0-canary.0",
"@blitzjs/display": "0.24.0-canary.0",
"@blitzjs/config": "0.23.1-canary.0",
"@blitzjs/display": "0.23.1-canary.0",
"bad-behavior": "1.0.1",
"cookie-session": "1.4.0",
"deepmerge": "4.2.2",
@@ -51,7 +51,7 @@
"pretty-ms": "6.0.1",
"react-query": "2.23.0",
"serialize-error": "6.0.0",
"superjson": "1.2.3",
"superjson": "1.2.2",
"url": "0.11.0"
},
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"

View File

@@ -1,28 +0,0 @@
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)
}
}

View File

@@ -4,17 +4,10 @@ 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 Error {
export class CSRFTokenMismatchError extends AuthenticationError {
name = "CSRFTokenMismatchError"
statusCode = 401
get _clearStack() {
return true
}
}
export class AuthorizationError extends Error {
@@ -23,9 +16,6 @@ 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 {
@@ -34,7 +24,4 @@ export class NotFoundError extends Error {
constructor(message = "This could not be found") {
super(message)
}
get _clearStack() {
return true
}
}

View File

@@ -1,12 +1,11 @@
import {NextPage, NextComponentType, NextPageContext} from "next"
import {AppProps as NextAppProps} from "next/app"
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-query"
export * from "./use-paginated-query"
export * from "./use-params"
export * from "./use-infinite-query"
export * from "./ssr-query"
export * from "./rpc"
export * from "./with-router"
export * from "./use-router"

View File

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

View File

@@ -6,8 +6,6 @@ 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[] = [
@@ -23,7 +21,7 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url, {method: "POST"})
const res = await fetch(url)
expect(res.status).toBe(201)
expect(res.headers.get("test")).toBe("works")
})
@@ -42,7 +40,7 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url, {method: "POST"})
const res = await fetch(url)
expect(res.status).toBe(201)
expect(res.headers.get("test")).toBe("works")
})
@@ -61,14 +59,13 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url, {method: "POST"})
const res = await fetch(url)
expect(res.status).toBe(201)
expect(res.headers.get("test")).toBe("works")
})
})
// Failing on windows for unknown reason
testIfNotWindows("middleware can throw", async () => {
it("middleware can throw", async () => {
console.log = jest.fn()
console.error = jest.fn()
const forbiddenMiddleware = jest.fn()
@@ -80,14 +77,13 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url, {method: "POST"})
const res = await fetch(url)
expect(forbiddenMiddleware).not.toBeCalled()
expect(res.status).toBe(500)
})
})
// Failing on windows for unknown reason
testIfNotWindows("middleware can return error", async () => {
it("middleware can return error", async () => {
console.log = jest.fn()
const forbiddenMiddleware = jest.fn()
const middleware: Middleware[] = [
@@ -98,7 +94,7 @@ describe("handleRequestWithMiddleware", () => {
]
await mockServer(middleware, async (url) => {
const res = await fetch(url, {method: "POST"})
const res = await fetch(url)
expect(forbiddenMiddleware).not.toBeCalled()
expect(res.status).toBe(500)
})

View File

@@ -1,12 +1,10 @@
/* eslint-disable es5/no-for-of -- file only used on the server */
/* eslint-disable es5/no-es6-methods -- file only used on the server */
import {BlitzApiRequest, BlitzApiResponse} from "."
import {IncomingMessage, ServerResponse} from "http"
import {EnhancedResolverModule} from "./rpc"
import {getConfig} from "@blitzjs/config"
import {log, baseLogger} from "@blitzjs/display"
import {EnhancedResolver} from "./types"
export interface DefaultCtx {}
export interface Ctx extends DefaultCtx {}
import {log} from "@blitzjs/display"
export interface MiddlewareRequest extends BlitzApiRequest {
protocol?: string
@@ -39,9 +37,12 @@ export type ConnectMiddleware = (
next: (error?: Error) => void,
) => void
export function getAllMiddlewareForModule<TInput, TResult>(
resolverModule: EnhancedResolver<TInput, TResult>,
) {
export type ResolverModule = {
default: (args: any, ctx: any) => Promise<unknown>
middleware?: Middleware[]
}
export function getAllMiddlewareForModule(resolverModule: EnhancedResolverModule) {
const middleware: Middleware[] = []
const config = getConfig()
if (config.middleware) {
@@ -63,7 +64,6 @@ 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,22 +89,20 @@ 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
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",
)
log.error("Error while processing the request:\n")
log.error(error)
} else {
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")
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",
)
}
}
if (error._clearStack) {
delete error.stack
}
baseLogger.prettyError(error)
log.newline()
if (throwOnError) throw error
throw error
}
}

View File

@@ -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} as any)
await session.setPublicData({[INTERNAL_REDIRECT_URL_KEY]: req.query.redirectUrl})
return next()
})
}
@@ -113,9 +113,9 @@ export function passportAuth(config: BlitzPassportConfig) {
const redirectUrlFromVerifyResult =
result && typeof result === "object" && (result as any).redirectUrl
let redirectUrl: string =
let redirectUrl =
redirectUrlFromVerifyResult ||
(session.publicData as any)[INTERNAL_REDIRECT_URL_KEY] ||
session.publicData[INTERNAL_REDIRECT_URL_KEY] ||
(error ? config.errorRedirectUrl : config.successRedirectUrl) ||
"/"
@@ -129,9 +129,10 @@ export function passportAuth(config: BlitzPassportConfig) {
assert(isVerifyCallbackResult(result), "Passport verify callback is invalid")
delete (result.publicData as any)[INTERNAL_REDIRECT_URL_KEY]
await session.create(result.publicData, result.privateData)
await session.create(
{...result.publicData, [INTERNAL_REDIRECT_URL_KEY]: undefined},
result.privateData,
)
res.setHeader("Location", redirectUrl)
res.statusCode = 302

View File

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

View File

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

View File

@@ -1,41 +1,26 @@
import {deserializeError} from "serialize-error"
import {queryCache} from "react-query"
import {isClient, isServer, clientDebug} from "./utils"
import {getQueryKey} from "./utils"
import {ResolverModule, Middleware} from "./middleware"
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 {
ResolverType,
ResolverModule,
EnhancedResolver,
EnhancedResolverRpcClient,
CancellablePromise,
ResolverRpc,
RpcOptions,
} from "./types"
import {SuperJSONResult} from "superjson/dist/types"
import {getQueryKeyFromUrlAndParams} from "./utils/react-query-utils"
import merge from "deepmerge"
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)",
)
}
type Options = {
fromQueryHook?: boolean
resultOfGetFetchMore?: any
}
if (isServer) return (Promise.resolve() as unknown) as CancellablePromise<TResult>
clientDebug("Starting request for", apiUrl)
export function executeRpcCall(url: string, params: any, opts: Options = {}) {
if (typeof window === "undefined") return
const headers: Record<string, any> = {
"Content-Type": "application/json",
@@ -43,19 +28,20 @@ export const executeRpcCall = <TInput, TResult>(
const antiCSRFToken = getAntiCSRFToken()
if (antiCSRFToken) {
clientDebug("Adding antiCSRFToken cookie header", antiCSRFToken)
headers[HEADER_CSRF] = antiCSRFToken
} else {
clientDebug("No antiCSRFToken cookie found")
}
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
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))
}
} else {
serialized = serialize(params)
}
@@ -63,14 +49,15 @@ export const executeRpcCall = <TInput, TResult>(
// Create a new AbortController instance for this request
const controller = new AbortController()
const promise = window
.fetch(apiUrl, {
const promise: CancellablePromise<any> = window
.fetch(url, {
method: "POST",
headers,
credentials: "include",
redirect: "follow",
body: JSON.stringify({
params: serialized.json,
// TODO remove `|| null` once superjson allows `undefined`
params: serialized.json || null,
meta: {
params: serialized.meta,
},
@@ -78,20 +65,15 @@ export const executeRpcCall = <TInput, TResult>(
signal: controller.signal,
})
.then(async (result) => {
clientDebug("Received request for", apiUrl)
if (result.headers) {
if (result.headers.get(HEADER_PUBLIC_DATA_TOKEN)) {
publicDataStore.updateState()
clientDebug("Public data updated")
}
if (result.headers.get(HEADER_SESSION_REVOKED)) {
clientDebug("Sessin revoked")
publicDataStore.clear()
}
if (result.headers.get(HEADER_CSRF_ERROR)) {
const err = new CSRFTokenMismatchError()
delete err.stack
throw err
throw new CSRFTokenMismatchError()
}
}
@@ -99,25 +81,15 @@ export const executeRpcCall = <TInput, TResult>(
try {
payload = await result.json()
} catch (error) {
throw new Error(`Failed to parse json from request to ${apiUrl}`)
throw new Error(`Failed to parse json from request to ${url}`)
}
if (payload.error) {
let error = deserializeError(payload.error) as any
const error = deserializeError(payload.error)
// 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 =
@@ -126,95 +98,81 @@ export const executeRpcCall = <TInput, TResult>(
: deserialize({json: payload.result, meta: payload.meta?.result})
if (!opts.fromQueryHook) {
const queryKey = getQueryKeyFromUrlAndParams(apiUrl, params)
const queryKey = getQueryKey(url, params)
queryCache.setQueryData(queryKey, data)
}
return data as TResult
return data
}
}) as CancellablePromise<TResult>
})
// Disable react-query request cancellation for now
// Having too many weird bugs with it enabled
// promise.cancel = () => controller.abort()
promise.cancel = () => controller.abort()
return promise
}
executeRpcCall.warm = (apiUrl: string) => {
if (isClient) {
return window.fetch(apiUrl, {method: "HEAD"})
} else {
return
executeRpcCall.warm = (url: string) => {
if (typeof window !== "undefined") {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
window.fetch(url, {method: "HEAD"})
}
}
const getApiUrlFromResolverFilePath = (resolverFilePath: string) =>
resolverFilePath.replace(/^app\/_resolvers/, "/api")
interface ResolverEnhancement {
_meta: {
name: string
type: string
path: string
apiUrl: string
}
}
/*
* 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: 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)
interface CancellablePromise<T> extends Promise<T> {
cancel?: Function
}
if (target === "client") {
const resolverRpc: ResolverRpc<TInput, TResult> = (params, opts) =>
executeRpcCall(apiUrl, params, opts)
const enhancedResolverRpcClient = resolverRpc as EnhancedResolverRpcClient<TInput, TResult>
export interface RpcFunction {
(params: any, opts: any): CancellablePromise<any>
}
export interface EnhancedRpcFunction extends RpcFunction, ResolverEnhancement {}
enhancedResolverRpcClient._meta = {
export interface EnhancedResolverModule extends ResolverEnhancement {
(input: any, ctx: Record<string, any>): CancellablePromise<unknown>
middleware?: Middleware[]
}
export function getIsomorphicRpcHandler(
resolver: ResolverModule,
resolverPath: string,
resolverName: string,
resolverType: string,
) {
const apiUrl = resolverPath.replace(/^app\/_resolvers/, "/api")
const enhance = <T extends ResolverEnhancement>(fn: T): T => {
fn._meta = {
name: resolverName,
type: resolverType,
filePath: resolverFilePath,
path: resolverPath,
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 enhancedResolverRpcClient
return rpcFn
} else {
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
let handler: EnhancedResolverModule = resolver.default as any
handler.middleware = resolver.middleware
handler = enhance(handler)
return handler
}
}

View File

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

View File

@@ -0,0 +1,45 @@
import {IncomingMessage, ServerResponse} from "http"
import {log} from "@blitzjs/display"
import {InferUnaryParam} from "./types"
import {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
MiddlewareResponse,
} from "./middleware"
import {EnhancedResolverModule} from "./rpc"
type QueryFn = (...args: any) => Promise<any>
type SsrQueryContext = {
req: IncomingMessage
res: ServerResponse
}
export async function ssrQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T>,
{req, res}: SsrQueryContext,
): Promise<ReturnType<T>> {
const handler = (queryFn as unknown) as EnhancedResolverModule
const middleware = getAllMiddlewareForModule(handler)
middleware.push(async (_req, res, next) => {
const logPrefix = `${handler._meta.name}`
log.newline()
try {
log.progress(`Running ${logPrefix}(${JSON.stringify(params, null, 2)})`)
const result = await handler(params, res.blitzCtx)
log.success(`${logPrefix} returned ${log.variable(JSON.stringify(result, null, 2))}\n`)
res.blitzResult = result
return next()
} catch (error) {
log.error(`${logPrefix} failed: ${error}\n`)
throw error
}
})
await handleRequestWithMiddleware(req, res, middleware)
return (res as MiddlewareResponse).blitzResult as ReturnType<T>
}

View File

@@ -1,31 +0,0 @@
import {parsePublicDataToken, TOKEN_SEPARATOR} from "./supertokens"
describe("supertokens", () => {
describe("parsePublicDataToken", () => {
it("throws if token is empty", () => {
const ret = () => parsePublicDataToken("")
expect(ret).toThrow("[parsePublicDataToken] Failed: token is empty")
})
it("throws if the token cannot be parsed", () => {
const invalidJSON = "{"
const ret = () => parsePublicDataToken(btoa(invalidJSON))
expect(ret).toThrowError("[parsePublicDataToken] Failed to parse publicDataStr: {")
})
it("parses the public data", () => {
const validJSON = '"foo"'
expect(parsePublicDataToken(btoa(validJSON))).toEqual({
publicData: "foo",
})
})
it("only uses the first separated tokens", () => {
const data = `"foo"${TOKEN_SEPARATOR}123`
expect(parsePublicDataToken(btoa(data))).toEqual({
publicData: "foo",
})
})
})
})

View File

@@ -1,7 +1,7 @@
import {useState} from "react"
import {publicDataStore} from "./public-data-store"
import BadBehavior from "bad-behavior"
import {useIsomorphicLayoutEffect} from "./utils/hooks"
import {readCookie} from "./utils/cookie"
import {queryCache} from "react-query"
export const TOKEN_SEPARATOR = ";"
export const HANDLE_SEPARATOR = ":"
@@ -27,16 +27,14 @@ function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
}
export interface DefaultPublicData {
export interface PublicData extends Record<any, any> {
userId: any
roles: string[]
}
export interface PublicData extends DefaultPublicData {}
export interface SessionModel extends Record<any, any> {
handle: string
userId?: PublicData["userId"]
userId?: any
expiresAt?: Date
hashedSessionToken?: string
antiCSRFToken?: string
@@ -49,20 +47,23 @@ export type SessionConfig = {
method?: "essential" | "advanced"
sameSite?: "none" | "lax" | "strict"
getSession: (handle: string) => Promise<SessionModel | null>
getSessions: (userId: PublicData["userId"]) => Promise<SessionModel[]>
getSessions: (userId: any) => Promise<SessionModel[]>
createSession: (session: SessionModel) => Promise<SessionModel>
updateSession: (handle: string, session: Partial<SessionModel>) => Promise<SessionModel>
deleteSession: (handle: string) => Promise<SessionModel>
unstable_isAuthorized: (userRoles: string[], input?: any) => boolean
}
export interface SessionContextBase {
userId: unknown
export interface SessionContext {
/**
* null if anonymous
*/
userId: any
roles: string[]
handle: string | null
publicData: unknown
authorize(input?: any): asserts this is AuthenticatedSessionContext
isAuthorized(input?: any): boolean
publicData: PublicData
authorize: (input?: any) => void
isAuthorized: (input?: any) => boolean
// authorize: (roleOrRoles?: string | string[]) => void
// isAuthorized: (roleOrRoles?: string | string[]) => boolean
create: (publicData: PublicData, privateData?: Record<any, any>) => Promise<void>
@@ -70,38 +71,97 @@ export interface SessionContextBase {
revokeAll: () => Promise<void>
getPrivateData: () => Promise<Record<any, any>>
setPrivateData: (data: Record<any, any>) => Promise<void>
setPublicData: (data: Partial<Omit<PublicData, "userId">>) => Promise<void>
setPublicData: (data: Record<any, any>) => Promise<void>
}
// Could be anonymous
export interface SessionContext extends SessionContextBase {
userId: PublicData["userId"] | null
publicData: Partial<PublicData>
// Taken from https://github.com/HenrikJoreteg/cookie-getter
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
export function readCookie(name: string) {
if (typeof document === "undefined") return null
const cookie = document.cookie
const setPos = cookie.search(new RegExp("\\b" + name + "="))
const stopPos = cookie.indexOf(";", setPos)
let res
if (!~setPos) return null
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1])
return res.charAt(0) === "{" ? JSON.parse(res) : res
}
export interface AuthenticatedSessionContext extends SessionContextBase {
userId: PublicData["userId"]
publicData: PublicData
export const setCookie = (name: string, value: string, expires: string) => {
const result = `${name}=${value};path=/;expires=${expires}`
document.cookie = result
}
export const deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")
export const getAntiCSRFToken = () => readCookie(COOKIE_CSRF_TOKEN)
export const getPublicDataToken = () => readCookie(COOKIE_PUBLIC_DATA_TOKEN)
export const parsePublicDataToken = (token: string) => {
assert(token, "[parsePublicDataToken] Failed: token is empty")
assert(token, "[parsePublicDataToken] Failed - token is empty")
const [publicDataStr] = atob(token).split(TOKEN_SEPARATOR)
const [publicDataStr, expireAt] = atob(token).split(TOKEN_SEPARATOR)
let publicData: PublicData
try {
const publicData: PublicData = JSON.parse(publicDataStr)
return {
publicData,
}
publicData = JSON.parse(publicDataStr)
} catch (error) {
throw new Error(`[parsePublicDataToken] Failed to parse publicDataStr: ${publicDataStr}`)
throw new Error("Failed to parse publicDataToken: " + publicDataStr)
}
return {
publicData,
expireAt: expireAt && new Date(expireAt),
}
}
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(publicDataStore.emptyPublicData)
const [publicData, setPublicData] = useState(emptyPublicData)
const [isLoading, setIsLoading] = useState(true)
useIsomorphicLayoutEffect(() => {
@@ -112,7 +172,7 @@ export const useSession = () => {
return subscription.unsubscribe
}, [])
return {...publicData, isLoading} as PublicData & {isLoading: boolean}
return {...publicData, isLoading}
}
/*

View File

@@ -1,9 +1,7 @@
import {Middleware} from "./middleware"
/**
* Infer the type of the parameter from function that takes a single argument
*/
export type FirstParam<F extends QueryFn> = Parameters<F>[0]
export type InferUnaryParam<F extends Function> = F extends (args: infer A) => any ? A : never
/**
* Get the type of the value, that the Promise holds.
@@ -15,73 +13,4 @@ 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
}
}

View File

@@ -0,0 +1,55 @@
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>]
}

View File

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

View File

@@ -0,0 +1,54 @@
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>]
}

View File

@@ -1,7 +1,6 @@
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
@@ -49,43 +48,39 @@ export function useParams(returnType?: "string" | "number" | "array") {
const router = useRouter()
const query = useRouterQuery()
const params = useMemo(() => {
const rawParams = extractRouterParams(router.query, query)
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
}, [router.query, query, returnType])
return params
return rawParams
}
export function useParam(key: string): undefined | string | string[]

View File

@@ -1,145 +0,0 @@
import {
useQuery as useReactQuery,
QueryResult,
QueryConfig,
usePaginatedQuery as usePaginatedReactQuery,
PaginatedQueryResult,
PaginatedQueryConfig,
useInfiniteQuery as useInfiniteReactQuery,
InfiniteQueryResult,
InfiniteQueryConfig as RQInfiniteQueryConfig,
queryCache,
} from "react-query"
import {FirstParam, QueryFn, PromiseReturnType} from "./types"
import {
QueryCacheFunctions,
getQueryCacheFunctions,
getQueryKey,
sanitize,
defaultQueryConfig,
} from "./utils/react-query-utils"
import Router from "next/router"
Router.events.on("routeChangeComplete", async () => {
await queryCache.invalidateQueries()
})
// -------------------------
// useQuery
// -------------------------
type RestQueryResult<TResult> = Omit<QueryResult<TResult>, "data"> & QueryCacheFunctions<TResult>
export function useQuery<T extends QueryFn, TResult = PromiseReturnType<T>>(
queryFn: T,
params: FirstParam<T>,
options?: QueryConfig<TResult>,
): [TResult, RestQueryResult<TResult>] {
if (typeof queryFn === "undefined") {
throw new Error("useQuery is missing the first argument - it must be a query function")
}
const enhancedResolverRpcClient = sanitize(queryFn)
const queryKey = getQueryKey(queryFn, params)
const {data, ...queryRest} = useReactQuery({
queryKey,
queryFn: (_apiUrl: string, params: any) =>
enhancedResolverRpcClient(params, {fromQueryHook: true, alreadySerialized: true}),
config: {
...defaultQueryConfig,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<TResult>(queryKey),
}
return [data as TResult, rest as RestQueryResult<TResult>]
}
// -------------------------
// usePaginatedQuery
// -------------------------
type RestPaginatedResult<TResult> = Omit<PaginatedQueryResult<TResult>, "resolvedData"> &
QueryCacheFunctions<TResult>
export function usePaginatedQuery<T extends QueryFn, TResult = PromiseReturnType<T>>(
queryFn: T,
params: FirstParam<T>,
options?: PaginatedQueryConfig<TResult>,
): [TResult, RestPaginatedResult<TResult>] {
if (typeof queryFn === "undefined") {
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
}
const enhancedResolverRpcClient = sanitize(queryFn)
const queryKey = getQueryKey(queryFn, params)
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
queryKey,
queryFn: (_apiUrl: string, params: any) =>
enhancedResolverRpcClient(params, {fromQueryHook: true, alreadySerialized: true}),
config: {
...defaultQueryConfig,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<TResult>(queryKey),
}
return [resolvedData as TResult, rest as RestPaginatedResult<TResult>]
}
// -------------------------
// useInfiniteQuery
// -------------------------
type RestInfiniteResult<TResult> = Omit<InfiniteQueryResult<TResult>, "resolvedData"> &
QueryCacheFunctions<TResult>
interface InfiniteQueryConfig<TResult, TFetchMoreResult> extends RQInfiniteQueryConfig<TResult> {
getFetchMore?: (lastPage: TResult, allPages: TResult[]) => TFetchMoreResult
}
// TODO - Fix TFetchMoreResult not actually taking affect in apps.
// It shows as 'unknown' in the params() input argumunt, but should show as TFetchMoreResult
export function useInfiniteQuery<
T extends QueryFn,
TFetchMoreResult = any,
TResult = PromiseReturnType<T>
>(
queryFn: T,
params: (fetchMoreResult: TFetchMoreResult) => FirstParam<T>,
options: InfiniteQueryConfig<TResult, TFetchMoreResult>,
): [TResult[], RestInfiniteResult<TResult>] {
if (typeof queryFn === "undefined") {
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
}
const enhancedResolverRpcClient = sanitize(queryFn)
const queryKey = getQueryKey(queryFn)
const {data, ...queryRest} = useInfiniteReactQuery({
// we need an extra cache key for infinite loading so that the cache for
// for this query is stored separately since the hook result is an array of results.
// Without this cache for usePaginatedQuery and this will conflict and break.
queryKey: [...queryKey, "infinite"],
queryFn: (_apiUrl: string, _infinite: string, resultOfGetFetchMore: TFetchMoreResult) =>
enhancedResolverRpcClient(params(resultOfGetFetchMore), {fromQueryHook: true}),
config: {
...defaultQueryConfig,
...options,
},
})
const rest = {
...queryRest,
...getQueryCacheFunctions<TResult>(queryKey),
}
return [data as TResult[], rest as RestInfiniteResult<TResult>]
}

View File

@@ -0,0 +1,66 @@
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>]
}

View File

@@ -1,15 +1,8 @@
import {useMemo} from "react"
import {useRouter} from "next/router"
import {parse} from "url"
export function useRouterQuery() {
const router = useRouter()
const query = useMemo(() => {
const {query} = parse(router.asPath, true)
return query
}, [router.asPath])
const {query} = parse(router.asPath, true)
return query
}

View File

@@ -1,18 +0,0 @@
// Taken from https://github.com/HenrikJoreteg/cookie-getter
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
export function readCookie(name: string) {
if (typeof document === "undefined") return null
const cookie = document.cookie
const setPos = cookie.search(new RegExp("\\b" + name + "="))
const stopPos = cookie.indexOf(";", setPos)
let res
if (!~setPos) return null
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1])
return res.charAt(0) === "{" ? JSON.parse(res) : res
}
export const setCookie = (name: string, value: string, expires: string) => {
const result = `${name}=${value};path=/;expires=${expires}`
document.cookie = result
}
export const deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")

View File

@@ -1,8 +1,12 @@
import {QueryKey} from "react-query"
import {BlitzApiRequest} from "../"
import {IncomingMessage} from "http"
export const isServer = typeof window === "undefined"
export const isClient = typeof window !== "undefined"
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKey[]] {
return [cacheKey, typeof params === "function" ? (params as Function)() : params]
}
export function isLocalhost(req: BlitzApiRequest | IncomingMessage): boolean {
let {host} = req.headers
@@ -13,9 +17,3 @@ export function isLocalhost(req: BlitzApiRequest | IncomingMessage): boolean {
}
return localhost
}
export function clientDebug(...args: any) {
if (typeof window !== "undefined" && (window as any)["DEBUG_BLITZ"]) {
console.log("[BLITZ]", ...args)
}
}

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
// 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\\""`;

View File

@@ -1,21 +1,18 @@
import {getQueryCacheFunctions} from "../src/utils/react-query-utils"
import {getQueryCacheFunctions} from "../src/utils/query-cache"
import {queryCache} from "react-query"
jest.mock("react-query")
describe("getQueryCacheFunctions", () => {
it("returns a mutate function with working options", async () => {
window.requestIdleCallback = jest.fn((fn) => {
fn({} as any)
})
it("returns a mutate function with working options", () => {
const spyRefetchQueries = jest.spyOn(queryCache, "invalidateQueries")
const {mutate} = getQueryCacheFunctions("testQueryKey")
expect(mutate).toBeTruthy()
await mutate({newData: true})
mutate({newData: true})
expect(spyRefetchQueries).toBeCalledTimes(1)
await mutate({newData: true}, {refetch: false})
mutate({newData: true}, {refetch: false})
expect(spyRefetchQueries).toBeCalledTimes(1)
await mutate({newData: true}, {refetch: true})
mutate({newData: true}, {refetch: true})
expect(spyRefetchQueries).toBeCalledTimes(2)
})
})

View File

@@ -1,4 +1,6 @@
import {executeRpcCall, getIsomorphicEnhancedResolver} from "@blitzjs/core"
import {executeRpcCall, getIsomorphicRpcHandler} from "@blitzjs/core"
global.fetch = jest.fn(() => Promise.resolve({json: () => ({result: null, error: null})}))
declare global {
namespace NodeJS {
@@ -8,13 +10,11 @@ declare global {
}
}
global.fetch = jest.fn(() => Promise.resolve({json: () => ({result: null, error: null})}))
describe("RPC", () => {
describe("HEAD", () => {
it("warms the endpoint", async () => {
it("warms the endpoint", () => {
expect.assertions(1)
await executeRpcCall.warm("/api/endpoint")
executeRpcCall.warm("/api/endpoint")
expect(global.fetch).toBeCalled()
})
})
@@ -32,16 +32,15 @@ describe("RPC", () => {
const resolverModule = {
default: jest.fn(),
}
const rpcFn = getIsomorphicEnhancedResolver(
const rpcFn = getIsomorphicRpcHandler(
resolverModule,
"app/_resolvers/queries/getProduct",
"testResolver",
"query",
"client",
)
try {
const result = await rpcFn({paramOne: 1234}, {fromQueryHook: true})
const result = await rpcFn("/api/endpoint", {paramOne: 1234})
expect(result).toBe("result")
expect(fetchMock).toBeCalled()
} finally {
@@ -60,16 +59,15 @@ describe("RPC", () => {
const resolverModule = {
default: jest.fn(),
}
const rpcFn = getIsomorphicEnhancedResolver(
const rpcFn = getIsomorphicRpcHandler(
resolverModule,
"app/_resolvers/queries/getProduct",
"testResolver",
"query",
"client",
)
try {
await expect(rpcFn({paramOne: 1234}, {fromQueryHook: true})).rejects.toThrowError(
await expect(rpcFn("/api/endpoint", {paramOne: 1234})).rejects.toThrowError(
/something broke/,
)
} finally {

View File

@@ -1,31 +1,34 @@
import React from "react"
import {act, render, waitForElementToBeRemoved, screen} from "./test-utils"
import {useQuery} from "../src/use-query-hooks"
import {useQuery} from "../src/use-query"
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() {
const [data] = useQuery(queryFn, params)
useQuery(
enhance((num: number) => num),
1,
)
const [data] = useQuery(enhance(queryFn), params)
Object.assign(res, {data})
return <div id="harness">{data ? "Ready" : "Missing Dependency"}</div>
}
@@ -45,17 +48,13 @@ describe("useQuery", () => {
const upcase = async (args: string): Promise<string> => {
return args.toUpperCase()
}
it("should work with Blitz queries", async () => {
const [res] = setupHook("test", enhance(upcase))
it("should work", async () => {
const [res] = setupHook("test", 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()
})
})
})

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/display",
"version": "0.24.0-canary.0",
"version": "0.23.1-canary.0",
"description": "Display package for the Blitz CLI",
"homepage": "https://github.com/blitz-js/blitz#readme",
"license": "MIT",
@@ -31,7 +31,6 @@
},
"dependencies": {
"chalk": "4.0.0",
"ora": "4.0.4",
"tslog": "2.9.0"
"ora": "4.0.4"
}
}

View File

@@ -1,9 +1,6 @@
import c from "chalk"
import chalk from "chalk"
import ora from "ora"
import readline from "readline"
import {Logger} from "tslog"
export const chalk = c
// const blitzTrueBrandColor = '6700AB'
const blitzBrightBrandColor = "8a3df0"
@@ -12,27 +9,23 @@ const blitzBrightBrandColor = "8a3df0"
const brandColor = blitzBrightBrandColor
const withBrand = (str: string) => {
return c.hex(brandColor).bold(str)
return chalk.hex(brandColor).bold(str)
}
const withWarning = (str: string) => {
return `⚠️ ${c.yellow(str)}`
return `⚠️ ${chalk.yellow(str)}`
}
const withCaret = (str: string) => {
return `${c.gray(">")} ${str}`
return `${chalk.gray(">")} ${str}`
}
const withCheck = (str: string) => {
return `${c.green("✔")} ${str}`
return `${chalk.green("✔")} ${str}`
}
const withX = (str: string) => {
return `${c.red.bold("✕")} ${str}`
}
const withProgress = (str: string) => {
return withCaret(c.bold(str))
return `${chalk.red.bold("✕")} ${str}`
}
/**
@@ -41,7 +34,7 @@ const withProgress = (str: string) => {
* @param {string} msg
*/
const branded = (msg: string) => {
console.log(c.hex(brandColor).bold(msg))
console.log(chalk.hex(brandColor).bold(msg))
}
/**
@@ -70,7 +63,7 @@ const warning = (msg: string) => {
* @param {string} msg
*/
const error = (msg: string) => {
console.error(withX(c.red.bold(msg)))
console.error(withX(chalk.red.bold(msg)))
}
/**
@@ -79,7 +72,7 @@ const error = (msg: string) => {
* @param {string} msg
*/
const meta = (msg: string) => {
console.log(withCaret(c.gray(msg)))
console.log(withCaret(chalk.gray(msg)))
}
/**
@@ -88,11 +81,11 @@ const meta = (msg: string) => {
* @param {string} msg
*/
const progress = (msg: string) => {
console.log(withCaret(c.bold(msg)))
console.log(withCaret(chalk.bold(msg)))
}
const info = (msg: string) => {
console.log(c.bold(msg))
console.log(chalk.bold(msg))
}
const spinner = (str: string) => {
@@ -112,7 +105,7 @@ const spinner = (str: string) => {
* @param {string} msg
*/
const success = (msg: string) => {
console.log(withCheck(c.green(msg)))
console.log(withCheck(chalk.green(msg)))
}
const newline = () => {
@@ -125,7 +118,7 @@ const newline = () => {
* @param {string} val
*/
const variable = (val: any) => {
return c.cyan.bold(`${val}`)
return chalk.cyan.bold(`${val}`)
}
/**
@@ -142,7 +135,6 @@ export const log = {
withCaret,
withCheck,
withX,
withProgress,
branded,
clearLine,
error,
@@ -156,20 +148,3 @@ 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",
})

View File

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

View File

@@ -1,22 +1,23 @@
import * as fs from "fs-extra"
import {unlink as unlinkFile, pathExists} 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) {
return resolve(folder, relative(file.cwd, file.path))
const {history, cwd} = file
const [firstPath] = history
return resolve(folder, relative(cwd, firstPath))
}
/**
* Deletes a file in the stream from the filesystem
* @param folder The destination folder
*/
export function unlink(folder: string, unlinkFile = fs.unlink, pathExists = fs.pathExists) {
export function unlink(folder: string) {
return transform.file(async (file) => {
if (file.event === "unlink" || file.event === "unlinkDir") {
const destPath = getDestPath(folder, file)
if (await pathExists(destPath)) await unlinkFile(destPath)
if (await pathExists(getDestPath(folder, file))) await unlinkFile(getDestPath(folder, file))
}
return file

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/generator",
"version": "0.24.0-canary.0",
"version": "0.23.1-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.24.0-canary.0",
"@blitzjs/display": "0.23.1-canary.0",
"@types/jscodeshift": "0.7.1",
"chalk": "4.0.0",
"cross-spawn": "7.0.3",

View File

@@ -22,7 +22,6 @@ 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) {
@@ -46,33 +45,43 @@ 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":
type = "finalform"
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"),
)
pkg.dependencies["final-form"] = "4.x"
pkg.dependencies["react-final-form"] = "6.x"
break
case "React Hook Form":
type = "hookform"
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"),
)
pkg.dependencies["react-hook-form"] = "6.x"
break
case "Formik":
type = "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"),
)
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)
@@ -177,7 +186,6 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
if (code !== 0) spinners[spinners.length - 1].fail()
else {
spinners[spinners.length - 1].succeed()
this.packageInstallSuccess = true
}
}
resolve()
@@ -193,18 +201,16 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
}
// Ensure the generated files are formatted with the installed prettier version
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()
}
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

View File

@@ -47,11 +47,8 @@ export class PageGenerator extends Generator<PageGeneratorOptions> {
}
getModelNamesPath() {
const kebabCaseContext = this.options.context
? `${camelCaseToKebabCase(this.options.context)}/`
: ""
const kebabCaseModelNames = camelCaseToKebabCase(this.options.modelNames)
return kebabCaseContext + kebabCaseModelNames
const context = this.options.context ? `${this.options.context}/` : ""
return context + this.options.modelNames
}
getTargetDirectory() {

View File

@@ -1,5 +1,5 @@
import React from "react"
import { Link, useMutation } from "blitz"
import { Link } from "blitz"
import { LabeledTextField } from "app/components/LabeledTextField"
import { Form, FORM_ERROR } from "app/components/Form"
import login from "app/auth/mutations/login"
@@ -10,8 +10,6 @@ type LoginFormProps = {
}
export const LoginForm = (props: LoginFormProps) => {
const [loginMutation] = useMutation(login)
return (
<div>
<h1>Login</h1>
@@ -22,7 +20,7 @@ export const LoginForm = (props: LoginFormProps) => {
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
try {
await loginMutation(values)
await login({ email: values.email, password: values.password })
props.onSuccess?.()
} catch (error) {
if (error.name === "AuthenticationError") {

View File

@@ -1,5 +1,4 @@
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"
@@ -10,8 +9,6 @@ type SignupFormProps = {
}
export const SignupForm = (props: SignupFormProps) => {
const [signupMutation] = useMutation(signup)
return (
<div>
<h1>Create an Account</h1>
@@ -22,7 +19,7 @@ export const SignupForm = (props: SignupFormProps) => {
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
try {
await signupMutation(values)
await signup({ email: values.email, password: values.password })
props.onSuccess?.()
} catch (error) {
if (error.code === "P2002" && error.meta?.target?.includes("email")) {

View File

@@ -1,15 +1,15 @@
import { protect } from "blitz"
import { SessionContext } from "blitz"
import { authenticateUser } from "app/auth/auth-utils"
import { LoginInput } from "../validations"
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)
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 session.create({ userId: user.id, roles: [user.role] })
await ctx.session!.create({ userId: user.id, roles: [user.role] })
return user
})
}

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