Compare commits
53 Commits
new-resolv
...
use-author
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0816950fb6 | ||
|
|
57c70795e8 | ||
|
|
12785484c0 | ||
|
|
2b65ce2206 | ||
|
|
ad58be3f52 | ||
|
|
37348f2595 | ||
|
|
bec3cd6cde | ||
|
|
a9c1171a14 | ||
|
|
b46a245f08 | ||
|
|
a5208e2b96 | ||
|
|
4fefbcbbb0 | ||
|
|
258c0491dd | ||
|
|
f9dca5c60a | ||
|
|
8ec0d929d8 | ||
|
|
adfc529852 | ||
|
|
aebc79fe9c | ||
|
|
da6393c538 | ||
|
|
e51a002892 | ||
|
|
d73750be0c | ||
|
|
95781eb6ba | ||
|
|
afcd47569c | ||
|
|
f405b5b4df | ||
|
|
a0d7378642 | ||
|
|
60bba38919 | ||
|
|
901d1cad7e | ||
|
|
383034d1fe | ||
|
|
bd6f37a6b0 | ||
|
|
b1116b6052 | ||
|
|
85c91e2e2e | ||
|
|
0bad1f181b | ||
|
|
971e695b30 | ||
|
|
c0e1246dc0 | ||
|
|
bce9822d13 | ||
|
|
e41219944c | ||
|
|
fe0d958368 | ||
|
|
06248f6005 | ||
|
|
24b85b0108 | ||
|
|
7804d3ea77 | ||
|
|
6dca78a8ed | ||
|
|
92679cfa03 | ||
|
|
46cb60a962 | ||
|
|
012d146fd9 | ||
|
|
5015693ddd | ||
|
|
e2ab39ed75 | ||
|
|
64ced80f77 | ||
|
|
050c4f7127 | ||
|
|
86de3303bf | ||
|
|
31724c7b2a | ||
|
|
d7647ad2be | ||
|
|
0a3836b30b | ||
|
|
1fccb1dc19 | ||
|
|
7195aaea66 | ||
|
|
d1c4553ddd |
@@ -1882,6 +1882,99 @@
|
||||
"design",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "queq1890",
|
||||
"name": "Yuji Matsumoto",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32263803?v=4",
|
||||
"profile": "http://queq1890.info",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Gim3l",
|
||||
"name": "Gimel Dick",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/46765702?v=4",
|
||||
"profile": "https://github.com/Gim3l",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "akbo",
|
||||
"name": "Andreas Bollig",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1926271?v=4",
|
||||
"profile": "https://github.com/akbo",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ajmarkow",
|
||||
"name": "AJ Markow",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/66390428?v=4",
|
||||
"profile": "https://ajm.codes",
|
||||
"contributions": [
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wafuwafu13",
|
||||
"name": "TagawaHirotaka",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/50798936?v=4",
|
||||
"profile": "https://wafuwafu13.hateblo.jp/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "merodiro",
|
||||
"name": "Amr A.Mohammed",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17033502?v=4",
|
||||
"profile": "https://github.com/merodiro",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lcswillems",
|
||||
"name": "Lucas Willems",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5437552?v=4",
|
||||
"profile": "http://www.lucaswillems.com",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "alii",
|
||||
"name": "Alistair Smith",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/25351731?v=4",
|
||||
"profile": "https://alistair.cloud",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rodrigoehlers",
|
||||
"name": "Rodrigo Ehlers",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/19683042?v=4",
|
||||
"profile": "https://rodrigoehlers.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mtford90",
|
||||
"name": "Michael Ford",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1734057?v=4",
|
||||
"profile": "https://www.builtopen.com/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.log
|
||||
.DS_Store
|
||||
.idea
|
||||
.jest-*
|
||||
lib
|
||||
node_modules
|
||||
|
||||
22
README.md
22
README.md
@@ -6,7 +6,7 @@
|
||||
<img alt="" src="https://img.shields.io/badge/Join%20our%20community-6700EB.svg?style=for-the-badge&labelColor=000000&logoWidth=20&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ9SURBVHgB7d3dVdtAEIbhcSpICUoH0IEogQqSVBBSAU4FSSpIOoAORAfQgSghHXzZ1U/YcMD4R9rZmf2ec3y448LyiNf27iLiGIAmPLrweC9Un3DhrzG6EarLNP09nlwJ1SOZ/lQr5N80/S/p2QMVCBf5N17XCfm1Y/rBHqjAG9PPHvBsz+mf9WAP+HLA9M/YA14cOP2payH7jpj+VCtk1wnTP+vj7xCy6cTpn7EHLMLp059iD1iD8eveJbVCNsSLheX1YA/YgOWnf8YeKB3Wmf7Ud6Fy4f/FHmtpxbl3YlC4MJ/Cj0bWdwPnPbARg+L0S54XQHS32WwuxClzd4CM0z9rPfeAuTtA5ulPXYQ7wZ04Y+oOoDD9KZc9YOoOoDj9s4dwFzgXR6w1wIPoOvPWA9buAHEJ173o3gWiy3AnuBUHLEbgmYwvAk1/wuM8vAgexThzbwPDkx7/DHwVXfFOxP2GmsKd4Ab6zPeAyU8CI7AHFmH2BRCBPXAyk18GzUrqAXCTiR4ssyj0VFw/oCU8+e+RZ33AWz6KMaYbIIWxB+JSLs1bsbkeMN0AqakHvoku9oA2sAfqBvbAQdw0QArsgb25aYBUQT3QgT2gB+yBuqGcHij2UCqXDZACe2Anlw2QYg/QAOyBuoE98CL3DZDCuK4/rh/Q7oGL6U+TOvcNkJoijN8X1C48+T+g75eQDrAH/qmqAVJgDwyqaoAUe4AGYA/UDZX3QLUNkEIZPRCd5+6BahsgVUgPROwBTSijB7jpVAvGHriHvmw9wAZ4BpX1ABvgmakHtPcbRuwBTWAPULgAV9D/jKDY9YRvwvgEaurD44uQHvAol7qBW7WKluVtIHiUS7GyvA0s6CiXDnxrpQfsgbqBS7GKk/2jYHCrVlGyfxTMrVo0ALdq1Q3sgSKofh0M9oA61a+D2QM0AHugbmAPqClmSRjK2apVVQ8UsySsoK1aHdgDesCtWnUDeyCrIpeFg1u3sylyWTi3btMA7IG6gT2wuuK3hoE9sKrit4YVslWLPaAN7IG6ocKt2zmY2h4O9sDiTG0PZw/QANy6XTewBxZj9ogYVHy025LMHhEz9cBn0We6B0yfERReBLfhx0/R1YQHPx/QBPbA0VwcEwf2wNFcHBPHHjiem3MC2QPHcXdSaJjA+KfgTPQ8hhfjBzHC40mhlzJ+Xq9lK4a4PCs43AVaGTed5mZq+iOXZwWHi3AnOj2wFWNcnxYe7gTxLtBKHuamP/J+Wnh8a5irB7ZC5Yk9gPX1QuXC+usHWqGyhYvUYR0a7zboUOFCNVhnk0krZAOW7wFOvzXhom2xnEbIHizTA1wEYhWW6YFGyC6c1gOcfg9wfA80Qj7g8B7g9HuCww+haIR8wf49wOn3Cvv9k8tGyC/s7gFOv3fY3QONkH+v9MBWqB7PeqDn9FcIT//kcitUn6kHOu/T/xfWzlQy3dEHhwAAAABJRU5ErkJggg==">
|
||||
</a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-199-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-209-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">
|
||||
@@ -48,7 +48,7 @@ _You can alternatively use [`npx`](https://www.npmjs.com/package/npx)_
|
||||
|
||||
1. `blitz new myAppName`
|
||||
2. `cd myAppName`
|
||||
3. `blitz start`
|
||||
3. `blitz dev`
|
||||
4. View your baby app at http://localhost:3000
|
||||
|
||||
<br><br>
|
||||
@@ -149,11 +149,9 @@ Your financial contributions help ensure Blitz continues to be developed and mai
|
||||
|
||||
### 🏆 Gold Sponsors
|
||||
|
||||
<div>
|
||||
<a aria-label="G2i" href="http://g2i.co/sign-up?utm_source=blitz&utm_medium=referral&utm_campaign=blitz2020">
|
||||
<img alt="" src="https://files-5oz00y7xp.vercel.app/G2i_Logo_wwords.png" width="160px">
|
||||
<a aria-label="Your Company" href="#">
|
||||
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="300px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### 💎 Diamond Sponsors
|
||||
|
||||
@@ -489,6 +487,18 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
|
||||
<td align="center"><a href="https://github.com/jonasthiesen"><img src="https://avatars.githubusercontent.com/u/23408018?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonas Thiesen</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jonasthiesen" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://thakkaryash94.github.io/"><img src="https://avatars.githubusercontent.com/u/7349778?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yash Thakkar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=thakkaryash94" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/rince"><img src="https://avatars.githubusercontent.com/u/933895?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kazuma Suzuki</b></sub></a><br /><a href="#design-rince" title="Design">🎨</a> <a href="https://github.com/blitz-js/blitz/commits?author=rince" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://queq1890.info"><img src="https://avatars.githubusercontent.com/u/32263803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yuji Matsumoto</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=queq1890" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Gim3l"><img src="https://avatars.githubusercontent.com/u/46765702?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gimel Dick</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Gim3l" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/akbo"><img src="https://avatars.githubusercontent.com/u/1926271?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andreas Bollig</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=akbo" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://ajm.codes"><img src="https://avatars.githubusercontent.com/u/66390428?v=4?s=100" width="100px;" alt=""/><br /><sub><b>AJ Markow</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ajmarkow" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=ajmarkow" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://wafuwafu13.hateblo.jp/"><img src="https://avatars.githubusercontent.com/u/50798936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TagawaHirotaka</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=wafuwafu13" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=wafuwafu13" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/merodiro"><img src="https://avatars.githubusercontent.com/u/17033502?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Amr A.Mohammed</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=merodiro" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.lucaswillems.com"><img src="https://avatars.githubusercontent.com/u/5437552?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Lucas Willems</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=lcswillems" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=lcswillems" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://alistair.cloud"><img src="https://avatars.githubusercontent.com/u/25351731?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alistair Smith</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=alii" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://rodrigoehlers.com"><img src="https://avatars.githubusercontent.com/u/19683042?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rodrigo Ehlers</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=rodrigoehlers" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.builtopen.com/"><img src="https://avatars.githubusercontent.com/u/1734057?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Ford</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mtford90" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ blitz prisma migrate dev --preview-feature
|
||||
3. Start the dev server
|
||||
|
||||
```
|
||||
blitz start
|
||||
blitz dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
@@ -3,6 +3,7 @@ import db from "db"
|
||||
import {Strategy as TwitterStrategy} from "passport-twitter"
|
||||
import {Strategy as GitHubStrategy} from "passport-github2"
|
||||
import {Strategy as Auth0Strategy} from "passport-auth0"
|
||||
import {Role} from "types"
|
||||
|
||||
function assert(condition: any, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message)
|
||||
@@ -91,7 +92,7 @@ export default passportAuth({
|
||||
|
||||
const publicData = {
|
||||
userId: user.id,
|
||||
roles: [user.role],
|
||||
roles: [user.role as Role],
|
||||
source: "github",
|
||||
githubUsername: profile.username,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {Link, useMutation, AuthenticationError} from "blitz"
|
||||
import {LabeledTextField} from "app/components/LabeledTextField"
|
||||
import {Form, FORM_ERROR} from "app/components/Form"
|
||||
import login, {LoginInput} from "app/auth/mutations/login"
|
||||
import {AuthenticationError, Link, useMutation} from "blitz"
|
||||
import {LabeledTextField} from "app/core/components/LabeledTextField"
|
||||
import {Form, FORM_ERROR} from "app/core/components/Form"
|
||||
import login from "app/auth/mutations/login"
|
||||
import {Login} from "app/auth/validations"
|
||||
|
||||
type LoginFormProps = {
|
||||
onSuccess?: () => void
|
||||
@@ -9,17 +10,19 @@ type LoginFormProps = {
|
||||
|
||||
export const LoginForm = (props: LoginFormProps) => {
|
||||
const [loginMutation] = useMutation(login)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
|
||||
<Form
|
||||
submitText="Login"
|
||||
schema={LoginInput}
|
||||
initialValues={{email: undefined, password: undefined}}
|
||||
schema={Login}
|
||||
initialValues={{email: "", password: ""}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await loginMutation(values)
|
||||
props.onSuccess && props.onSuccess()
|
||||
props.onSuccess?.()
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return {[FORM_ERROR]: "Sorry, those credentials are invalid"}
|
||||
@@ -34,7 +37,13 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
>
|
||||
<LabeledTextField name="email" label="Email" placeholder="Email" />
|
||||
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
|
||||
<div>
|
||||
<Link href="/forgot-password">
|
||||
<a>Forgot your password?</a>
|
||||
</Link>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div style={{marginTop: "1rem"}}>
|
||||
Or <Link href="/signup">Sign Up</Link>
|
||||
</div>
|
||||
|
||||
44
examples/auth/app/auth/components/SignupForm.tsx
Normal file
44
examples/auth/app/auth/components/SignupForm.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react"
|
||||
import {useMutation} from "blitz"
|
||||
import {LabeledTextField} from "app/core/components/LabeledTextField"
|
||||
import {Form, FORM_ERROR} from "app/core/components/Form"
|
||||
import signup from "app/auth/mutations/signup"
|
||||
import {Signup} from "app/auth/validations"
|
||||
|
||||
type SignupFormProps = {
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const SignupForm = (props: SignupFormProps) => {
|
||||
const [signupMutation] = useMutation(signup)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Create an Account</h1>
|
||||
|
||||
<Form
|
||||
submitText="Create Account"
|
||||
schema={Signup}
|
||||
initialValues={{email: "", password: ""}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await signupMutation(values)
|
||||
props.onSuccess?.()
|
||||
} catch (error) {
|
||||
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
|
||||
// This error comes from Prisma
|
||||
return {email: "This email is already being used"}
|
||||
} else {
|
||||
return {[FORM_ERROR]: error.toString()}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LabeledTextField name="email" label="Email" placeholder="Email" />
|
||||
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupForm
|
||||
23
examples/auth/app/auth/mutations/changePassword.ts
Normal file
23
examples/auth/app/auth/mutations/changePassword.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {NotFoundError, SecurePassword, resolver} from "blitz"
|
||||
import db from "db"
|
||||
import {authenticateUser} from "./login"
|
||||
import {ChangePassword} from "../validations"
|
||||
|
||||
export default resolver.pipe(
|
||||
resolver.zod(ChangePassword),
|
||||
resolver.authorize(),
|
||||
async ({currentPassword, newPassword}, ctx) => {
|
||||
const user = await db.user.findFirst({where: {id: ctx.session.userId!}})
|
||||
if (!user) throw new NotFoundError()
|
||||
|
||||
await authenticateUser(user.email, currentPassword)
|
||||
|
||||
const hashedPassword = await SecurePassword.hash(newPassword)
|
||||
await db.user.update({
|
||||
where: {id: user.id},
|
||||
data: {hashedPassword},
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
56
examples/auth/app/auth/mutations/forgotPassword.test.ts
Normal file
56
examples/auth/app/auth/mutations/forgotPassword.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {hash256, Ctx} from "blitz"
|
||||
import forgotPassword from "./forgotPassword"
|
||||
import db from "db"
|
||||
import previewEmail from "preview-email"
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.$reset()
|
||||
})
|
||||
|
||||
const generatedToken = "plain-token"
|
||||
jest.mock("blitz", () => ({
|
||||
...jest.requireActual("blitz")!,
|
||||
generateToken: () => generatedToken,
|
||||
}))
|
||||
jest.mock("preview-email", () => jest.fn())
|
||||
|
||||
describe("forgotPassword mutation", () => {
|
||||
it("does not throw error if user doesn't exist", async () => {
|
||||
await expect(forgotPassword({email: "no-user@email.com"}, {} as Ctx)).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it("works correctly", async () => {
|
||||
// Create test user
|
||||
const user = await db.user.create({
|
||||
data: {
|
||||
email: "user@example.com",
|
||||
tokens: {
|
||||
// Create old token to ensure it's deleted
|
||||
create: {
|
||||
type: "RESET_PASSWORD",
|
||||
hashedToken: "token",
|
||||
expiresAt: new Date(),
|
||||
sentTo: "user@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {tokens: true},
|
||||
})
|
||||
|
||||
// Invoke the mutation
|
||||
await forgotPassword({email: user.email}, {} as Ctx)
|
||||
|
||||
const tokens = await db.token.findMany({where: {userId: user.id}})
|
||||
const token = tokens[0]
|
||||
|
||||
// delete's existing tokens
|
||||
expect(tokens.length).toBe(1)
|
||||
|
||||
expect(token.id).not.toBe(user.tokens[0].id)
|
||||
expect(token.type).toBe("RESET_PASSWORD")
|
||||
expect(token.sentTo).toBe(user.email)
|
||||
expect(token.hashedToken).toBe(hash256(generatedToken))
|
||||
expect(token.expiresAt > new Date()).toBe(true)
|
||||
expect(previewEmail).toBeCalled()
|
||||
})
|
||||
})
|
||||
41
examples/auth/app/auth/mutations/forgotPassword.ts
Normal file
41
examples/auth/app/auth/mutations/forgotPassword.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {resolver, generateToken, hash256} from "blitz"
|
||||
import db from "db"
|
||||
import {forgotPasswordMailer} from "mailers/forgotPasswordMailer"
|
||||
import {ForgotPassword} from "../validations"
|
||||
|
||||
const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4
|
||||
|
||||
export default resolver.pipe(resolver.zod(ForgotPassword), async ({email}) => {
|
||||
// 1. Get the user
|
||||
const user = await db.user.findFirst({where: {email: email.toLowerCase()}})
|
||||
|
||||
// 2. Generate the token and expiration date.
|
||||
const token = generateToken()
|
||||
const hashedToken = hash256(token)
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS)
|
||||
|
||||
// 3. If user with this email was found
|
||||
if (user) {
|
||||
// 4. Delete any existing password reset tokens
|
||||
await db.token.deleteMany({where: {type: "RESET_PASSWORD", userId: user.id}})
|
||||
// 5. Save this new token in the database.
|
||||
await db.token.create({
|
||||
data: {
|
||||
user: {connect: {id: user.id}},
|
||||
type: "RESET_PASSWORD",
|
||||
expiresAt,
|
||||
hashedToken,
|
||||
sentTo: user.email,
|
||||
},
|
||||
})
|
||||
// 6. Send the email
|
||||
await forgotPasswordMailer({to: user.email, token}).send()
|
||||
} else {
|
||||
// 7. If no user found wait the same time so attackers can't tell the difference
|
||||
await new Promise((resolve) => setTimeout(resolve, 750))
|
||||
}
|
||||
|
||||
// 8. Return the same result whether a password reset email was sent or not
|
||||
return
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import {resolver, SecurePassword, AuthenticationError} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
import {Login} from "../validations"
|
||||
import {Role} from "types"
|
||||
|
||||
export const authenticateUser = async (email: string, password: string) => {
|
||||
const user = await db.user.findFirst({where: {email}})
|
||||
@@ -18,16 +19,11 @@ export const authenticateUser = async (email: string, password: string) => {
|
||||
return rest
|
||||
}
|
||||
|
||||
export const LoginInput = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
})
|
||||
|
||||
export default resolver.pipe(resolver.zod(LoginInput), async ({email, password}, {session}) => {
|
||||
export default resolver.pipe(resolver.zod(Login), async ({email, password}, ctx) => {
|
||||
// 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 as Role]})
|
||||
|
||||
return user
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Ctx} from "blitz"
|
||||
|
||||
export default async function logout(_: any, {session}: Ctx) {
|
||||
return await session.$revoke()
|
||||
export default async function logout(_: any, ctx: Ctx) {
|
||||
return await ctx.session.$revoke()
|
||||
}
|
||||
|
||||
82
examples/auth/app/auth/mutations/resetPassword.test.ts
Normal file
82
examples/auth/app/auth/mutations/resetPassword.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import resetPassword from "./resetPassword"
|
||||
import db from "db"
|
||||
import {hash256, SecurePassword} from "blitz"
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.$reset()
|
||||
})
|
||||
|
||||
const mockCtx: any = {
|
||||
session: {
|
||||
$create: jest.fn,
|
||||
},
|
||||
}
|
||||
|
||||
describe("resetPassword mutation", () => {
|
||||
it("works correctly", async () => {
|
||||
expect(true).toBe(true)
|
||||
|
||||
// Create test user
|
||||
const goodToken = "randomPasswordResetToken"
|
||||
const expiredToken = "expiredRandomPasswordResetToken"
|
||||
const future = new Date()
|
||||
future.setHours(future.getHours() + 4)
|
||||
const past = new Date()
|
||||
past.setHours(past.getHours() - 4)
|
||||
|
||||
const user = await db.user.create({
|
||||
data: {
|
||||
email: "user@example.com",
|
||||
tokens: {
|
||||
// Create old token to ensure it's deleted
|
||||
create: [
|
||||
{
|
||||
type: "RESET_PASSWORD",
|
||||
hashedToken: hash256(expiredToken),
|
||||
expiresAt: past,
|
||||
sentTo: "user@example.com",
|
||||
},
|
||||
{
|
||||
type: "RESET_PASSWORD",
|
||||
hashedToken: hash256(goodToken),
|
||||
expiresAt: future,
|
||||
sentTo: "user@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {tokens: true},
|
||||
})
|
||||
|
||||
const newPassword = "newPassword"
|
||||
|
||||
// Non-existent token
|
||||
await expect(
|
||||
resetPassword({token: "no-token", password: "", passwordConfirmation: ""}, mockCtx),
|
||||
).rejects.toThrowError()
|
||||
|
||||
// Expired token
|
||||
await expect(
|
||||
resetPassword(
|
||||
{token: expiredToken, password: newPassword, passwordConfirmation: newPassword},
|
||||
mockCtx,
|
||||
),
|
||||
).rejects.toThrowError()
|
||||
|
||||
// Good token
|
||||
await resetPassword(
|
||||
{token: goodToken, password: newPassword, passwordConfirmation: newPassword},
|
||||
mockCtx,
|
||||
)
|
||||
|
||||
// Delete's the token
|
||||
const numberOfTokens = await db.token.count({where: {userId: user.id}})
|
||||
expect(numberOfTokens).toBe(0)
|
||||
|
||||
// Updates user's password
|
||||
const updatedUser = await db.user.findFirst({where: {id: user.id}})
|
||||
expect(await SecurePassword.verify(updatedUser!.hashedPassword, newPassword)).toBe(
|
||||
SecurePassword.VALID,
|
||||
)
|
||||
})
|
||||
})
|
||||
47
examples/auth/app/auth/mutations/resetPassword.ts
Normal file
47
examples/auth/app/auth/mutations/resetPassword.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {resolver, SecurePassword, hash256} from "blitz"
|
||||
import db from "db"
|
||||
import {ResetPassword} from "../validations"
|
||||
import login from "./login"
|
||||
|
||||
export class ResetPasswordError extends Error {
|
||||
name = "ResetPasswordError"
|
||||
message = "Reset password link is invalid or it has expired."
|
||||
}
|
||||
|
||||
export default resolver.pipe(resolver.zod(ResetPassword), async ({password, token}, ctx) => {
|
||||
// 1. Try to find this token in the database
|
||||
const hashedToken = hash256(token)
|
||||
const possibleToken = await db.token.findFirst({
|
||||
where: {hashedToken, type: "RESET_PASSWORD"},
|
||||
include: {user: true},
|
||||
})
|
||||
|
||||
// 2. If token not found, error
|
||||
if (!possibleToken) {
|
||||
throw new ResetPasswordError()
|
||||
}
|
||||
const savedToken = possibleToken
|
||||
|
||||
// 3. Delete token so it can't be used again
|
||||
await db.token.delete({where: {id: savedToken.id}})
|
||||
|
||||
// 4. If token has expired, error
|
||||
if (savedToken.expiresAt < new Date()) {
|
||||
throw new ResetPasswordError()
|
||||
}
|
||||
|
||||
// 5. Since token is valid, now we can update the user's password
|
||||
const hashedPassword = await SecurePassword.hash(password)
|
||||
const user = await db.user.update({
|
||||
where: {id: savedToken.userId},
|
||||
data: {hashedPassword},
|
||||
})
|
||||
|
||||
// 6. Revoke all existing login sessions for this user
|
||||
await db.session.deleteMany({where: {userId: user.id}})
|
||||
|
||||
// 7. Now log the user in with the new credentials
|
||||
await login({email: user.email, password}, ctx)
|
||||
|
||||
return true
|
||||
})
|
||||
@@ -1,20 +1,15 @@
|
||||
import {resolver, SecurePassword} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
import {Signup} from "app/auth/validations"
|
||||
import {Role} from "types"
|
||||
|
||||
export const SignupInput = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(10).max(100),
|
||||
})
|
||||
|
||||
export default resolver.pipe(resolver.zod(SignupInput), async ({email, password}, {session}) => {
|
||||
export default resolver.pipe(resolver.zod(Signup), async ({email, password}, ctx) => {
|
||||
const hashedPassword = await SecurePassword.hash(password)
|
||||
const user = await db.user.create({
|
||||
data: {email, hashedPassword, role: "user"},
|
||||
data: {email: email.toLowerCase(), hashedPassword, role: "user"},
|
||||
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 as Role]})
|
||||
return user
|
||||
})
|
||||
|
||||
47
examples/auth/app/auth/pages/forgot-password.tsx
Normal file
47
examples/auth/app/auth/pages/forgot-password.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import {BlitzPage, useMutation} from "blitz"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import {LabeledTextField} from "app/core/components/LabeledTextField"
|
||||
import {Form, FORM_ERROR} from "app/core/components/Form"
|
||||
import {ForgotPassword} from "app/auth/validations"
|
||||
import forgotPassword from "app/auth/mutations/forgotPassword"
|
||||
|
||||
const ForgotPasswordPage: BlitzPage = () => {
|
||||
const [forgotPasswordMutation, {isSuccess}] = useMutation(forgotPassword)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Forgot your password?</h1>
|
||||
|
||||
{isSuccess ? (
|
||||
<div>
|
||||
<h2>Request Submitted</h2>
|
||||
<p>
|
||||
If your email is in our system, you will receive instructions to reset your password
|
||||
shortly.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Form
|
||||
submitText="Send Reset Password Instructions"
|
||||
schema={ForgotPassword}
|
||||
initialValues={{email: ""}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await forgotPasswordMutation(values)
|
||||
} catch (error) {
|
||||
return {
|
||||
[FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.",
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LabeledTextField name="email" label="Email" placeholder="Email" />
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ForgotPasswordPage.getLayout = (page) => <Layout title="Forgot Your Password?">{page}</Layout>
|
||||
|
||||
export default ForgotPasswordPage
|
||||
@@ -1,21 +1,19 @@
|
||||
import {Head, useRouter, BlitzPage} from "blitz"
|
||||
import React from "react"
|
||||
import {useRouter, BlitzPage, useRedirectAuthenticatedUser} from "blitz"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import {LoginForm} from "app/auth/components/LoginForm"
|
||||
|
||||
const SignupPage: BlitzPage = () => {
|
||||
const LoginPage: BlitzPage = () => {
|
||||
useRedirectAuthenticatedUser("/projects")
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Login</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<div>
|
||||
<LoginForm onSuccess={() => router.push("/")} />
|
||||
</div>
|
||||
</>
|
||||
<div>
|
||||
<LoginForm onSuccess={() => router.push("/")} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupPage
|
||||
LoginPage.getLayout = (page) => <Layout title="Log In">{page}</Layout>
|
||||
|
||||
export default LoginPage
|
||||
|
||||
58
examples/auth/app/auth/pages/reset-password.tsx
Normal file
58
examples/auth/app/auth/pages/reset-password.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import {BlitzPage, useRouterQuery, Link, useMutation} from "blitz"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import {LabeledTextField} from "app/core/components/LabeledTextField"
|
||||
import {Form, FORM_ERROR} from "app/core/components/Form"
|
||||
import {ResetPassword} from "app/auth/validations"
|
||||
import resetPassword from "app/auth/mutations/resetPassword"
|
||||
|
||||
const ResetPasswordPage: BlitzPage = () => {
|
||||
const query = useRouterQuery()
|
||||
const [resetPasswordMutation, {isSuccess}] = useMutation(resetPassword)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Set a New Password</h1>
|
||||
|
||||
{isSuccess ? (
|
||||
<div>
|
||||
<h2>Password Reset Successfully</h2>
|
||||
<p>
|
||||
Go to the <Link href="/">homepage</Link>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Form
|
||||
submitText="Reset Password"
|
||||
schema={ResetPassword}
|
||||
initialValues={{password: "", passwordConfirmation: "", token: query.token as string}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await resetPasswordMutation(values)
|
||||
} catch (error) {
|
||||
if (error.name === "ResetPasswordError") {
|
||||
return {
|
||||
[FORM_ERROR]: error.message,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
[FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.",
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LabeledTextField name="password" label="New Password" type="password" />
|
||||
<LabeledTextField
|
||||
name="passwordConfirmation"
|
||||
label="Confirm New Password"
|
||||
type="password"
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ResetPasswordPage.getLayout = (page) => <Layout title="Reset Your Password">{page}</Layout>
|
||||
|
||||
export default ResetPasswordPage
|
||||
@@ -1,53 +1,19 @@
|
||||
import {Head, useRouter, BlitzPage, useMutation} from "blitz"
|
||||
import {Form, FORM_ERROR} from "app/components/Form"
|
||||
import {LabeledTextField} from "app/components/LabeledTextField"
|
||||
import signup, {SignupInput} from "app/auth/mutations/signup"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import {Form, FORM_ERROR} from "app/core/components/Form"
|
||||
import {LabeledTextField} from "app/core/components/LabeledTextField"
|
||||
import {SignupForm} from "app/auth/components/SignupForm"
|
||||
|
||||
const SignupPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
const [signupMutation] = useMutation(signup)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sign Up</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<div>
|
||||
<h1>Create an Account</h1>
|
||||
|
||||
<Form
|
||||
submitText="Create Account"
|
||||
schema={SignupInput}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await signupMutation(values)
|
||||
router.push("/")
|
||||
} catch (error) {
|
||||
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
|
||||
// This error comes from Prisma
|
||||
return {email: "This email is already being used"}
|
||||
} else {
|
||||
return {
|
||||
[FORM_ERROR]:
|
||||
"Sorry, we had an unexpected error. Please try again. - " + error.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LabeledTextField name="email" label="Email" placeholder="Email" />
|
||||
<LabeledTextField
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
<div>
|
||||
<SignupForm onSuccess={() => router.push("/")} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SignupPage.getLayout = (page) => <Layout title="Sign Up">{page}</Layout>
|
||||
|
||||
export default SignupPage
|
||||
|
||||
33
examples/auth/app/auth/validations.ts
Normal file
33
examples/auth/app/auth/validations.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as z from "zod"
|
||||
|
||||
const password = z.string().min(10).max(100)
|
||||
|
||||
export const Signup = z.object({
|
||||
email: z.string().email(),
|
||||
password,
|
||||
})
|
||||
|
||||
export const Login = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
})
|
||||
|
||||
export const ForgotPassword = z.object({
|
||||
email: z.string().email(),
|
||||
})
|
||||
|
||||
export const ResetPassword = z
|
||||
.object({
|
||||
password: password,
|
||||
passwordConfirmation: password,
|
||||
token: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.passwordConfirmation, {
|
||||
message: "Passwords don't match",
|
||||
path: ["passwordConfirmation"], // set the path of the error
|
||||
})
|
||||
|
||||
export const ChangePassword = z.object({
|
||||
currentPassword: z.string(),
|
||||
newPassword: password,
|
||||
})
|
||||
0
examples/auth/app/core/components/.keep
Normal file
0
examples/auth/app/core/components/.keep
Normal file
@@ -1,17 +1,18 @@
|
||||
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"
|
||||
|
||||
type FormProps<S extends z.ZodType<any, any>> = {
|
||||
export interface FormProps<S extends z.ZodType<any, any>>
|
||||
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
|
||||
/** All your form fields */
|
||||
children: ReactNode
|
||||
children?: ReactNode
|
||||
/** Text to display in the submit button */
|
||||
submitText?: string
|
||||
schema?: S
|
||||
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
|
||||
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
|
||||
schema?: S
|
||||
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
|
||||
}
|
||||
|
||||
export function Form<S extends z.ZodType<any, any>>({
|
||||
children,
|
||||
@@ -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,12 +11,16 @@ 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,
|
||||
meta: {touched, error, submitError, submitting},
|
||||
} = useField(name)
|
||||
} = useField(name, {
|
||||
parse: props.type === "number" ? Number : undefined,
|
||||
})
|
||||
|
||||
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
|
||||
|
||||
return (
|
||||
<div {...outerProps}>
|
||||
@@ -25,9 +29,9 @@ export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldPro
|
||||
<input {...input} disabled={submitting} {...props} ref={ref} />
|
||||
</label>
|
||||
|
||||
{touched && (error || submitError) && (
|
||||
{touched && normalizedError && (
|
||||
<div role="alert" style={{color: "red"}}>
|
||||
{error || submitError}
|
||||
{normalizedError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
13
examples/auth/app/mutations/makeCoffee.ts
Normal file
13
examples/auth/app/mutations/makeCoffee.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {resolver} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
const __Name__ = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
})
|
||||
.nonstrict()
|
||||
|
||||
export default resolver.pipe(resolver.zod(__Name__), resolver.authorize(), async (input) => {
|
||||
// Do your stuff :)
|
||||
})
|
||||
@@ -10,12 +10,10 @@ import {ErrorBoundary} from "react-error-boundary"
|
||||
import {queryCache} from "react-query"
|
||||
import LoginForm from "app/auth/components/LoginForm"
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
;(window as any)["DEBUG_BLITZ"] = 1
|
||||
}
|
||||
|
||||
export default function App({Component, pageProps}: AppProps) {
|
||||
const getLayout = Component.getLayout || ((page) => page)
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={RootErrorFallback}
|
||||
@@ -26,7 +24,7 @@ export default function App({Component, pageProps}: AppProps) {
|
||||
queryCache.resetErrorBoundaries()
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import {render} from "test/utils"
|
||||
|
||||
import Home from "./index"
|
||||
import {useCurrentUser} from "app/hooks/useCurrentUser"
|
||||
import {useCurrentUser} from "app/core/hooks/useCurrentUser"
|
||||
|
||||
jest.mock("app/hooks/useCurrentUser")
|
||||
jest.mock("app/core/hooks/useCurrentUser")
|
||||
const mockUseCurrentUser = useCurrentUser as jest.MockedFunction<typeof useCurrentUser>
|
||||
|
||||
test("renders blitz documentation link", () => {
|
||||
// This is an example of how to ensure a specific item is in the document
|
||||
// But it's disabled by default (by test.skip) so the test doesn't fail
|
||||
// when you remove the the default content from the page
|
||||
|
||||
// This is an example on how to mock api hooks when testing
|
||||
mockUseCurrentUser.mockReturnValue({
|
||||
id: 1,
|
||||
name: "User",
|
||||
@@ -16,5 +21,6 @@ test("renders blitz documentation link", () => {
|
||||
|
||||
const {getByText} = render(<Home />)
|
||||
const element = getByText(/powered by blitz/i)
|
||||
// @ts-ignore
|
||||
expect(element).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Head, Link, useSession, useRouterQuery, useMutation, invoke} from "blitz
|
||||
import getUser from "app/users/queries/getUser"
|
||||
import trackView from "app/users/mutations/trackView"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import {useCurrentUser} from "app/hooks/useCurrentUser"
|
||||
import {useCurrentUser} from "app/core/hooks/useCurrentUser"
|
||||
// import getUsers from "app/users/queries/getUsers"
|
||||
|
||||
const CurrentUserInfo = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Suspense} from "react"
|
||||
import {Link, useRouter, useQuery, useParam, BlitzPage, useMutation} from "blitz"
|
||||
import {Head, Link, useRouter, useQuery, useParam, BlitzPage, useMutation} from "blitz"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import getProject from "app/projects/queries/getProject"
|
||||
import deleteProject from "app/projects/mutations/deleteProject"
|
||||
@@ -7,30 +7,37 @@ import deleteProject from "app/projects/mutations/deleteProject"
|
||||
export const Project = () => {
|
||||
const router = useRouter()
|
||||
const projectId = useParam("projectId", "number")
|
||||
const [project] = useQuery(getProject, {where: {id: projectId}})
|
||||
const [deleteProjectMutation] = useMutation(deleteProject)
|
||||
const [deleteProjectMutation, {isSuccess}] = useMutation(deleteProject)
|
||||
const [project] = useQuery(getProject, {id: projectId}, {enabled: !isSuccess})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Project {project.id}</h1>
|
||||
<pre>{JSON.stringify(project, null, 2)}</pre>
|
||||
<>
|
||||
<Head>
|
||||
<title>Project {project.id}</title>
|
||||
</Head>
|
||||
|
||||
<Link href={`/projects/${project.id}/edit`}>
|
||||
<a>Edit</a>
|
||||
</Link>
|
||||
<div>
|
||||
<h1>Project {project.id}</h1>
|
||||
<pre>{JSON.stringify(project, null, 2)}</pre>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (window.confirm("This will be deleted")) {
|
||||
await deleteProjectMutation({where: {id: project.id}})
|
||||
router.push("/projects")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<Link href={`/projects/${project.id}/edit`}>
|
||||
<a>Edit</a>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (window.confirm("This will be deleted")) {
|
||||
await deleteProjectMutation({id: project.id})
|
||||
router.push("/projects")
|
||||
}
|
||||
}}
|
||||
style={{marginLeft: "0.5rem"}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,6 +57,6 @@ const ShowProjectPage: BlitzPage = () => {
|
||||
)
|
||||
}
|
||||
|
||||
ShowProjectPage.getLayout = (page) => <Layout title={"Project"}>{page}</Layout>
|
||||
ShowProjectPage.getLayout = (page) => <Layout>{page}</Layout>
|
||||
|
||||
export default ShowProjectPage
|
||||
|
||||
@@ -1,39 +1,51 @@
|
||||
import {Suspense} from "react"
|
||||
import {Link, useRouter, useQuery, useMutation, useParam, BlitzPage} from "blitz"
|
||||
import {Head, Link, useRouter, useQuery, useMutation, useParam, BlitzPage} from "blitz"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import getProject from "app/projects/queries/getProject"
|
||||
import updateProject from "app/projects/mutations/updateProject"
|
||||
import ProjectForm from "app/projects/components/ProjectForm"
|
||||
import {ProjectForm, FORM_ERROR} from "app/projects/components/ProjectForm"
|
||||
|
||||
export const EditProject = () => {
|
||||
const router = useRouter()
|
||||
const projectId = useParam("projectId", "number")
|
||||
const [project, {setQueryData}] = useQuery(getProject, {where: {id: projectId}})
|
||||
const [project, {setQueryData}] = useQuery(getProject, {id: projectId})
|
||||
const [updateProjectMutation] = useMutation(updateProject)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Edit Project {project.id}</h1>
|
||||
<pre>{JSON.stringify(project)}</pre>
|
||||
<>
|
||||
<Head>
|
||||
<title>Edit Project {project.id}</title>
|
||||
</Head>
|
||||
|
||||
<ProjectForm
|
||||
initialValues={project}
|
||||
onSubmit={async () => {
|
||||
try {
|
||||
const updated = await updateProjectMutation({
|
||||
where: {id: project.id},
|
||||
data: {name: "MyNewName"},
|
||||
})
|
||||
await setQueryData(updated)
|
||||
alert("Success!" + JSON.stringify(updated))
|
||||
router.push(`/projects/${updated.id}`)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
alert("Error editing project " + JSON.stringify(error, null, 2))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Edit Project {project.id}</h1>
|
||||
<pre>{JSON.stringify(project)}</pre>
|
||||
|
||||
<ProjectForm
|
||||
submitText="Update Project"
|
||||
// TODO use a zod schema for form validation
|
||||
// - Tip: extract mutation's schema into a shared `validations.ts` file and
|
||||
// then import and use it here
|
||||
// schema={UpdateProject}
|
||||
initialValues={project}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
const updated = await updateProjectMutation({
|
||||
id: project.id,
|
||||
...values,
|
||||
})
|
||||
await setQueryData(updated)
|
||||
router.push(`/projects/${updated.id}`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return {
|
||||
[FORM_ERROR]: error.toString(),
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,6 +65,6 @@ const EditProjectPage: BlitzPage = () => {
|
||||
)
|
||||
}
|
||||
|
||||
EditProjectPage.getLayout = (page) => <Layout title={"Edit Project"}>{page}</Layout>
|
||||
EditProjectPage.getLayout = (page) => <Layout>{page}</Layout>
|
||||
|
||||
export default EditProjectPage
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Suspense} from "react"
|
||||
import {Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
|
||||
import {Head, Link, usePaginatedQuery, useRouter, BlitzPage, useAuthorize} from "blitz"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import getProjects from "app/projects/queries/getProjects"
|
||||
|
||||
@@ -40,21 +40,29 @@ export const ProjectsList = () => {
|
||||
}
|
||||
|
||||
const ProjectsPage: BlitzPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<Link href="/projects/new">
|
||||
<a>Create Project</a>
|
||||
</Link>
|
||||
</p>
|
||||
useAuthorize()
|
||||
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ProjectsList />
|
||||
</Suspense>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Projects</title>
|
||||
</Head>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<Link href="/projects/new">
|
||||
<a>Create Project</a>
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ProjectsList />
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectsPage.getLayout = (page) => <Layout title={"Projects"}>{page}</Layout>
|
||||
ProjectsPage.getLayout = (page) => <Layout>{page}</Layout>
|
||||
|
||||
export default ProjectsPage
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Link, useRouter, useMutation, BlitzPage} from "blitz"
|
||||
import Layout from "app/core/layouts/Layout"
|
||||
import createProject from "app/projects/mutations/createProject"
|
||||
import ProjectForm from "app/projects/components/ProjectForm"
|
||||
import {ProjectForm, FORM_ERROR} from "app/projects/components/ProjectForm"
|
||||
|
||||
const NewProjectPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
@@ -12,14 +12,21 @@ const NewProjectPage: BlitzPage = () => {
|
||||
<h1>Create New Project</h1>
|
||||
|
||||
<ProjectForm
|
||||
initialValues={{}}
|
||||
onSubmit={async () => {
|
||||
submitText="Create Project"
|
||||
// TODO use a zod schema for form validation
|
||||
// - Tip: extract mutation's schema into a shared `validations.ts` file and
|
||||
// then import and use it here
|
||||
// schema={CreateProject}
|
||||
// initialValues={{}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
const project = await createProjectMutation({name: "MyName"})
|
||||
alert("Success!" + JSON.stringify(project))
|
||||
const project = await createProjectMutation(values)
|
||||
router.push(`/projects/${project.id}`)
|
||||
} catch (error) {
|
||||
alert("Error creating project " + JSON.stringify(error, null, 2))
|
||||
console.error(error)
|
||||
return {
|
||||
[FORM_ERROR]: error.toString(),
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -27,7 +27,7 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, re
|
||||
// https://github.com/blitz-js/blitz/issues/794
|
||||
path.resolve("next.config.js")
|
||||
path.resolve("blitz.config.js")
|
||||
path.resolve(".next/__db.js")
|
||||
path.resolve(".next/blitz/db.js")
|
||||
// End anti-tree-shaking
|
||||
|
||||
const session = await getSessionContext(req, res)
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import React from "react"
|
||||
import {Form, FormProps} from "app/core/components/Form"
|
||||
import {LabeledTextField} from "app/core/components/LabeledTextField"
|
||||
import * as z from "zod"
|
||||
export {FORM_ERROR} from "app/core/components/Form"
|
||||
|
||||
type ProjectFormProps = {
|
||||
initialValues: any
|
||||
onSubmit: React.FormEventHandler<HTMLFormElement>
|
||||
}
|
||||
|
||||
const ProjectForm = ({initialValues, onSubmit}: ProjectFormProps) => {
|
||||
export function ProjectForm<S extends z.ZodType<any, any>>(props: FormProps<S>) {
|
||||
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>
|
||||
<Form<S> {...props}>
|
||||
<LabeledTextField name="name" label="Name" placeholder="Name" />
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectForm
|
||||
|
||||
@@ -2,24 +2,15 @@ import {resolver} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
export const CreateProject = z.object({
|
||||
name: z.string(),
|
||||
dueDate: z.date().optional(),
|
||||
const CreateProject = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
})
|
||||
.nonstrict()
|
||||
|
||||
export default resolver.pipe(resolver.zod(CreateProject), resolver.authorize(), async (input) => {
|
||||
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
|
||||
const project = await db.project.create({data: input})
|
||||
|
||||
return project
|
||||
})
|
||||
|
||||
export default resolver.pipe(
|
||||
resolver.zod(CreateProject),
|
||||
(input, _ctx) => ({extraFieldForIntegrationTesting: _ctx.session.userId, ...input}),
|
||||
resolver.authorize(),
|
||||
// How to set a default input value
|
||||
(input, _ctx) => ({dueDate: new Date(), ...input}),
|
||||
async (input, _ctx) => {
|
||||
console.log("Creating project...")
|
||||
const project = await db.project.create({
|
||||
data: input,
|
||||
})
|
||||
console.log("Created project")
|
||||
|
||||
return project
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import {Ctx} from "blitz"
|
||||
import db, {Prisma} from "db"
|
||||
import {resolver} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
type DeleteProjectInput = Pick<Prisma.ProjectDeleteArgs, "where">
|
||||
const DeleteProject = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
})
|
||||
.nonstrict()
|
||||
|
||||
export default async function deleteProject({where}: DeleteProjectInput, ctx: Ctx) {
|
||||
ctx.session.$authorize()
|
||||
|
||||
const project = await db.project.delete({where})
|
||||
export default resolver.pipe(resolver.zod(DeleteProject), resolver.authorize(), async ({id}) => {
|
||||
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
|
||||
const project = await db.project.delete({where: {id}})
|
||||
|
||||
return project
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import {Ctx} from "blitz"
|
||||
import db, {Prisma} from "db"
|
||||
import {resolver} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
type UpdateProjectInput = Pick<Prisma.ProjectUpdateArgs, "where" | "data">
|
||||
const UpdateProject = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
})
|
||||
.nonstrict()
|
||||
|
||||
export default async function updateProject({where, data}: UpdateProjectInput, ctx: Ctx) {
|
||||
ctx.session.$authorize()
|
||||
export default resolver.pipe(
|
||||
resolver.zod(UpdateProject),
|
||||
resolver.authorize(),
|
||||
async ({id, ...data}) => {
|
||||
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
|
||||
const project = await db.project.update({where: {id}, data})
|
||||
|
||||
const project = await db.project.update({where, data})
|
||||
|
||||
return project
|
||||
}
|
||||
return project
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import {Ctx, NotFoundError} from "blitz"
|
||||
import db, {Prisma} from "db"
|
||||
import {resolver, NotFoundError} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
type GetProjectInput = Pick<Prisma.ProjectFindFirstArgs, "where">
|
||||
const GetProject = z.object({
|
||||
// This accepts type of undefined, but is required at runtime
|
||||
id: z.number().optional().refine(Boolean, "Required"),
|
||||
})
|
||||
|
||||
export default async function getProject({where}: GetProjectInput, ctx: Ctx) {
|
||||
ctx.session.$authorize()
|
||||
|
||||
const project = await db.project.findFirst({where})
|
||||
export default resolver.pipe(resolver.zod(GetProject), resolver.authorize(), async ({id}) => {
|
||||
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
|
||||
const project = await db.project.findFirst({where: {id}})
|
||||
|
||||
if (!project) throw new NotFoundError()
|
||||
|
||||
return project
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import {Ctx} from "blitz"
|
||||
import {paginate, resolver} from "blitz"
|
||||
import db, {Prisma} from "db"
|
||||
|
||||
type GetProjectsInput = Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take">
|
||||
interface GetProjectsInput
|
||||
extends Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take"> {}
|
||||
|
||||
export default async function getProjects(
|
||||
{where, orderBy, skip = 0, take}: GetProjectsInput,
|
||||
ctx: Ctx,
|
||||
) {
|
||||
ctx.session.$authorize()
|
||||
export default resolver.pipe(
|
||||
resolver.authorize(),
|
||||
async ({where, orderBy, skip = 0, take = 100}: GetProjectsInput) => {
|
||||
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
|
||||
const {items: projects, hasMore, nextPage, count} = await paginate({
|
||||
skip,
|
||||
take,
|
||||
count: () => db.project.count({where}),
|
||||
query: (paginateArgs) => db.project.findMany({...paginateArgs, where, orderBy}),
|
||||
})
|
||||
|
||||
const projects = await db.project.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
take,
|
||||
skip,
|
||||
})
|
||||
|
||||
const count = await db.project.count()
|
||||
const hasMore = typeof take === "number" ? skip + take < count : false
|
||||
const nextPage = hasMore ? {take, skip: skip + take!} : null
|
||||
|
||||
return {
|
||||
projects,
|
||||
nextPage,
|
||||
hasMore,
|
||||
count,
|
||||
}
|
||||
}
|
||||
return {
|
||||
projects,
|
||||
nextPage,
|
||||
hasMore,
|
||||
count,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Ctx} from "blitz"
|
||||
import db from "db"
|
||||
|
||||
export default async function getCurrentUser(_ = null, ctx: Ctx) {
|
||||
if (!ctx.session.userId) return null
|
||||
export default async function getCurrentUser(_ = null, {session}: Ctx) {
|
||||
if (!session.userId) return null
|
||||
|
||||
const user = await db.user.findFirst({
|
||||
where: {id: ctx.session.userId},
|
||||
where: {id: session.userId},
|
||||
select: {id: true, name: true, email: true, role: true},
|
||||
})
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ module.exports = withBundleAnalyzer({
|
||||
middleware: [
|
||||
sessionMiddleware({
|
||||
isAuthorized: simpleRolesIsAuthorized,
|
||||
sessionExpiryMinutes: 4,
|
||||
// sessionExpiryMinutes: 4,
|
||||
}),
|
||||
],
|
||||
log: {
|
||||
// level: "trace",
|
||||
},
|
||||
experimental: {
|
||||
isomorphicResolverImports: true,
|
||||
isomorphicResolverImports: false,
|
||||
},
|
||||
/*
|
||||
webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ describe("index page", () => {
|
||||
|
||||
cy.signup(user)
|
||||
|
||||
cy.wait(500)
|
||||
cy.contains("button", "Logout").click()
|
||||
cy.contains("a", /login/i).click()
|
||||
|
||||
@@ -37,6 +38,7 @@ describe("index page", () => {
|
||||
cy.contains("button", /login/i).click()
|
||||
|
||||
cy.location("pathname").should("equal", "/")
|
||||
cy.wait(500)
|
||||
cy.contains("button", "Logout")
|
||||
})
|
||||
|
||||
|
||||
7
examples/auth/cypress/tsconfig.json
Normal file
7
examples/auth/cypress/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "es5"],
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
||||
0
examples/auth/db/migrations/.keep
Normal file
0
examples/auth/db/migrations/.keep
Normal file
@@ -0,0 +1,15 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Token" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"hashedToken" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"sentTo" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Token.hashedToken_type_unique" ON "Token"("hashedToken", "type");
|
||||
@@ -20,18 +20,20 @@ generator client {
|
||||
// --------------------------------------
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
name String?
|
||||
email String @unique
|
||||
email String @unique
|
||||
hashedPassword String?
|
||||
role String @default("user")
|
||||
sessions Session[]
|
||||
role String @default("user")
|
||||
|
||||
sessions Session[]
|
||||
tokens Token[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
expiresAt DateTime?
|
||||
@@ -44,10 +46,25 @@ model Session {
|
||||
privateData String?
|
||||
}
|
||||
|
||||
model Token {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
hashedToken String
|
||||
type String
|
||||
expiresAt DateTime
|
||||
sentTo String
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
|
||||
@@unique([hashedToken, type])
|
||||
}
|
||||
|
||||
model Project {
|
||||
id Int @default(autoincrement()) @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
name String
|
||||
dueDate DateTime?
|
||||
}
|
||||
|
||||
0
examples/auth/mailers/.keep
Normal file
0
examples/auth/mailers/.keep
Normal file
45
examples/auth/mailers/forgotPasswordMailer.ts
Normal file
45
examples/auth/mailers/forgotPasswordMailer.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/* TODO - You need to add a mailer integration in `integrations/` and import here.
|
||||
*
|
||||
* The integration file can be very simple. Instantiate the email client
|
||||
* and then export it. That way you can import here and anywhere else
|
||||
* and use it straight away.
|
||||
*/
|
||||
import previewEmail from "preview-email"
|
||||
|
||||
type ResetPasswordMailer = {
|
||||
to: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export function forgotPasswordMailer({to, token}: ResetPasswordMailer) {
|
||||
// In production, set APP_ORIGIN to your production server origin
|
||||
const origin = process.env.APP_ORIGIN || process.env.BLITZ_DEV_SERVER_ORIGIN
|
||||
const resetUrl = `${origin}/reset-password?token=${token}`
|
||||
|
||||
const msg = {
|
||||
from: "TODO@example.com",
|
||||
to,
|
||||
subject: "Your Password Reset Instructions",
|
||||
html: `
|
||||
<h1>Reset Your Password</h1>
|
||||
<h3>NOTE: You must set up a production email integration in mailers/forgotPasswordMailer.ts</h3>
|
||||
|
||||
<a href="${resetUrl}">
|
||||
Click here to set a new password
|
||||
</a>
|
||||
`,
|
||||
}
|
||||
|
||||
return {
|
||||
async send() {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// TODO - send the production email, like this:
|
||||
// await postmark.sendEmail(msg)
|
||||
throw new Error("No production email implementation in mailers/forgotPasswordMailer")
|
||||
} else {
|
||||
// Preview email in the browser
|
||||
await previewEmail(msg)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "@examples/auth",
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"scripts": {
|
||||
"dev": "blitz dev",
|
||||
"build": "blitz build",
|
||||
"start": "blitz start",
|
||||
"studio": "blitz prisma studio",
|
||||
"build": "blitz build",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
"analyze": "cross-env ANALYZE=true blitz build",
|
||||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run || cypress run",
|
||||
"test": "prisma generate && yarn test:jest && yarn test:e2e",
|
||||
"test:jest": "jest",
|
||||
"test:server": "blitz prisma migrate deploy --preview-feature && blitz build && blitz start --production -p 3099",
|
||||
"test:server": "blitz prisma migrate deploy --preview-feature && blitz build && blitz start -p 3099",
|
||||
"test:e2e": "cross-env NODE_ENV=test start-server-and-test test:server http://localhost:3099 cy:run"
|
||||
},
|
||||
"browserslist": [
|
||||
@@ -38,13 +39,13 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.15.0",
|
||||
"@prisma/client": "2.15.0",
|
||||
"blitz": "0.30.0-canary.4",
|
||||
"@prisma/client": "2.16.0",
|
||||
"blitz": "0.30.0-canary.6",
|
||||
"final-form": "4.20.1",
|
||||
"passport-auth0": "1.4.0",
|
||||
"passport-github2": "0.1.12",
|
||||
"passport-twitter": "1.0.4",
|
||||
"prisma": "2.16.0",
|
||||
"react": "0.0.0-experimental-3310209d0",
|
||||
"react-dom": "0.0.0-experimental-3310209d0",
|
||||
"react-error-boundary": "3.1.0",
|
||||
@@ -53,20 +54,22 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/skip-test": "2.6.0",
|
||||
"@next/bundle-analyzer": "^10.0.5",
|
||||
"@next/bundle-analyzer": "^10.0.6",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@testing-library/react-hooks": "4.0.1",
|
||||
"@types/passport-auth0": "1.0.4",
|
||||
"@types/passport-github2": "1.2.4",
|
||||
"@types/passport-twitter": "1.0.36",
|
||||
"@types/preview-email": "2.0.0",
|
||||
"@types/react": "17.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "6.2.1",
|
||||
"eslint": "7.17.0",
|
||||
"husky": "4.3.7",
|
||||
"eslint": "7.18.0",
|
||||
"husky": "4.3.8",
|
||||
"lint-staged": "10.5.3",
|
||||
"prettier": "2.2.1",
|
||||
"pretty-quick": "3.1.0",
|
||||
"preview-email": "3.0.3",
|
||||
"start-server-and-test": "1.11.7",
|
||||
"typescript": "4.1.3"
|
||||
},
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
// This is the jest 'setupFilesAfterEnv' setup file
|
||||
// It's a good place to set globals, add global before/after hooks, etc
|
||||
|
||||
export {} // so TS doesn't complain
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react"
|
||||
import {RouterContext, BlitzRouter} from "blitz"
|
||||
import {render as defaultRender} from "@testing-library/react"
|
||||
import {renderHook as defaultRenderHook} from "@testing-library/react-hooks"
|
||||
@@ -13,14 +14,6 @@ export * from "@testing-library/react"
|
||||
// This is the place to add any other context providers you need while testing.
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
type DefaultParams = Parameters<typeof defaultRender>
|
||||
type RenderUI = DefaultParams[0]
|
||||
type RenderOptions = DefaultParams[1] & {router?: Partial<BlitzRouter>}
|
||||
|
||||
type DefaultHookParams = Parameters<typeof defaultRenderHook>
|
||||
type RenderHook = DefaultHookParams[0]
|
||||
type RenderHookOptions = DefaultHookParams[1] & {router?: Partial<BlitzRouter>}
|
||||
|
||||
// --------------------------------------------------
|
||||
// render()
|
||||
// --------------------------------------------------
|
||||
@@ -87,3 +80,11 @@ export const mockRouter: BlitzRouter = {
|
||||
},
|
||||
isFallback: false,
|
||||
}
|
||||
|
||||
type DefaultParams = Parameters<typeof defaultRender>
|
||||
type RenderUI = DefaultParams[0]
|
||||
type RenderOptions = DefaultParams[1] & {router?: Partial<BlitzRouter>}
|
||||
|
||||
type DefaultHookParams = Parameters<typeof defaultRenderHook>
|
||||
type RenderHook = DefaultHookParams[0]
|
||||
type RenderHookOptions = DefaultHookParams[1] & {router?: Partial<BlitzRouter>}
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules", "cypress"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import {DefaultCtx, SessionContext} from "blitz"
|
||||
import {simpleRolesIsAuthorized} from "@blitzjs/server"
|
||||
import {SimpleRolesIsAuthorized} from "@blitzjs/server"
|
||||
import {User} from "db"
|
||||
|
||||
export type Role = "ADMIN" | "USER"
|
||||
|
||||
declare module "blitz" {
|
||||
export interface Ctx extends DefaultCtx {
|
||||
session: SessionContext
|
||||
}
|
||||
export interface Session {
|
||||
isAuthorized: typeof simpleRolesIsAuthorized
|
||||
isAuthorized: SimpleRolesIsAuthorized<Role>
|
||||
PublicData: {
|
||||
userId: User["id"]
|
||||
roles: string[]
|
||||
roles: Role[]
|
||||
views?: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
3. Start the dev server
|
||||
|
||||
```sh
|
||||
blitz start
|
||||
blitz dev
|
||||
|
||||
// Or if you want hot-reloading of server.js, use:
|
||||
yarn start
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
@@ -25,5 +25,5 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
||||
|
||||
```sh
|
||||
blitz build
|
||||
blitz start --production
|
||||
blitz start
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@ const Home: BlitzPage = () => {
|
||||
<code>Ctrl + c</code>
|
||||
</pre>
|
||||
<pre>
|
||||
<code>blitz start</code>
|
||||
<code>blitz dev</code>
|
||||
</pre>
|
||||
<p>
|
||||
and go to{" "}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "@examples/custom-server",
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"scripts": {
|
||||
"start": "nodemon --watch server.js --exec 'blitz start'",
|
||||
"dev": "nodemon --watch server.js --exec 'blitz dev'",
|
||||
"build": "blitz build",
|
||||
"start": "blitz start",
|
||||
"studio": "blitz prisma studio",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
"test-watch": "jest --watch",
|
||||
@@ -11,7 +12,7 @@
|
||||
"cy-run": "cypress run",
|
||||
"test:migrate": "prisma generate && blitz prisma migrate deploy --preview-feature",
|
||||
"test:jest": "jest",
|
||||
"test-server": "blitz build && blitz start --production",
|
||||
"test-server": "blitz build && blitz start",
|
||||
"test:e2e": "cross-env NODE_ENV=test PORT=3099 start-server-and-test test-server http://localhost:3099 cy-run",
|
||||
"test": "run-s test:*"
|
||||
},
|
||||
@@ -39,12 +40,12 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.15.0",
|
||||
"@prisma/client": "2.15.0",
|
||||
"blitz": "0.30.0-canary.4",
|
||||
"@prisma/client": "2.16.0",
|
||||
"blitz": "0.30.0-canary.6",
|
||||
"final-form": "4.20.1",
|
||||
"react": "0.0.0-experimental-4ead6b530",
|
||||
"react-dom": "0.0.0-experimental-4ead6b530",
|
||||
"prisma": "2.16.0",
|
||||
"react": "0.0.0-experimental-3310209d0",
|
||||
"react-dom": "0.0.0-experimental-3310209d0",
|
||||
"react-error-boundary": "2.3.2",
|
||||
"react-final-form": "6.5.2",
|
||||
"secure-password": "4.0.0",
|
||||
@@ -53,7 +54,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/skip-test": "2.6.0",
|
||||
"@testing-library/jest-dom": "5.11.6",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@testing-library/react-hooks": "4.0.1",
|
||||
"@types/jest": "26.0.20",
|
||||
@@ -63,7 +64,7 @@
|
||||
"@typescript-eslint/parser": "4.12.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"cypress": "6.2.1",
|
||||
"eslint": "7.17.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-react-app": "5.2.1",
|
||||
"eslint-plugin-cypress": "2.11.1",
|
||||
"eslint-plugin-flowtype": "5.2.0",
|
||||
@@ -71,7 +72,7 @@
|
||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||
"eslint-plugin-react": "7.21.5",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"husky": "4.3.7",
|
||||
"husky": "4.3.8",
|
||||
"jest": "26.6.3",
|
||||
"jest-environment-jsdom-fourteen": "1.0.1",
|
||||
"jest-watch-typeahead": "0.6.1",
|
||||
@@ -82,7 +83,7 @@
|
||||
"pretty-quick": "3.1.0",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"start-server-and-test": "1.11.2",
|
||||
"ts-jest": "26.4.4"
|
||||
"ts-jest": "26.5.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ FAUNA_SECRET=YOUR_AUTH_KEY
|
||||
2. Start the dev server
|
||||
|
||||
```
|
||||
yarn blitz start
|
||||
yarn blitz dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "@examples/fauna",
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"scripts": {
|
||||
"dev": "blitz dev",
|
||||
"build": "blitz build",
|
||||
"start": "blitz start",
|
||||
"studio": "blitz prisma studio",
|
||||
"build": "blitz build",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
@@ -27,9 +28,9 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"blitz": "0.30.0-canary.4",
|
||||
"blitz": "0.30.0-canary.6",
|
||||
"final-form": "4.20.1",
|
||||
"graphql": "15.4.0",
|
||||
"graphql": "15.5.0",
|
||||
"graphql-request": "3.4.0",
|
||||
"react": "0.0.0-experimental-3310209d0",
|
||||
"react-dom": "0.0.0-experimental-3310209d0",
|
||||
@@ -39,7 +40,7 @@
|
||||
"zod": "1.11.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "5.11.8",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@testing-library/react-hooks": "4.0.1",
|
||||
"@types/jest": "26.0.20",
|
||||
@@ -48,14 +49,14 @@
|
||||
"@typescript-eslint/eslint-plugin": "4.12.0",
|
||||
"@typescript-eslint/parser": "4.12.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"eslint": "7.17.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-react-app": "6.0.0",
|
||||
"eslint-plugin-flowtype": "5.2.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||
"eslint-plugin-react": "7.22.0",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"husky": "4.3.7",
|
||||
"husky": "4.3.8",
|
||||
"jest": "26.6.3",
|
||||
"jest-environment-jsdom-fourteen": "1.0.1",
|
||||
"jest-watch-typeahead": "0.6.1",
|
||||
@@ -63,7 +64,7 @@
|
||||
"prettier": "2.2.1",
|
||||
"pretty-quick": "3.1.0",
|
||||
"start-server-and-test": "1.11.7",
|
||||
"ts-jest": "26.4.4",
|
||||
"ts-jest": "26.5.0",
|
||||
"typescript": "4.1.3"
|
||||
},
|
||||
"private": true
|
||||
|
||||
@@ -14,7 +14,7 @@ model Project {
|
||||
3. Start the dev server
|
||||
|
||||
```
|
||||
blitz start
|
||||
blitz dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "no-prisma",
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"dev": "blitz dev",
|
||||
"build": "blitz build",
|
||||
"start": "blitz start",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
@@ -26,7 +27,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"blitz": "0.30.0-canary.4",
|
||||
"blitz": "0.30.0-canary.6",
|
||||
"knex": "0.21.16",
|
||||
"react": "0.0.0-experimental-3310209d0",
|
||||
"react-dom": "0.0.0-experimental-3310209d0",
|
||||
@@ -37,14 +38,14 @@
|
||||
"@typescript-eslint/eslint-plugin": "4.12.0",
|
||||
"@typescript-eslint/parser": "4.12.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"eslint": "7.17.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-react-app": "6.0.0",
|
||||
"eslint-plugin-flowtype": "5.2.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||
"eslint-plugin-react": "7.22.0",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"husky": "4.3.7",
|
||||
"husky": "4.3.8",
|
||||
"lint-staged": "10.5.3",
|
||||
"prettier": "2.2.1",
|
||||
"pretty-quick": "3.1.0",
|
||||
|
||||
@@ -20,7 +20,7 @@ blitz prisma migrate dev --preview-feature
|
||||
3. Start the dev server
|
||||
|
||||
```
|
||||
blitz start
|
||||
blitz dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "@examples/plain-js",
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"dev": "blitz dev",
|
||||
"build": "blitz prisma migrate deploy --preview-feature && blitz build",
|
||||
"start": "blitz start",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
"test": "echo \"DISABLED\""
|
||||
},
|
||||
@@ -29,9 +30,9 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.15.0",
|
||||
"@prisma/client": "2.15.0",
|
||||
"blitz": "0.30.0-canary.4",
|
||||
"@prisma/client": "2.16.0",
|
||||
"blitz": "0.30.0-canary.6",
|
||||
"prisma": "2.16.0",
|
||||
"react": "0.0.0-experimental-3310209d0",
|
||||
"react-dom": "0.0.0-experimental-3310209d0"
|
||||
},
|
||||
@@ -39,14 +40,14 @@
|
||||
"@typescript-eslint/eslint-plugin": "4.12.0",
|
||||
"@typescript-eslint/parser": "4.12.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"eslint": "7.17.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-react-app": "6.0.0",
|
||||
"eslint-plugin-flowtype": "5.2.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||
"eslint-plugin-react": "7.22.0",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"husky": "4.3.7",
|
||||
"husky": "4.3.8",
|
||||
"lint-staged": "10.5.3",
|
||||
"prettier": "2.2.1",
|
||||
"pretty-quick": "3.1.0",
|
||||
|
||||
@@ -9,7 +9,7 @@ yarn blitz prisma migrate dev --preview-feature
|
||||
2. Start the dev server
|
||||
|
||||
```
|
||||
yarn blitz start
|
||||
yarn blitz dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
@@ -16,7 +16,7 @@ export const getStaticProps = async () => {
|
||||
const Page: BlitzPage<InferGetStaticPropsType<typeof getStaticProps>> = function ({products}) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Products</h1>
|
||||
<h1>First 100 Products</h1>
|
||||
<div id="products">
|
||||
{products.map((product) => (
|
||||
<p key={product.id}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Middleware} from "blitz"
|
||||
import {Middleware, paginate} from "blitz"
|
||||
import db, {Prisma, Product} from "db"
|
||||
import {sum} from "lodash"
|
||||
|
||||
@@ -10,15 +10,12 @@ export function averagePrice(products: Product[]) {
|
||||
type GetProductsInput = {
|
||||
where?: Prisma.ProductFindManyArgs["where"]
|
||||
orderBy?: Prisma.ProductFindManyArgs["orderBy"]
|
||||
skip?: Prisma.ProductFindManyArgs["skip"]
|
||||
cursor?: Prisma.ProductFindManyArgs["cursor"]
|
||||
take?: Prisma.ProductFindManyArgs["take"]
|
||||
// Only available if a model relationship exists
|
||||
// include?: Prisma.ProductFindManyArgs['include']
|
||||
skip?: number
|
||||
take?: number
|
||||
}
|
||||
|
||||
export default async function getProducts(
|
||||
{where, orderBy, skip = 0, cursor, take}: GetProductsInput,
|
||||
{where, orderBy, skip = 0, take = 100}: GetProductsInput,
|
||||
ctx: Record<any, unknown> = {},
|
||||
) {
|
||||
if (ctx.referer) {
|
||||
@@ -27,22 +24,18 @@ export default async function getProducts(
|
||||
|
||||
console.log("this line should not be included in the frontend bundle")
|
||||
|
||||
const products = await db.product.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
const {items: products, hasMore, nextPage, count} = await paginate({
|
||||
skip,
|
||||
cursor,
|
||||
take,
|
||||
count: () => db.product.count({where}),
|
||||
query: (paginateArgs) => db.product.findMany({...paginateArgs, where, orderBy}),
|
||||
})
|
||||
|
||||
const count = await db.product.count()
|
||||
const hasMore = typeof take === "number" ? skip + take < count : false
|
||||
const nextPage = hasMore ? {take, skip: skip + take!} : null
|
||||
|
||||
return {
|
||||
products,
|
||||
nextPage,
|
||||
hasMore,
|
||||
nextPage,
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Variant" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"productId" INTEGER NOT NULL,
|
||||
FOREIGN KEY ("productId") REFERENCES "Product" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
2
examples/store/db/migrations/migration_lock.toml
Normal file
2
examples/store/db/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
# Please do not edit this file manually
|
||||
provider = "sqlite"
|
||||
@@ -38,4 +38,15 @@ model Product {
|
||||
name String?
|
||||
description String?
|
||||
price Int?
|
||||
|
||||
variants Variant[]
|
||||
}
|
||||
|
||||
model Variant {
|
||||
id Int @default(autoincrement()) @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
name String
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
productId Int
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@examples/store",
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "blitz prisma migrate deploy --preview-feature && blitz build",
|
||||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run || cypress run",
|
||||
"test:server": "prisma generate && blitz prisma migrate deploy --preview-feature && blitz db seed && blitz build && blitz start --production -p 3099",
|
||||
"test:server": "prisma generate && blitz prisma migrate deploy --preview-feature && blitz db seed && blitz build && blitz start -p 3099",
|
||||
"test": "cross-env NODE_ENV=test start-server-and-test test:server http://localhost:3099 cy:run",
|
||||
"posttest": "node assert-tree-shaking-works.js"
|
||||
},
|
||||
@@ -20,10 +20,10 @@
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.15.0",
|
||||
"@prisma/client": "2.15.0",
|
||||
"blitz": "0.30.0-canary.4",
|
||||
"@prisma/client": "2.16.0",
|
||||
"blitz": "0.30.0-canary.6",
|
||||
"final-form": "4.20.1",
|
||||
"prisma": "2.16.0",
|
||||
"react": "0.0.0-experimental-3310209d0",
|
||||
"react-dom": "0.0.0-experimental-3310209d0",
|
||||
"react-error-boundary": "3.1.0",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@types/react": "17.0.0",
|
||||
"cypress": "6.2.1",
|
||||
"eslint-plugin-cypress": "2.11.2",
|
||||
"sass": "1.32.0",
|
||||
"sass": "1.32.5",
|
||||
"start-server-and-test": "1.11.7"
|
||||
}
|
||||
}
|
||||
|
||||
23
jest.config.js
Normal file
23
jest.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const {resolveAliases} = require("@blitzjs/config")
|
||||
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "json"],
|
||||
modulePathIgnorePatterns: ["<rootDir>/tmp", "<rootDir>/dist", "<rootDir>/templates"],
|
||||
moduleNameMapper: {
|
||||
...resolveAliases.node,
|
||||
},
|
||||
coverageReporters: ["json", "lcov", "text", "clover"],
|
||||
// collectCoverage: !!`Boolean(process.env.CI)`,
|
||||
collectCoverageFrom: ["src/**/*.ts"],
|
||||
coveragePathIgnorePatterns: ["/templates/"],
|
||||
// coverageThreshold: {
|
||||
// global: {
|
||||
// branches: 100,
|
||||
// functions: 100,
|
||||
// lines: 100,
|
||||
// statements: 100,
|
||||
// },
|
||||
// },
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"packages": ["packages/*"],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
|
||||
32
package.json
32
package.json
@@ -39,12 +39,14 @@
|
||||
"publish-danger": "lerna publish --canary --pre-dist-tag danger --preid danger.$(git rev-parse --short HEAD) --allow-branch * --force-publish",
|
||||
"danger:push-all": "git push --no-verify && git push --no-verify --tags"
|
||||
},
|
||||
"resolutions-NOTE1": "typescript and ts-jest pinned to here overside tsdx included versions",
|
||||
"resolutions": {
|
||||
"typescript": "4.1.3"
|
||||
"typescript": "4.1.3",
|
||||
"ts-jest": "26.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/pluginutils": "4.1.0",
|
||||
"@testing-library/jest-dom": "5.11.8",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@testing-library/react-hooks": "4.0.1",
|
||||
"@types/b64-lite": "1.3.0",
|
||||
@@ -62,13 +64,14 @@
|
||||
"@types/gulp-if": "0.0.33",
|
||||
"@types/ink-spinner": "3.0.0",
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/module-alias": "2.0.0",
|
||||
"@types/jsonwebtoken": "8.5.0",
|
||||
"@types/lodash": "4.14.166",
|
||||
"@types/lodash": "4.14.168",
|
||||
"@types/mem-fs": "1.1.2",
|
||||
"@types/mem-fs-editor": "7.0.0",
|
||||
"@types/merge-stream": "1.1.2",
|
||||
"@types/mock-fs": "4.13.0",
|
||||
"@types/node": "14.14.20",
|
||||
"@types/node": "14.14.22",
|
||||
"@types/node-fetch": "2.5.8",
|
||||
"@types/parallel-transform": "1.1.0",
|
||||
"@types/passport": "1.0.5",
|
||||
@@ -86,7 +89,6 @@
|
||||
"@types/through2": "2.0.36",
|
||||
"@types/vinyl": "2.0.4",
|
||||
"@types/vinyl-fs": "2.4.11",
|
||||
"@types/webpack": "4.41.26",
|
||||
"@typescript-eslint/eslint-plugin": "4.12.0",
|
||||
"@typescript-eslint/parser": "4.12.0",
|
||||
"@wessberg/cjs-to-esm-transformer": "0.0.22",
|
||||
@@ -96,10 +98,11 @@
|
||||
"concurrently": "5.3.0",
|
||||
"cpy-cli": "3.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "6.2.1",
|
||||
"debug": "4.3.1",
|
||||
"delay": "4.4.0",
|
||||
"delay": "4.4.1",
|
||||
"directory-tree": "2.2.5",
|
||||
"eslint": "7.17.0",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-react-app": "6.0.0",
|
||||
"eslint-plugin-es": "4.1.0",
|
||||
"eslint-plugin-es5": "1.5.0",
|
||||
@@ -110,20 +113,21 @@
|
||||
"eslint-plugin-react": "7.22.0",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"eslint-plugin-unicorn": "25.0.1",
|
||||
"husky": "4.3.7",
|
||||
"@size-limit/preset-small-lib": "4.9.1",
|
||||
"size-limit": "4.9.1",
|
||||
"eslint-plugin-unicorn": "26.0.1",
|
||||
"husky": "4.3.8",
|
||||
"@size-limit/preset-small-lib": "4.9.2",
|
||||
"size-limit": "4.9.2",
|
||||
"jest": "26.6.3",
|
||||
"jest-environment-jsdom-sixteen": "1.0.3",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "10.5.3",
|
||||
"mock-fs": "4.13.0",
|
||||
"nock": "13.0.5",
|
||||
"nock": "13.0.6",
|
||||
"npm-run-all": "4.1.5",
|
||||
"patch-package": "6.2.2",
|
||||
"postinstall-postinstall": "2.1.0",
|
||||
"prettier": "2.2.1",
|
||||
"prettier-plugin-prisma": "0.2.0",
|
||||
"pretty-quick": "3.1.0",
|
||||
"prompt": "1.1.0",
|
||||
"react-test-renderer": "17.0.1",
|
||||
@@ -138,13 +142,13 @@
|
||||
"semver": "7.3.4",
|
||||
"stdout-stderr": "0.1.13",
|
||||
"test-listen": "1.1.0",
|
||||
"ts-jest": "26.4.4",
|
||||
"ts-jest": "26.5.0",
|
||||
"tsdx": "0.14.1",
|
||||
"tslib": "2.1.0",
|
||||
"typescript": "4.1.3",
|
||||
"wait-on": "5.2.1",
|
||||
"yalc": "1.0.0-pre.49",
|
||||
"eslint_d": "9.1.2"
|
||||
"eslint_d": "10.0.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/babel-preset",
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
|
||||
22
packages/babel-preset/src/add-blitz-app-root.ts
Normal file
22
packages/babel-preset/src/add-blitz-app-root.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { PluginObj } from '@babel/core';
|
||||
import { getFileName, wrapExportDefaultDeclaration } from './utils';
|
||||
|
||||
function AddBlitzAppRoot(): PluginObj {
|
||||
return {
|
||||
name: 'AddBlitzAppRoot',
|
||||
visitor: {
|
||||
ExportDefaultDeclaration(path, state) {
|
||||
const filePath = getFileName(state);
|
||||
|
||||
if (!filePath?.match(/_app\./)) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrapExportDefaultDeclaration(path, 'withBlitzAppRoot', '@blitzjs/core');
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AddBlitzAppRoot;
|
||||
@@ -1,7 +1,9 @@
|
||||
import AddBlitzAppRoot from './add-blitz-app-root';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function preset(_api: any, options = {}) {
|
||||
return {
|
||||
presets: [[require('next/babel'), options]],
|
||||
plugins: [require('babel-plugin-superjson-next')],
|
||||
plugins: [require('babel-plugin-superjson-next'), AddBlitzAppRoot],
|
||||
};
|
||||
}
|
||||
|
||||
9
packages/babel-preset/src/types.d.ts
vendored
Normal file
9
packages/babel-preset/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare module '@babel/helper-module-imports' {
|
||||
import { NodePath, types } from '@babel/core';
|
||||
|
||||
function addNamed(
|
||||
path: NodePath,
|
||||
named: string,
|
||||
source: string
|
||||
): types.Identifier;
|
||||
}
|
||||
77
packages/babel-preset/src/utils.ts
Normal file
77
packages/babel-preset/src/utils.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NodePath, PluginPass, types as t } from '@babel/core';
|
||||
import { addNamed as addNamedImport } from '@babel/helper-module-imports';
|
||||
|
||||
export function functionDeclarationToExpression(
|
||||
declaration: t.FunctionDeclaration
|
||||
) {
|
||||
return t.functionExpression(
|
||||
declaration.id,
|
||||
declaration.params,
|
||||
declaration.body,
|
||||
declaration.generator,
|
||||
declaration.async
|
||||
);
|
||||
}
|
||||
|
||||
export function classDeclarationToExpression(declaration: t.ClassDeclaration) {
|
||||
return t.classExpression(
|
||||
declaration.id,
|
||||
declaration.superClass,
|
||||
declaration.body,
|
||||
declaration.decorators
|
||||
);
|
||||
}
|
||||
|
||||
export function getFileName(state: PluginPass) {
|
||||
const { filename, cwd } = state;
|
||||
|
||||
if (!filename) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cwd && filename.startsWith(cwd)) {
|
||||
return filename.slice(cwd.length);
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
export function wrapExportDefaultDeclaration(
|
||||
path: NodePath<any>,
|
||||
HOFName: string,
|
||||
importFrom: string
|
||||
) {
|
||||
function wrapInHOF(path: NodePath<any>, expr: t.Expression) {
|
||||
return t.callExpression(addNamedImport(path, HOFName, importFrom), [expr]);
|
||||
}
|
||||
|
||||
const { node } = path;
|
||||
|
||||
if (
|
||||
t.isIdentifier(node.declaration) ||
|
||||
t.isFunctionExpression(node.declaration) ||
|
||||
t.isCallExpression(node.declaration)
|
||||
) {
|
||||
node.declaration = wrapInHOF(path, node.declaration);
|
||||
} else if (
|
||||
t.isFunctionDeclaration(node.declaration) ||
|
||||
t.isClassDeclaration(node.declaration)
|
||||
) {
|
||||
if (node.declaration.id) {
|
||||
path.insertBefore(node.declaration);
|
||||
node.declaration = wrapInHOF(path, node.declaration.id);
|
||||
} else {
|
||||
if (t.isFunctionDeclaration(node.declaration)) {
|
||||
node.declaration = wrapInHOF(
|
||||
path,
|
||||
functionDeclarationToExpression(node.declaration)
|
||||
);
|
||||
} else {
|
||||
node.declaration = wrapInHOF(
|
||||
path,
|
||||
classDeclarationToExpression(node.declaration)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
const path = require("path")
|
||||
const pkgDir = require("pkg-dir")
|
||||
const {pathsToModuleNameMapper} = require("ts-jest/utils")
|
||||
const projectRoot = pkgDir.sync() || process.cwd()
|
||||
const {getProjectRoot, resolveAliases} = require("@blitzjs/config")
|
||||
const projectRoot = getProjectRoot()
|
||||
const {compilerOptions} = require(path.join(projectRoot, "tsconfig"))
|
||||
|
||||
module.exports = {
|
||||
maxWorkers: 1,
|
||||
const common = {
|
||||
globalSetup: path.resolve(__dirname, "./jest-preset/global-setup.js"),
|
||||
setupFilesAfterEnv: [
|
||||
path.resolve(__dirname, "./jest-preset/setup-after-env.js"),
|
||||
"<rootDir>/test/setup.ts",
|
||||
],
|
||||
// Add type checking to TypeScript test files
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jest-environment-jsdom-fourteen",
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
testPathIgnorePatterns: ["/node_modules/", "/.blitz/", "/.next/", "<rootDir>/db/migrations"],
|
||||
testPathIgnorePatterns: [
|
||||
"/node_modules/",
|
||||
"/.blitz/",
|
||||
"/.next/",
|
||||
"<rootDir>/db/migrations",
|
||||
"<rootDir>/test/e2e",
|
||||
"<rootDir>/cypress",
|
||||
],
|
||||
transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$"],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "babel-jest",
|
||||
@@ -24,13 +25,9 @@ module.exports = {
|
||||
// This makes absolute imports work
|
||||
moduleDirectories: ["node_modules", "<rootDir>"],
|
||||
// Ignore the build directories
|
||||
modulePathIgnorePatterns: [
|
||||
"<rootDir>/.blitz",
|
||||
"<rootDir>/.next",
|
||||
"<rootDir>/test/e2e",
|
||||
"<rootDir>/cypress",
|
||||
],
|
||||
modulePathIgnorePatterns: ["<rootDir>/.blitz", "<rootDir>/.next"],
|
||||
moduleNameMapper: {
|
||||
...resolveAliases.node,
|
||||
// This ensures any path aliases in tsconfig also work in jest
|
||||
...pathsToModuleNameMapper(compilerOptions.paths || {}),
|
||||
"\\.(css|less|sass|scss)$": path.resolve(__dirname, "./jest-preset/identity-obj-proxy.js"),
|
||||
@@ -44,3 +41,41 @@ module.exports = {
|
||||
coverageDirectory: ".coverage",
|
||||
collectCoverageFrom: ["**/*.{js,jsx,ts,tsx}", "!**/*.d.ts", "!**/node_modules/**"],
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// TODO - check on https://github.com/facebook/jest/issues/10936
|
||||
maxWorkers: 1,
|
||||
projects: [
|
||||
{
|
||||
...common,
|
||||
name: "client",
|
||||
displayName: {
|
||||
name: "CLIENT",
|
||||
color: "cyan",
|
||||
},
|
||||
testEnvironment: "jest-environment-jsdom-fourteen",
|
||||
testRegex: ["^((?!queries|mutations|api|\\.server\\.).)*\\.(test|spec)\\.(j|t)sx?$"],
|
||||
setupFilesAfterEnv: [
|
||||
path.resolve(__dirname, "./jest-preset/client/setup-after-env.js"),
|
||||
"<rootDir>/test/setup.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
...common,
|
||||
name: "server",
|
||||
displayName: {
|
||||
name: "SERVER",
|
||||
color: "magenta",
|
||||
},
|
||||
testEnvironment: "node",
|
||||
testRegex: [
|
||||
"\\.server\\.(spec|test)\\.(j|t)sx?$",
|
||||
"[\\/](queries|mutations|api)[\\/].*\\.(test|spec)\\.(j|t)sx?$",
|
||||
],
|
||||
setupFilesAfterEnv: [
|
||||
path.resolve(__dirname, "./jest-preset/server/setup-after-env.js"),
|
||||
"<rootDir>/test/setup.ts",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
1
packages/blitz/jest-preset/client/setup-after-env.js
Normal file
1
packages/blitz/jest-preset/client/setup-after-env.js
Normal file
@@ -0,0 +1 @@
|
||||
require("@testing-library/jest-dom")
|
||||
@@ -3,6 +3,8 @@ require("@testing-library/jest-dom")
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await global._blitz_prismaClient.$disconnect()
|
||||
// console.log("DISCONNECT")
|
||||
// await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
// ignore error
|
||||
}
|
||||
@@ -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.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
@@ -43,20 +43,20 @@
|
||||
"url": "https://github.com/blitz-js/blitz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blitzjs/babel-preset": "0.30.0-canary.4",
|
||||
"@blitzjs/cli": "0.30.0-canary.4",
|
||||
"@blitzjs/config": "0.30.0-canary.4",
|
||||
"@blitzjs/core": "0.30.0-canary.4",
|
||||
"@blitzjs/display": "0.30.0-canary.4",
|
||||
"@blitzjs/generator": "0.30.0-canary.4",
|
||||
"@blitzjs/installer": "0.30.0-canary.4",
|
||||
"@blitzjs/server": "0.30.0-canary.4",
|
||||
"@blitzjs/babel-preset": "0.30.0-canary.6",
|
||||
"@blitzjs/cli": "0.30.0-canary.6",
|
||||
"@blitzjs/config": "0.30.0-canary.6",
|
||||
"@blitzjs/core": "0.30.0-canary.6",
|
||||
"@blitzjs/display": "0.30.0-canary.6",
|
||||
"@blitzjs/generator": "0.30.0-canary.6",
|
||||
"@blitzjs/installer": "0.30.0-canary.6",
|
||||
"@blitzjs/server": "0.30.0-canary.6",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "^11.2.3",
|
||||
"@testing-library/react-hooks": "^4.0.1",
|
||||
"@types/jest": "^26.0.20",
|
||||
"envinfo": "^7.7.3",
|
||||
"eslint-config-blitz": "0.30.0-canary.4",
|
||||
"eslint-config-blitz": "0.30.0-canary.6",
|
||||
"jest": "^26.6.3",
|
||||
"jest-environment-jsdom-fourteen": "^1.0.1",
|
||||
"jest-watch-typeahead": "^0.6.1",
|
||||
|
||||
@@ -14,6 +14,8 @@ const common = {
|
||||
"next",
|
||||
"fs",
|
||||
"path",
|
||||
"os",
|
||||
"tty",
|
||||
],
|
||||
plugins: [
|
||||
json(),
|
||||
|
||||
@@ -93,7 +93,7 @@ async function printEnvInfo() {
|
||||
{
|
||||
System: ["OS", "CPU", "Memory", "Shell"],
|
||||
Binaries: ["Node", "Yarn", "npm", "Watchman"],
|
||||
npmPackages: ["blitz", "typescript", "react", "react-dom", "@prisma/cli", "@prisma/client"],
|
||||
npmPackages: ["blitz", "typescript", "react", "react-dom", "prisma", "@prisma/client"],
|
||||
},
|
||||
{showNotFound: true},
|
||||
)
|
||||
|
||||
@@ -33,5 +33,5 @@ blitz build
|
||||
### Launch the development server
|
||||
|
||||
```bash
|
||||
blitz start
|
||||
blitz dev
|
||||
```
|
||||
|
||||
3
packages/cli/cypress.json
Normal file
3
packages/cli/cypress.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"integrationFolder": "test/e2e/cypress/integration"
|
||||
}
|
||||
5
packages/cli/cypress/fixtures/example.json
Normal file
5
packages/cli/cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
21
packages/cli/cypress/plugins/index.js
Normal file
21
packages/cli/cypress/plugins/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
25
packages/cli/cypress/support/commands.js
Normal file
25
packages/cli/cypress/support/commands.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
20
packages/cli/cypress/support/index.js
Normal file
20
packages/cli/cypress/support/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands"
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
BIN
packages/cli/cypress/videos/basic.ts.mp4
Normal file
BIN
packages/cli/cypress/videos/basic.ts.mp4
Normal file
Binary file not shown.
@@ -1,23 +1,9 @@
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
moduleFileExtensions: ["ts", "js", "json"],
|
||||
coverageReporters: ["json", "lcov", "text", "clover"],
|
||||
preset: "../../jest.config.js",
|
||||
// collectCoverage: !!`Boolean(process.env.CI)`,
|
||||
collectCoverageFrom: ["src/**/*.ts"],
|
||||
modulePathIgnorePatterns: ["<rootDir>/tmp", "<rootDir>/lib"],
|
||||
testPathIgnorePatterns: ["src/commands/test.ts"],
|
||||
testTimeout: 30000,
|
||||
// TODO enable threshold
|
||||
// coverageThreshold: {
|
||||
// global: {
|
||||
// branches: 100,
|
||||
// functions: 100,
|
||||
// lines: 100,
|
||||
// statements: 100,
|
||||
// },
|
||||
// },
|
||||
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig: "test/tsconfig.json",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@blitzjs/cli",
|
||||
"description": "Blitz.js CLI",
|
||||
"version": "0.30.0-canary.4",
|
||||
"version": "0.30.0-canary.6",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"b": "./bin/run",
|
||||
@@ -14,7 +14,9 @@
|
||||
"dev": "rimraf lib && tsc --watch --preserveWatchOutput",
|
||||
"build": "rimraf lib && tsc",
|
||||
"test": "tsdx test",
|
||||
"test:watch": "jest --watch"
|
||||
"test:watch": "jest --watch",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run"
|
||||
},
|
||||
"author": {
|
||||
"name": "Brandon Bayer",
|
||||
@@ -30,15 +32,15 @@
|
||||
"/lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@blitzjs/display": "0.30.0-canary.4",
|
||||
"@blitzjs/repl": "0.30.0-canary.4",
|
||||
"@blitzjs/display": "0.30.0-canary.6",
|
||||
"@blitzjs/repl": "0.30.0-canary.6",
|
||||
"@oclif/command": "1.8.0",
|
||||
"@oclif/config": "1.17.0",
|
||||
"@oclif/plugin-autocomplete": "0.3.0",
|
||||
"@oclif/plugin-help": "3.2.1",
|
||||
"@oclif/plugin-not-found": "1.2.4",
|
||||
"@prisma/sdk": "2.15.0",
|
||||
"@salesforce/lazy-require": "0.3.4",
|
||||
"@prisma/sdk": "2.16.0",
|
||||
"@salesforce/lazy-require": "0.4.0",
|
||||
"camelcase": "^6.2.0",
|
||||
"chalk": "4.1.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
@@ -51,6 +53,7 @@
|
||||
"hasbin": "1.2.3",
|
||||
"import-cwd": "3.0.0",
|
||||
"minimist": "1.2.5",
|
||||
"module-alias": "2.2.2",
|
||||
"p-event": "4.2.0",
|
||||
"pkg-dir": "^5.0.0",
|
||||
"pluralize": "^8.0.0",
|
||||
@@ -61,13 +64,13 @@
|
||||
"v8-compile-cache": "2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blitzjs/generator": "0.30.0-canary.4",
|
||||
"@blitzjs/installer": "0.30.0-canary.4",
|
||||
"@blitzjs/server": "0.30.0-canary.4",
|
||||
"@blitzjs/generator": "0.30.0-canary.6",
|
||||
"@blitzjs/installer": "0.30.0-canary.6",
|
||||
"@blitzjs/server": "0.30.0-canary.6",
|
||||
"@oclif/dev-cli": "1.26.0",
|
||||
"@oclif/test": "1.2.8",
|
||||
"@prisma/cli": "2.15.0",
|
||||
"nock": "13.0.5",
|
||||
"nock": "13.0.6",
|
||||
"prisma": "2.16.0",
|
||||
"stdout-stderr": "0.1.13"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -10,7 +10,7 @@ export function getDbName(connectionString: string): string {
|
||||
async function runSeed() {
|
||||
require("../utils/setup-ts-node").setupTsnode()
|
||||
|
||||
const projectRoot = require("../utils/get-project-root").projectRoot
|
||||
const projectRoot = require("@blitzjs/config").getProjectRoot()
|
||||
const seedPath = require("path").join(projectRoot, "db/seeds")
|
||||
const dbPath = require("path").join(projectRoot, "db/index")
|
||||
|
||||
|
||||
46
packages/cli/src/commands/dev.ts
Normal file
46
packages/cli/src/commands/dev.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {ServerConfig} from "@blitzjs/server"
|
||||
import {Command, flags} from "@oclif/command"
|
||||
|
||||
export class Dev extends Command {
|
||||
static description = "Start a development server"
|
||||
static aliases = ["d"]
|
||||
|
||||
static flags = {
|
||||
help: flags.help({char: "h"}),
|
||||
port: flags.integer({
|
||||
char: "p",
|
||||
description: "Set port number",
|
||||
}),
|
||||
hostname: flags.string({
|
||||
char: "H",
|
||||
description: "Set server hostname",
|
||||
}),
|
||||
inspect: flags.boolean({
|
||||
description: "Enable the Node.js inspector",
|
||||
}),
|
||||
["no-incremental-build"]: flags.boolean({
|
||||
description: "Disable incremental build and start from a fresh cache",
|
||||
}),
|
||||
}
|
||||
|
||||
async run() {
|
||||
const {flags} = this.parse(Dev)
|
||||
|
||||
const config: ServerConfig = {
|
||||
rootFolder: process.cwd(),
|
||||
port: flags.port,
|
||||
hostname: flags.hostname,
|
||||
inspect: flags.inspect,
|
||||
clean: flags["no-incremental-build"],
|
||||
env: "dev",
|
||||
}
|
||||
|
||||
try {
|
||||
const dev = (await import("@blitzjs/server")).dev
|
||||
await dev(config)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(1) // clean up?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@ import {flags} from "@oclif/command"
|
||||
import {log} from "@blitzjs/display"
|
||||
import {
|
||||
PageGenerator,
|
||||
MutationsGenerator,
|
||||
MutationGenerator,
|
||||
QueriesGenerator,
|
||||
FormGenerator,
|
||||
ModelGenerator,
|
||||
QueryGenerator,
|
||||
singleCamel,
|
||||
capitalize,
|
||||
uncapitalize,
|
||||
singlePascal,
|
||||
pluralCamel,
|
||||
pluralPascal,
|
||||
@@ -20,17 +23,18 @@ import chalk from "chalk"
|
||||
const debug = require("debug")("blitz:generate")
|
||||
const getIsTypeScript = () =>
|
||||
require("fs").existsSync(
|
||||
require("path").join(require("../utils/get-project-root").projectRoot, "tsconfig.json"),
|
||||
require("path").join(require("@blitzjs/config").getProjectRoot(), "tsconfig.json"),
|
||||
)
|
||||
|
||||
enum ResourceType {
|
||||
All = "all",
|
||||
Crud = "crud",
|
||||
Model = "model",
|
||||
Mutations = "mutations",
|
||||
Pages = "pages",
|
||||
Queries = "queries",
|
||||
Query = "query",
|
||||
Mutations = "mutations",
|
||||
Mutation = "mutation",
|
||||
Resource = "resource",
|
||||
}
|
||||
|
||||
@@ -60,19 +64,20 @@ function ModelNames(input: string = "") {
|
||||
|
||||
const generatorMap = {
|
||||
[ResourceType.All]: [
|
||||
ModelGenerator,
|
||||
PageGenerator,
|
||||
FormGenerator,
|
||||
QueriesGenerator,
|
||||
MutationGenerator,
|
||||
MutationsGenerator,
|
||||
ModelGenerator,
|
||||
],
|
||||
[ResourceType.Crud]: [MutationGenerator, QueriesGenerator],
|
||||
[ResourceType.Crud]: [MutationsGenerator, QueriesGenerator],
|
||||
[ResourceType.Model]: [ModelGenerator],
|
||||
[ResourceType.Mutations]: [MutationGenerator],
|
||||
[ResourceType.Pages]: [PageGenerator, FormGenerator],
|
||||
[ResourceType.Queries]: [QueriesGenerator],
|
||||
[ResourceType.Query]: [QueryGenerator],
|
||||
[ResourceType.Resource]: [ModelGenerator, QueriesGenerator, MutationGenerator],
|
||||
[ResourceType.Mutations]: [MutationsGenerator],
|
||||
[ResourceType.Mutation]: [MutationGenerator],
|
||||
[ResourceType.Resource]: [QueriesGenerator, MutationsGenerator, ModelGenerator],
|
||||
}
|
||||
|
||||
export class Generate extends Command {
|
||||
@@ -226,7 +231,8 @@ export class Generate extends Command {
|
||||
parentModels: modelNames(flags.parent),
|
||||
ParentModel: ModelName(flags.parent),
|
||||
ParentModels: ModelNames(flags.parent),
|
||||
rawInput: model,
|
||||
name: uncapitalize(model),
|
||||
Name: capitalize(model),
|
||||
dryRun: flags["dry-run"],
|
||||
context: context,
|
||||
useTs: getIsTypeScript(),
|
||||
|
||||
@@ -185,7 +185,7 @@ export class New extends Command {
|
||||
)
|
||||
}
|
||||
|
||||
postInstallSteps.push("blitz start")
|
||||
postInstallSteps.push("blitz dev")
|
||||
|
||||
this.log("\n" + log.withBrand("Your new Blitz app is ready! Next steps:") + "\n")
|
||||
|
||||
|
||||
@@ -3,8 +3,16 @@ import {Command} from "@oclif/command"
|
||||
// @blitzjs/server imports react, so we must import the @blitzjs/server version of the
|
||||
// local app instead of the global.
|
||||
// import-cwd is required so this works correctly during new app generation
|
||||
const getPrismaBin = () =>
|
||||
require("import-cwd")("@blitzjs/server").resolveBinAsync("@prisma/cli", "prisma")
|
||||
const getPrismaBin = async () => {
|
||||
let bin: any
|
||||
try {
|
||||
bin = require("import-cwd")("@blitzjs/server").resolveBinAsync("prisma", "prisma")
|
||||
} catch {
|
||||
// legacy compatability
|
||||
bin = require("import-cwd")("@blitzjs/server").resolveBinAsync("@prisma/cli", "prisma")
|
||||
}
|
||||
return bin
|
||||
}
|
||||
|
||||
let prismaBin: string
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import {dev as Dev, prod as Prod, ServerConfig} from "@blitzjs/server"
|
||||
import {ServerConfig} from "@blitzjs/server"
|
||||
import {Command, flags} from "@oclif/command"
|
||||
|
||||
export class Start extends Command {
|
||||
static description = "Start a development server"
|
||||
static description = "Start the production server"
|
||||
static aliases = ["s"]
|
||||
|
||||
static flags = {
|
||||
help: flags.help({char: "h"}),
|
||||
production: flags.boolean({
|
||||
description: "Create and start a production server",
|
||||
}),
|
||||
port: flags.integer({
|
||||
char: "p",
|
||||
description: "Set port number",
|
||||
@@ -21,10 +18,6 @@ export class Start extends Command {
|
||||
inspect: flags.boolean({
|
||||
description: "Enable the Node.js inspector",
|
||||
}),
|
||||
["no-incremental-build"]: flags.boolean({
|
||||
description:
|
||||
"Disable incremental build and start from a fresh cache. Incremental build is automatically enabled for development mode and disabled during `blitz build` or when the `--production` flag is supplied.",
|
||||
}),
|
||||
}
|
||||
|
||||
async run() {
|
||||
@@ -35,18 +28,13 @@ export class Start extends Command {
|
||||
port: flags.port,
|
||||
hostname: flags.hostname,
|
||||
inspect: flags.inspect,
|
||||
clean: flags["no-incremental-build"],
|
||||
env: flags.production ? "prod" : "dev",
|
||||
clean: true,
|
||||
env: "prod",
|
||||
}
|
||||
|
||||
try {
|
||||
if (flags.production) {
|
||||
const prod: typeof Prod = require("@blitzjs/server").prod
|
||||
await prod(config)
|
||||
} else {
|
||||
const dev: typeof Dev = require("@blitzjs/server").dev
|
||||
await dev(config)
|
||||
}
|
||||
const prod = (await import("@blitzjs/server")).prod
|
||||
await prod(config)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(1) // clean up?
|
||||
|
||||
@@ -3,6 +3,10 @@ const cacheFile = require("path").join(__dirname, ".blitzjs-cli-cache")
|
||||
const lazyLoad = require("@salesforce/lazy-require").default.create(cacheFile)
|
||||
lazyLoad.start()
|
||||
import {run as oclifRun} from "@oclif/command"
|
||||
import moduleAlias from "module-alias"
|
||||
import {resolveAliases} from "@blitzjs/config"
|
||||
|
||||
moduleAlias.addAliases(resolveAliases.node)
|
||||
|
||||
// Load the .env environment variable so it's available for all commands
|
||||
require("dotenv-expand")(require("dotenv-flow").config({silent: true}))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user