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

Compare commits

..

2 Commits

Author SHA1 Message Date
Brandon Bayer
0c3fb25d44 Merge branch 'canary' into error-fallback-props 2021-01-29 20:18:40 -05:00
Brandon Bayer
e3c4bed828 add type ErrorFallbackProps for better types in _app.tsx 2021-01-29 19:45:11 -05:00
210 changed files with 2292 additions and 3724 deletions

View File

@@ -1882,99 +1882,6 @@
"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,

3
.gitignore vendored
View File

@@ -1,6 +1,5 @@
.log
.DS_Store
.idea
.jest-*
lib
node_modules
@@ -25,5 +24,3 @@ dist
.tsbuildinfo
.nvmrc
**/.test*
examples/auth2
.idea

View File

@@ -6,7 +6,7 @@
<img alt="" src="https://img.shields.io/badge/Join%20our%20community-6700EB.svg?style=for-the-badge&labelColor=000000&logoWidth=20&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ9SURBVHgB7d3dVdtAEIbhcSpICUoH0IEogQqSVBBSAU4FSSpIOoAORAfQgSghHXzZ1U/YcMD4R9rZmf2ec3y448LyiNf27iLiGIAmPLrweC9Un3DhrzG6EarLNP09nlwJ1SOZ/lQr5N80/S/p2QMVCBf5N17XCfm1Y/rBHqjAG9PPHvBsz+mf9WAP+HLA9M/YA14cOP2payH7jpj+VCtk1wnTP+vj7xCy6cTpn7EHLMLp059iD1iD8eveJbVCNsSLheX1YA/YgOWnf8YeKB3Wmf7Ud6Fy4f/FHmtpxbl3YlC4MJ/Cj0bWdwPnPbARg+L0S54XQHS32WwuxClzd4CM0z9rPfeAuTtA5ulPXYQ7wZ04Y+oOoDD9KZc9YOoOoDj9s4dwFzgXR6w1wIPoOvPWA9buAHEJ173o3gWiy3AnuBUHLEbgmYwvAk1/wuM8vAgexThzbwPDkx7/DHwVXfFOxP2GmsKd4Ab6zPeAyU8CI7AHFmH2BRCBPXAyk18GzUrqAXCTiR4ssyj0VFw/oCU8+e+RZ33AWz6KMaYbIIWxB+JSLs1bsbkeMN0AqakHvoku9oA2sAfqBvbAQdw0QArsgb25aYBUQT3QgT2gB+yBuqGcHij2UCqXDZACe2Anlw2QYg/QAOyBuoE98CL3DZDCuK4/rh/Q7oGL6U+TOvcNkJoijN8X1C48+T+g75eQDrAH/qmqAVJgDwyqaoAUe4AGYA/UDZX3QLUNkEIZPRCd5+6BahsgVUgPROwBTSijB7jpVAvGHriHvmw9wAZ4BpX1ABvgmakHtPcbRuwBTWAPULgAV9D/jKDY9YRvwvgEaurD44uQHvAol7qBW7WKluVtIHiUS7GyvA0s6CiXDnxrpQfsgbqBS7GKk/2jYHCrVlGyfxTMrVo0ALdq1Q3sgSKofh0M9oA61a+D2QM0AHugbmAPqClmSRjK2apVVQ8UsySsoK1aHdgDesCtWnUDeyCrIpeFg1u3sylyWTi3btMA7IG6gT2wuuK3hoE9sKrit4YVslWLPaAN7IG6ocKt2zmY2h4O9sDiTG0PZw/QANy6XTewBxZj9ogYVHy025LMHhEz9cBn0We6B0yfERReBLfhx0/R1YQHPx/QBPbA0VwcEwf2wNFcHBPHHjiem3MC2QPHcXdSaJjA+KfgTPQ8hhfjBzHC40mhlzJ+Xq9lK4a4PCs43AVaGTed5mZq+iOXZwWHi3AnOj2wFWNcnxYe7gTxLtBKHuamP/J+Wnh8a5irB7ZC5Yk9gPX1QuXC+usHWqGyhYvUYR0a7zboUOFCNVhnk0krZAOW7wFOvzXhom2xnEbIHizTA1wEYhWW6YFGyC6c1gOcfg9wfA80Qj7g8B7g9HuCww+haIR8wf49wOn3Cvv9k8tGyC/s7gFOv3fY3QONkH+v9MBWqB7PeqDn9FcIT//kcitUn6kHOu/T/xfWzlQy3dEHhwAAAABJRU5ErkJggg==">
</a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-209-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-199-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 dev`
3. `blitz start`
4. View your baby app at http://localhost:3000
<br><br>
@@ -149,9 +149,11 @@ Your financial contributions help ensure Blitz continues to be developed and mai
### 🏆 Gold Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="300px">
<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>
</div>
### 💎 Diamond Sponsors
@@ -487,18 +489,6 @@ 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>

View File

@@ -20,7 +20,7 @@ blitz prisma migrate dev --preview-feature
3. Start the dev server
```
blitz dev
blitz start
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -1,9 +1,8 @@
import {passportAuth, PublicData} from "blitz"
import {passportAuth} from "blitz"
import db from "db"
import {Strategy as TwitterStrategy} from "passport-twitter"
import {Strategy as GitHubStrategy} from "passport-github2"
import {Strategy as Auth0Strategy} from "passport-auth0"
import {Role} from "types"
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
@@ -68,12 +67,7 @@ export default passportAuth({
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/github/callback"
: "http://localhost:3000/api/auth/github/callback",
},
async function (
_token: string,
_tokenSecret: string,
profile: any,
done: (err: Error | null, data?: {publicData: PublicData}) => void,
) {
async function (_token, _tokenSecret, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
@@ -92,7 +86,7 @@ export default passportAuth({
const publicData = {
userId: user.id,
roles: [user.role as Role],
roles: [user.role],
source: "github",
githubUsername: profile.username,
}
@@ -112,7 +106,7 @@ export default passportAuth({
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/auth0/callback"
: "http://localhost:3000/api/auth/auth0/callback",
},
async function (_token, _tokenSecret, _extraParams, profile, done) {
async function (_token, _tokenSecret, extraParams, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {

View File

@@ -1,8 +1,7 @@
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"
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"
type LoginFormProps = {
onSuccess?: () => void
@@ -10,19 +9,17 @@ type LoginFormProps = {
export const LoginForm = (props: LoginFormProps) => {
const [loginMutation] = useMutation(login)
return (
<div>
<h1>Login</h1>
<Form
submitText="Login"
schema={Login}
initialValues={{email: "", password: ""}}
schema={LoginInput}
initialValues={{email: undefined, password: undefined}}
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"}
@@ -37,13 +34,7 @@ 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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import {resolver, SecurePassword, AuthenticationError} from "blitz"
import db from "db"
import {Login} from "../validations"
import {Role} from "types"
import * as z from "zod"
export const authenticateUser = async (email: string, password: string) => {
const user = await db.user.findFirst({where: {email}})
@@ -19,11 +18,16 @@ export const authenticateUser = async (email: string, password: string) => {
return rest
}
export default resolver.pipe(resolver.zod(Login), async ({email, password}, ctx) => {
export const LoginInput = z.object({
email: z.string().email(),
password: z.string(),
})
export default resolver.pipe(resolver.zod(LoginInput), async ({email, password}, {session}) => {
// This throws an error if credentials are invalid
const user = await authenticateUser(email, password)
await ctx.session.$create({userId: user.id, roles: [user.role as Role]})
await session.$create({userId: user.id, roles: [user.role]})
return user
})

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,20 @@
import {resolver, SecurePassword} from "blitz"
import db from "db"
import {Signup} from "app/auth/validations"
import {Role} from "types"
import * as z from "zod"
export default resolver.pipe(resolver.zod(Signup), async ({email, password}, ctx) => {
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}) => {
const hashedPassword = await SecurePassword.hash(password)
const user = await db.user.create({
data: {email: email.toLowerCase(), hashedPassword, role: "user"},
data: {email, hashedPassword, role: "user"},
select: {id: true, name: true, email: true, role: true},
})
await ctx.session.$create({userId: user.id, roles: [user.role as Role]})
await session.$create({userId: user.id, roles: [user.role]})
return user
})

View File

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

View File

@@ -1,18 +1,21 @@
import React from "react"
import {useRouter, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import {Head, useRouter, BlitzPage} from "blitz"
import {LoginForm} from "app/auth/components/LoginForm"
const LoginPage: BlitzPage = () => {
const SignupPage: BlitzPage = () => {
const router = useRouter()
return (
<div>
<LoginForm onSuccess={() => router.push("/")} />
</div>
<>
<Head>
<title>Login</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div>
<LoginForm onSuccess={() => router.push("/")} />
</div>
</>
)
}
LoginPage.getLayout = (page) => <Layout title="Log In">{page}</Layout>
export default LoginPage
export default SignupPage

View File

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

View File

@@ -1,19 +1,53 @@
import {Head, useRouter, BlitzPage, useMutation} from "blitz"
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"
import {Form, FORM_ERROR} from "app/components/Form"
import {LabeledTextField} from "app/components/LabeledTextField"
import signup, {SignupInput} from "app/auth/mutations/signup"
const SignupPage: BlitzPage = () => {
const router = useRouter()
const [signupMutation] = useMutation(signup)
return (
<div>
<SignupForm onSuccess={() => router.push("/")} />
</div>
<>
<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>
</>
)
}
SignupPage.getLayout = (page) => <Layout title="Sign Up">{page}</Layout>
export default SignupPage

View File

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

View File

@@ -1,18 +1,17 @@
import React, {ReactNode, PropsWithoutRef} from "react"
import {ReactNode, PropsWithoutRef} from "react"
import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form"
import * as z from "zod"
export {FORM_ERROR} from "final-form"
export interface FormProps<S extends z.ZodType<any, any>>
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
type FormProps<S extends z.ZodType<any, any>> = {
/** 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,

View File

@@ -1,4 +1,4 @@
import React, {PropsWithoutRef} from "react"
import {forwardRef, PropsWithoutRef} from "react"
import {useField} from "react-final-form"
export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
@@ -11,16 +11,12 @@ export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElem
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
}
export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFieldProps>(
export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
({name, label, outerProps, ...props}, ref) => {
const {
input,
meta: {touched, error, submitError, submitting},
} = useField(name, {
parse: props.type === "number" ? Number : undefined,
})
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
} = useField(name)
return (
<div {...outerProps}>
@@ -29,9 +25,9 @@ export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFi
<input {...input} disabled={submitting} {...props} ref={ref} />
</label>
{touched && normalizedError && (
{touched && (error || submitError) && (
<div role="alert" style={{color: "red"}}>
{normalizedError}
{error || submitError}
</div>
)}

View File

@@ -2,7 +2,7 @@ import {useSession, useRouter, useMutation, Head} from "blitz"
import logout from "app/auth/mutations/logout"
export default function Layout({title, children}: {title?: string; children: React.ReactNode}) {
const session = useSession({suspense: false})
const session = useSession()
const router = useRouter()
const [logoutMutation] = useMutation(logout)
return (

View File

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

View File

@@ -1,20 +1,14 @@
import {
withBlitzAppRoot,
AppProps,
ErrorComponent,
useRouter,
AuthenticationError,
AuthorizationError,
ErrorFallbackProps,
} from "blitz"
import {AppProps, ErrorComponent, useRouter, AuthenticationError, AuthorizationError} from "blitz"
import {ErrorBoundary} from "react-error-boundary"
import {queryCache} from "react-query"
import LoginForm from "app/auth/components/LoginForm"
export default function App({Component, pageProps}: AppProps) {
const getLayout = Component.getLayout || ((page) => page)
const router = useRouter()
if (typeof window !== "undefined") {
window["DEBUG_BLITZ"] = 1
}
export default function App({Component, pageProps}: AppProps) {
const router = useRouter()
return (
<ErrorBoundary
FallbackComponent={RootErrorFallback}
@@ -25,12 +19,12 @@ export default function App({Component, pageProps}: AppProps) {
queryCache.resetErrorBoundaries()
}}
>
{getLayout(<Component {...pageProps} />)}
<Component {...pageProps} />
</ErrorBoundary>
)
}
function RootErrorFallback({error, resetErrorBoundary}: ErrorFallbackProps) {
function RootErrorFallback({error, resetErrorBoundary}) {
if (error instanceof AuthenticationError) {
return <LoginForm onSuccess={resetErrorBoundary} />
} else if (error instanceof AuthorizationError) {

View File

@@ -1,27 +1,20 @@
import {render} from "test/utils"
import Home from "./index"
jest.mock("blitz", () => ({
...jest.requireActual("blitz")!,
useQuery: () => [
{
id: 1,
name: "User",
email: "user@email.com",
role: "user",
},
],
}))
import Home from "./index"
import {useCurrentUser} from "app/hooks/useCurrentUser"
jest.mock("app/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",
email: "user@email.com",
role: "user",
})
const {getByText} = render(<Home />)
const element = getByText(/powered by blitz/i)
// @ts-ignore
expect(element).toBeInTheDocument()
})

View File

@@ -1,21 +1,30 @@
import {Suspense} from "react"
import {Head, Link, useSession, useRouterQuery, useMutation, invoke, useQuery} from "blitz"
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 getUsers from "app/users/queries/getUsers"
const CurrentUserInfo = () => {
const session = useSession()
const [currentUser] = useQuery(getUser, {where: {id: session.userId!}})
const currentUser = useCurrentUser()
return <pre>{JSON.stringify(currentUser, null, 2)}</pre>
}
// const Users = () => {
// const [users] = useQuery(getUsers, {})
//
// return <pre style={{maxWidth: "30rem"}}>{JSON.stringify(users, null, 2)}</pre>
// }
const UserStuff = () => {
const session = useSession()
const query = useRouterQuery()
const [trackViewMutation] = useMutation(trackView)
if (session.isLoading) return <div>Loading...</div>
return (
<div>
{!session.userId && (
@@ -39,6 +48,11 @@ const UserStuff = () => {
<Suspense fallback="Loading...">
<CurrentUserInfo />
</Suspense>
{/*
<Suspense fallback="Loading...">
<Users />
</Suspense>
*/}
<button
onClick={async () => {
try {
@@ -80,9 +94,7 @@ const Home = () => (
<img src="/logo.png" alt="blitz.js" />
</div>
<Suspense fallback={"Loading..."}>
<UserStuff />
</Suspense>
<UserStuff />
</main>
<footer>

View File

@@ -1,5 +1,5 @@
import {Suspense} from "react"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage, useMutation} from "blitz"
import {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,39 +7,30 @@ import deleteProject from "app/projects/mutations/deleteProject"
export const Project = () => {
const router = useRouter()
const projectId = useParam("projectId", "number")
const [deleteProjectMutation, {isSuccess}] = useMutation(deleteProject)
const [project] = useQuery(getProject, {id: projectId}, {enabled: !isSuccess})
if (!project) return null
const [project] = useQuery(getProject, {where: {id: projectId}})
const [deleteProjectMutation] = useMutation(deleteProject)
return (
<>
<Head>
<title>Project {project.id}</title>
</Head>
<div>
<h1>Project {project.id}</h1>
<pre>{JSON.stringify(project, null, 2)}</pre>
<div>
<h1>Project {project.id}</h1>
<pre>{JSON.stringify(project, null, 2)}</pre>
<Link href={`/projects/${project.id}/edit`}>
<a>Edit</a>
</Link>
<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>
</>
<button
type="button"
onClick={async () => {
if (window.confirm("This will be deleted")) {
await deleteProjectMutation({where: {id: project.id}})
router.push("/projects")
}
}}
>
Delete
</button>
</div>
)
}
@@ -59,6 +50,6 @@ const ShowProjectPage: BlitzPage = () => {
)
}
ShowProjectPage.getLayout = (page) => <Layout>{page}</Layout>
ShowProjectPage.getLayout = (page) => <Layout title={"Project"}>{page}</Layout>
export default ShowProjectPage

View File

@@ -1,51 +1,39 @@
import {Suspense} from "react"
import {Head, Link, useRouter, useQuery, useMutation, useParam, BlitzPage} from "blitz"
import {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, FORM_ERROR} from "app/projects/components/ProjectForm"
import ProjectForm from "app/projects/components/ProjectForm"
export const EditProject = () => {
const router = useRouter()
const projectId = useParam("projectId", "number")
const [project, {setQueryData}] = useQuery(getProject, {id: projectId})
const [project, {setQueryData}] = useQuery(getProject, {where: {id: projectId}})
const [updateProjectMutation] = useMutation(updateProject)
return (
<>
<Head>
<title>Edit Project {project.id}</title>
</Head>
<div>
<h1>Edit Project {project.id}</h1>
<pre>{JSON.stringify(project)}</pre>
<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>
</>
<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>
)
}
@@ -65,6 +53,6 @@ const EditProjectPage: BlitzPage = () => {
)
}
EditProjectPage.getLayout = (page) => <Layout>{page}</Layout>
EditProjectPage.getLayout = (page) => <Layout title={"Edit Project"}>{page}</Layout>
export default EditProjectPage

View File

@@ -1,5 +1,5 @@
import {Suspense} from "react"
import {Head, Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
import {Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import getProjects from "app/projects/queries/getProjects"
@@ -41,26 +41,20 @@ export const ProjectsList = () => {
const ProjectsPage: BlitzPage = () => {
return (
<>
<Head>
<title>Projects</title>
</Head>
<div>
<p>
<Link href="/projects/new">
<a>Create Project</a>
</Link>
</p>
<div>
<p>
<Link href="/projects/new">
<a>Create Project</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<ProjectsList />
</Suspense>
</div>
</>
<Suspense fallback={<div>Loading...</div>}>
<ProjectsList />
</Suspense>
</div>
)
}
ProjectsPage.getLayout = (page) => <Layout>{page}</Layout>
ProjectsPage.getLayout = (page) => <Layout title={"Projects"}>{page}</Layout>
export default ProjectsPage

View File

@@ -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, FORM_ERROR} from "app/projects/components/ProjectForm"
import ProjectForm from "app/projects/components/ProjectForm"
const NewProjectPage: BlitzPage = () => {
const router = useRouter()
@@ -12,21 +12,14 @@ const NewProjectPage: BlitzPage = () => {
<h1>Create New Project</h1>
<ProjectForm
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) => {
initialValues={{}}
onSubmit={async () => {
try {
const project = await createProjectMutation(values)
const project = await createProjectMutation({name: "MyName"})
alert("Success!" + JSON.stringify(project))
router.push(`/projects/${project.id}`)
} catch (error) {
console.error(error)
return {
[FORM_ERROR]: error.toString(),
}
alert("Error creating project " + JSON.stringify(error, null, 2))
}
}}
/>

View File

@@ -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/blitz/db.js")
path.resolve(".next/__db.js")
// End anti-tree-shaking
const session = await getSessionContext(req, res)

View File

@@ -1,12 +1,23 @@
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"
import React from "react"
export function ProjectForm<S extends z.ZodType<any, any>>(props: FormProps<S>) {
type ProjectFormProps = {
initialValues: any
onSubmit: React.FormEventHandler<HTMLFormElement>
}
const ProjectForm = ({initialValues, onSubmit}: ProjectFormProps) => {
return (
<Form<S> {...props}>
<LabeledTextField name="name" label="Name" placeholder="Name" />
</Form>
<form
onSubmit={(event) => {
event.preventDefault()
onSubmit(event)
}}
>
<div>Put your form fields here. But for now, just click submit</div>
<div>{JSON.stringify(initialValues)}</div>
<button>Submit</button>
</form>
)
}
export default ProjectForm

View File

@@ -2,15 +2,23 @@ import {resolver} from "blitz"
import db from "db"
import * as z from "zod"
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 const CreateProject = z.object({
name: z.string(),
dueDate: z.date().optional(),
})
export default resolver.pipe(
resolver.zod(CreateProject),
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
},
)

View File

@@ -1,16 +1,12 @@
import {resolver} from "blitz"
import db from "db"
import * as z from "zod"
import {Ctx} from "blitz"
import db, {Prisma} from "db"
const DeleteProject = z
.object({
id: z.number(),
})
.nonstrict()
type DeleteProjectInput = Pick<Prisma.ProjectDeleteArgs, "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}})
export default async function deleteProject({where}: DeleteProjectInput, ctx: Ctx) {
ctx.session.$authorize()
const project = await db.project.delete({where})
return project
})
}

View File

@@ -1,21 +1,12 @@
import {resolver} from "blitz"
import db from "db"
import * as z from "zod"
import {Ctx} from "blitz"
import db, {Prisma} from "db"
const UpdateProject = z
.object({
id: z.number(),
name: z.string(),
})
.nonstrict()
type UpdateProjectInput = Pick<Prisma.ProjectUpdateArgs, "where" | "data">
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})
export default async function updateProject({where, data}: UpdateProjectInput, ctx: Ctx) {
ctx.session.$authorize()
return project
},
)
const project = await db.project.update({where, data})
return project
}

View File

@@ -1,17 +1,14 @@
import {resolver, NotFoundError} from "blitz"
import db from "db"
import * as z from "zod"
import {Ctx, NotFoundError} from "blitz"
import db, {Prisma} from "db"
const GetProject = z.object({
// This accepts type of undefined, but is required at runtime
id: z.number().optional().refine(Boolean, "Required"),
})
type GetProjectInput = Pick<Prisma.ProjectFindFirstArgs, "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}})
export default async function getProject({where}: GetProjectInput, ctx: Ctx) {
ctx.session.$authorize()
const project = await db.project.findFirst({where})
if (!project) throw new NotFoundError()
return project
})
}

View File

@@ -1,25 +1,29 @@
import {paginate, resolver} from "blitz"
import {Ctx} from "blitz"
import db, {Prisma} from "db"
interface GetProjectsInput
extends Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take"> {}
type GetProjectsInput = Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take">
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}),
})
export default async function getProjects(
{where, orderBy, skip = 0, take}: GetProjectsInput,
ctx: Ctx,
) {
ctx.session.$authorize()
return {
projects,
nextPage,
hasMore,
count,
}
},
)
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,
}
}

View File

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

View File

@@ -6,7 +6,7 @@ type GetUserInput = {
}
export default async function getUser({where}: GetUserInput, ctx: Ctx) {
if (!ctx.session.userId) return null
ctx.session.$authorize()
const user = await db.user.findFirst({where})

View File

@@ -7,14 +7,14 @@ module.exports = withBundleAnalyzer({
middleware: [
sessionMiddleware({
isAuthorized: simpleRolesIsAuthorized,
// sessionExpiryMinutes: 4,
sessionExpiryMinutes: 4,
}),
],
log: {
// level: "trace",
},
experimental: {
isomorphicResolverImports: false,
isomorphicResolverImports: true,
},
/*
webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => {

View File

@@ -29,7 +29,6 @@ describe("index page", () => {
cy.signup(user)
cy.wait(500)
cy.contains("button", "Logout").click()
cy.contains("a", /login/i).click()
@@ -38,7 +37,6 @@ describe("index page", () => {
cy.contains("button", /login/i).click()
cy.location("pathname").should("equal", "/")
cy.wait(500)
cy.contains("button", "Logout")
})

View File

@@ -1,7 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es5"],
"types": ["cypress"]
}
}

View File

@@ -1,15 +0,0 @@
-- 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");

View File

@@ -20,20 +20,18 @@ generator client {
// --------------------------------------
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
email String @unique
email String @unique
hashedPassword String?
role String @default("user")
sessions Session[]
tokens Token[]
role String @default("user")
sessions Session[]
}
model Session {
id Int @id @default(autoincrement())
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime?
@@ -46,25 +44,10 @@ 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 @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
dueDate DateTime?
}

View File

@@ -1,45 +0,0 @@
/* 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)
}
},
}
}

View File

@@ -1,18 +1,17 @@
{
"name": "@examples/auth",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"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": "cross-env NODE_ENV=test blitz prisma migrate deploy --preview-feature && blitz build && cross-env NODE_ENV=test blitz start -p 3099",
"test:server": "blitz prisma migrate deploy --preview-feature && blitz build && blitz start --production -p 3099",
"test:e2e": "cross-env NODE_ENV=test start-server-and-test test:server http://localhost:3099 cy:run"
},
"browserslist": [
@@ -39,13 +38,13 @@
]
},
"dependencies": {
"@prisma/client": "2.16.0",
"blitz": "0.30.0-canary.7",
"@prisma/cli": "2.15.0",
"@prisma/client": "2.15.0",
"blitz": "0.30.0-canary.3",
"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",
@@ -54,22 +53,20 @@
},
"devDependencies": {
"@cypress/skip-test": "2.6.0",
"@next/bundle-analyzer": "^10.0.6",
"@next/bundle-analyzer": "^10.0.5",
"@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.18.0",
"husky": "4.3.8",
"eslint": "7.17.0",
"husky": "4.3.7",
"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"
},

View File

@@ -1,4 +1,2 @@
// 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

View File

@@ -1,4 +1,3 @@
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"
@@ -14,6 +13,14 @@ 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()
// --------------------------------------------------
@@ -80,11 +87,3 @@ 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>}

View File

@@ -1,11 +1,11 @@
{
"compilerOptions": {
"strict": true,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"baseUrl": "./",
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
@@ -16,6 +16,6 @@
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": ["node_modules", "cypress"],
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View File

@@ -1,18 +1,16 @@
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: SimpleRolesIsAuthorized<Role>
isAuthorized: typeof simpleRolesIsAuthorized
PublicData: {
userId: User["id"]
roles: Role[]
roles: string[]
views?: number
}
}

View File

@@ -13,10 +13,10 @@
3. Start the dev server
```sh
blitz dev
blitz start
// Or if you want hot-reloading of server.js, use:
yarn dev
yarn start
```
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
blitz start --production
```

View File

@@ -84,7 +84,7 @@ const Home: BlitzPage = () => {
<code>Ctrl + c</code>
</pre>
<pre>
<code>blitz dev</code>
<code>blitz start</code>
</pre>
<p>
and go to{" "}

View File

@@ -1,10 +1,9 @@
{
"name": "@examples/custom-server",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"scripts": {
"dev": "nodemon --watch server.js --exec 'blitz dev'",
"start": "nodemon --watch server.js --exec 'blitz start'",
"build": "blitz build",
"start": "blitz start",
"studio": "blitz prisma studio",
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
"test-watch": "jest --watch",
@@ -12,7 +11,7 @@
"cy-run": "cypress run",
"test:migrate": "prisma generate && blitz prisma migrate deploy --preview-feature",
"test:jest": "jest",
"test-server": "blitz build && blitz start",
"test-server": "blitz build && blitz start --production",
"test:e2e": "cross-env NODE_ENV=test PORT=3099 start-server-and-test test-server http://localhost:3099 cy-run",
"test": "run-s test:*"
},
@@ -40,12 +39,12 @@
]
},
"dependencies": {
"@prisma/client": "2.16.0",
"blitz": "0.30.0-canary.7",
"@prisma/cli": "2.15.0",
"@prisma/client": "2.15.0",
"blitz": "0.30.0-canary.3",
"final-form": "4.20.1",
"prisma": "2.16.0",
"react": "0.0.0-experimental-3310209d0",
"react-dom": "0.0.0-experimental-3310209d0",
"react": "0.0.0-experimental-4ead6b530",
"react-dom": "0.0.0-experimental-4ead6b530",
"react-error-boundary": "2.3.2",
"react-final-form": "6.5.2",
"secure-password": "4.0.0",
@@ -54,7 +53,7 @@
},
"devDependencies": {
"@cypress/skip-test": "2.6.0",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/jest-dom": "5.11.6",
"@testing-library/react": "11.2.3",
"@testing-library/react-hooks": "4.0.1",
"@types/jest": "26.0.20",
@@ -64,7 +63,7 @@
"@typescript-eslint/parser": "4.12.0",
"babel-eslint": "10.1.0",
"cypress": "6.2.1",
"eslint": "7.18.0",
"eslint": "7.17.0",
"eslint-config-react-app": "5.2.1",
"eslint-plugin-cypress": "2.11.1",
"eslint-plugin-flowtype": "5.2.0",
@@ -72,7 +71,7 @@
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-react": "7.21.5",
"eslint-plugin-react-hooks": "4.2.0",
"husky": "4.3.8",
"husky": "4.3.7",
"jest": "26.6.3",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-watch-typeahead": "0.6.1",
@@ -83,7 +82,7 @@
"pretty-quick": "3.1.0",
"react-test-renderer": "16.14.0",
"start-server-and-test": "1.11.2",
"ts-jest": "26.5.0"
"ts-jest": "26.4.4"
},
"private": true
}

View File

@@ -31,7 +31,7 @@ FAUNA_SECRET=YOUR_AUTH_KEY
2. Start the dev server
```
yarn blitz dev
yarn blitz start
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -1,11 +1,10 @@
{
"name": "@examples/fauna",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"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\""
},
@@ -28,9 +27,9 @@
]
},
"dependencies": {
"blitz": "0.30.0-canary.7",
"blitz": "0.30.0-canary.3",
"final-form": "4.20.1",
"graphql": "15.5.0",
"graphql": "15.4.0",
"graphql-request": "3.4.0",
"react": "0.0.0-experimental-3310209d0",
"react-dom": "0.0.0-experimental-3310209d0",
@@ -40,7 +39,7 @@
"zod": "1.11.11"
},
"devDependencies": {
"@testing-library/jest-dom": "5.11.9",
"@testing-library/jest-dom": "5.11.8",
"@testing-library/react": "11.2.3",
"@testing-library/react-hooks": "4.0.1",
"@types/jest": "26.0.20",
@@ -49,14 +48,14 @@
"@typescript-eslint/eslint-plugin": "4.12.0",
"@typescript-eslint/parser": "4.12.0",
"babel-eslint": "10.1.0",
"eslint": "7.18.0",
"eslint": "7.17.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.8",
"husky": "4.3.7",
"jest": "26.6.3",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-watch-typeahead": "0.6.1",
@@ -64,7 +63,7 @@
"prettier": "2.2.1",
"pretty-quick": "3.1.0",
"start-server-and-test": "1.11.7",
"ts-jest": "26.5.0",
"ts-jest": "26.4.4",
"typescript": "4.1.3"
},
"private": true

View File

@@ -14,7 +14,7 @@ model Project {
3. Start the dev server
```
blitz dev
blitz start
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -1,10 +1,9 @@
{
"name": "no-prisma",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"scripts": {
"dev": "blitz dev",
"build": "blitz build",
"start": "blitz start",
"build": "blitz build",
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
"test": "echo \"No tests yet\""
},
@@ -27,7 +26,7 @@
]
},
"dependencies": {
"blitz": "0.30.0-canary.7",
"blitz": "0.30.0-canary.3",
"knex": "0.21.16",
"react": "0.0.0-experimental-3310209d0",
"react-dom": "0.0.0-experimental-3310209d0",
@@ -38,14 +37,14 @@
"@typescript-eslint/eslint-plugin": "4.12.0",
"@typescript-eslint/parser": "4.12.0",
"babel-eslint": "10.1.0",
"eslint": "7.18.0",
"eslint": "7.17.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.8",
"husky": "4.3.7",
"lint-staged": "10.5.3",
"prettier": "2.2.1",
"pretty-quick": "3.1.0",

View File

@@ -20,7 +20,7 @@ blitz prisma migrate dev --preview-feature
3. Start the dev server
```
blitz dev
blitz start
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -1,10 +1,9 @@
{
"name": "@examples/plain-js",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"scripts": {
"dev": "blitz dev",
"build": "blitz prisma migrate deploy --preview-feature && blitz build",
"start": "blitz start",
"build": "blitz prisma migrate deploy --preview-feature && blitz build",
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
"test": "echo \"DISABLED\""
},
@@ -30,9 +29,9 @@
]
},
"dependencies": {
"@prisma/client": "2.16.0",
"blitz": "0.30.0-canary.7",
"prisma": "2.16.0",
"@prisma/cli": "2.15.0",
"@prisma/client": "2.15.0",
"blitz": "0.30.0-canary.3",
"react": "0.0.0-experimental-3310209d0",
"react-dom": "0.0.0-experimental-3310209d0"
},
@@ -40,14 +39,14 @@
"@typescript-eslint/eslint-plugin": "4.12.0",
"@typescript-eslint/parser": "4.12.0",
"babel-eslint": "10.1.0",
"eslint": "7.18.0",
"eslint": "7.17.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.8",
"husky": "4.3.7",
"lint-staged": "10.5.3",
"prettier": "2.2.1",
"pretty-quick": "3.1.0",

View File

@@ -9,7 +9,7 @@ yarn blitz prisma migrate dev --preview-feature
2. Start the dev server
```
yarn blitz dev
yarn blitz start
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -16,7 +16,7 @@ export const getStaticProps = async () => {
const Page: BlitzPage<InferGetStaticPropsType<typeof getStaticProps>> = function ({products}) {
return (
<div>
<h1>First 100 Products</h1>
<h1>Products</h1>
<div id="products">
{products.map((product) => (
<p key={product.id}>

View File

@@ -1,4 +1,4 @@
import {Middleware, paginate} from "blitz"
import {Middleware} from "blitz"
import db, {Prisma, Product} from "db"
import {sum} from "lodash"
@@ -10,12 +10,15 @@ export function averagePrice(products: Product[]) {
type GetProductsInput = {
where?: Prisma.ProductFindManyArgs["where"]
orderBy?: Prisma.ProductFindManyArgs["orderBy"]
skip?: number
take?: number
skip?: Prisma.ProductFindManyArgs["skip"]
cursor?: Prisma.ProductFindManyArgs["cursor"]
take?: Prisma.ProductFindManyArgs["take"]
// Only available if a model relationship exists
// include?: Prisma.ProductFindManyArgs['include']
}
export default async function getProducts(
{where, orderBy, skip = 0, take = 100}: GetProductsInput,
{where, orderBy, skip = 0, cursor, take}: GetProductsInput,
ctx: Record<any, unknown> = {},
) {
if (ctx.referer) {
@@ -24,18 +27,22 @@ export default async function getProducts(
console.log("this line should not be included in the frontend bundle")
const {items: products, hasMore, nextPage, count} = await paginate({
const products = await db.product.findMany({
where,
orderBy,
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,
hasMore,
nextPage,
count,
hasMore,
}
}

View File

@@ -1,9 +0,0 @@
-- 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
);

View File

@@ -1,2 +0,0 @@
# Please do not edit this file manually
provider = "sqlite"

View File

@@ -38,15 +38,4 @@ 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
}

View File

@@ -1,12 +1,12 @@
{
"name": "@examples/store",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"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 -p 3099",
"test:server": "prisma generate && blitz prisma migrate deploy --preview-feature && blitz db seed && blitz build && blitz start --production -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/client": "2.16.0",
"blitz": "0.30.0-canary.7",
"@prisma/cli": "2.15.0",
"@prisma/client": "2.15.0",
"blitz": "0.30.0-canary.3",
"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.5",
"sass": "1.32.0",
"start-server-and-test": "1.11.7"
}
}

View File

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

View File

@@ -1,5 +1,5 @@
{
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"packages": ["packages/*"],
"npmClient": "yarn",
"useWorkspaces": true,

View File

@@ -39,14 +39,12 @@
"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",
"ts-jest": "26.5.0"
"typescript": "4.1.3"
},
"devDependencies": {
"@rollup/pluginutils": "4.1.0",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/jest-dom": "5.11.8",
"@testing-library/react": "11.2.3",
"@testing-library/react-hooks": "4.0.1",
"@types/b64-lite": "1.3.0",
@@ -64,14 +62,13 @@
"@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.168",
"@types/lodash": "4.14.166",
"@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.22",
"@types/node": "14.14.20",
"@types/node-fetch": "2.5.8",
"@types/parallel-transform": "1.1.0",
"@types/passport": "1.0.5",
@@ -89,6 +86,7 @@
"@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",
@@ -98,11 +96,10 @@
"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.1",
"delay": "4.4.0",
"directory-tree": "2.2.5",
"eslint": "7.18.0",
"eslint": "7.17.0",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-es": "4.1.0",
"eslint-plugin-es5": "1.5.0",
@@ -113,21 +110,20 @@
"eslint-plugin-react": "7.22.0",
"eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"eslint-plugin-unicorn": "26.0.1",
"husky": "4.3.8",
"@size-limit/preset-small-lib": "4.9.2",
"size-limit": "4.9.2",
"eslint-plugin-unicorn": "25.0.1",
"husky": "4.3.7",
"@size-limit/preset-small-lib": "4.9.1",
"size-limit": "4.9.1",
"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.6",
"nock": "13.0.5",
"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",
@@ -142,13 +138,13 @@
"semver": "7.3.4",
"stdout-stderr": "0.1.13",
"test-listen": "1.1.0",
"ts-jest": "26.5.0",
"ts-jest": "26.4.4",
"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": "10.0.0"
"eslint_d": "9.1.2"
},
"husky": {
"hooks": {

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/babel-preset",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",

View File

@@ -1,22 +0,0 @@
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;

View File

@@ -1,9 +1,7 @@
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'), AddBlitzAppRoot],
plugins: [require('babel-plugin-superjson-next')],
};
}

View File

@@ -1,9 +0,0 @@
declare module '@babel/helper-module-imports' {
import { NodePath, types } from '@babel/core';
function addNamed(
path: NodePath,
named: string,
source: string
): types.Identifier;
}

View File

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

View File

@@ -1,23 +1,22 @@
const path = require("path")
const pkgDir = require("pkg-dir")
const {pathsToModuleNameMapper} = require("ts-jest/utils")
const {getProjectRoot, resolveAliases} = require("@blitzjs/config")
const projectRoot = getProjectRoot()
const projectRoot = pkgDir.sync() || process.cwd()
const {compilerOptions} = require(path.join(projectRoot, "tsconfig"))
const common = {
module.exports = {
maxWorkers: 1,
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",
"<rootDir>/test/e2e",
"<rootDir>/cypress",
],
testPathIgnorePatterns: ["/node_modules/", "/.blitz/", "/.next/", "<rootDir>/db/migrations"],
transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$"],
transform: {
"^.+\\.(ts|tsx)$": "babel-jest",
@@ -25,9 +24,13 @@ const common = {
// This makes absolute imports work
moduleDirectories: ["node_modules", "<rootDir>"],
// Ignore the build directories
modulePathIgnorePatterns: ["<rootDir>/.blitz", "<rootDir>/.next"],
modulePathIgnorePatterns: [
"<rootDir>/.blitz",
"<rootDir>/.next",
"<rootDir>/test/e2e",
"<rootDir>/cypress",
],
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"),
@@ -41,41 +44,3 @@ const common = {
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",
],
},
],
}

View File

@@ -1 +0,0 @@
require("@testing-library/jest-dom")

View File

@@ -3,8 +3,6 @@ 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
}

View File

@@ -1,7 +1,7 @@
{
"name": "blitz",
"description": "Blitz is a Rails-like framework for monolithic, full-stack React apps — built on Next.js",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"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.7",
"@blitzjs/cli": "0.30.0-canary.7",
"@blitzjs/config": "0.30.0-canary.7",
"@blitzjs/core": "0.30.0-canary.7",
"@blitzjs/display": "0.30.0-canary.7",
"@blitzjs/generator": "0.30.0-canary.7",
"@blitzjs/installer": "0.30.0-canary.7",
"@blitzjs/server": "0.30.0-canary.7",
"@blitzjs/babel-preset": "0.30.0-canary.3",
"@blitzjs/cli": "0.30.0-canary.3",
"@blitzjs/config": "0.30.0-canary.3",
"@blitzjs/core": "0.30.0-canary.3",
"@blitzjs/display": "0.30.0-canary.3",
"@blitzjs/generator": "0.30.0-canary.3",
"@blitzjs/installer": "0.30.0-canary.3",
"@blitzjs/server": "0.30.0-canary.3",
"@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.7",
"eslint-config-blitz": "0.30.0-canary.3",
"jest": "^26.6.3",
"jest-environment-jsdom-fourteen": "^1.0.1",
"jest-watch-typeahead": "^0.6.1",

View File

@@ -14,8 +14,6 @@ const common = {
"next",
"fs",
"path",
"os",
"tty",
],
plugins: [
json(),

View File

@@ -93,7 +93,7 @@ async function printEnvInfo() {
{
System: ["OS", "CPU", "Memory", "Shell"],
Binaries: ["Node", "Yarn", "npm", "Watchman"],
npmPackages: ["blitz", "typescript", "react", "react-dom", "prisma", "@prisma/client"],
npmPackages: ["blitz", "typescript", "react", "react-dom", "@prisma/cli", "@prisma/client"],
},
{showNotFound: true},
)

View File

@@ -33,5 +33,5 @@ blitz build
### Launch the development server
```bash
blitz dev
blitz start
```

View File

@@ -1,3 +0,0 @@
{
"integrationFolder": "test/e2e/cypress/integration"
}

View File

@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

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

View File

@@ -1,25 +0,0 @@
// ***********************************************
// 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) => { ... })

View File

@@ -1,20 +0,0 @@
// ***********************************************************
// 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')

View File

@@ -1,9 +1,23 @@
module.exports = {
preset: "../../jest.config.js",
preset: "ts-jest",
testEnvironment: "node",
moduleFileExtensions: ["ts", "js", "json"],
coverageReporters: ["json", "lcov", "text", "clover"],
// 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",

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/cli",
"description": "Blitz.js CLI",
"version": "0.30.0-canary.7",
"version": "0.30.0-canary.3",
"license": "MIT",
"scripts": {
"b": "./bin/run",
@@ -14,9 +14,7 @@
"dev": "rimraf lib && tsc --watch --preserveWatchOutput",
"build": "rimraf lib && tsc",
"test": "tsdx test",
"test:watch": "jest --watch",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
"test:watch": "jest --watch"
},
"author": {
"name": "Brandon Bayer",
@@ -32,15 +30,15 @@
"/lib"
],
"dependencies": {
"@blitzjs/display": "0.30.0-canary.7",
"@blitzjs/repl": "0.30.0-canary.7",
"@blitzjs/display": "0.30.0-canary.3",
"@blitzjs/repl": "0.30.0-canary.3",
"@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.16.0",
"@salesforce/lazy-require": "0.4.0",
"@prisma/sdk": "2.15.0",
"@salesforce/lazy-require": "0.3.4",
"camelcase": "^6.2.0",
"chalk": "4.1.0",
"cross-spawn": "7.0.3",
@@ -53,7 +51,6 @@
"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",
@@ -64,13 +61,13 @@
"v8-compile-cache": "2.2.0"
},
"devDependencies": {
"@blitzjs/generator": "0.30.0-canary.7",
"@blitzjs/installer": "0.30.0-canary.7",
"@blitzjs/server": "0.30.0-canary.7",
"@blitzjs/generator": "0.30.0-canary.3",
"@blitzjs/installer": "0.30.0-canary.3",
"@blitzjs/server": "0.30.0-canary.3",
"@oclif/dev-cli": "1.26.0",
"@oclif/test": "1.2.8",
"nock": "13.0.6",
"prisma": "2.16.0",
"@prisma/cli": "2.15.0",
"nock": "13.0.5",
"stdout-stderr": "0.1.13"
},
"jest": {

View File

@@ -10,7 +10,7 @@ export function getDbName(connectionString: string): string {
async function runSeed() {
require("../utils/setup-ts-node").setupTsnode()
const projectRoot = require("@blitzjs/config").getProjectRoot()
const projectRoot = require("../utils/get-project-root").projectRoot
const seedPath = require("path").join(projectRoot, "db/seeds")
const dbPath = require("path").join(projectRoot, "db/index")

View File

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

View File

@@ -2,15 +2,12 @@ import {flags} from "@oclif/command"
import {log} from "@blitzjs/display"
import {
PageGenerator,
MutationsGenerator,
MutationGenerator,
QueriesGenerator,
FormGenerator,
ModelGenerator,
QueryGenerator,
singleCamel,
capitalize,
uncapitalize,
singlePascal,
pluralCamel,
pluralPascal,
@@ -23,18 +20,17 @@ import chalk from "chalk"
const debug = require("debug")("blitz:generate")
const getIsTypeScript = () =>
require("fs").existsSync(
require("path").join(require("@blitzjs/config").getProjectRoot(), "tsconfig.json"),
require("path").join(require("../utils/get-project-root").projectRoot, "tsconfig.json"),
)
enum ResourceType {
All = "all",
Crud = "crud",
Model = "model",
Mutations = "mutations",
Pages = "pages",
Queries = "queries",
Query = "query",
Mutations = "mutations",
Mutation = "mutation",
Resource = "resource",
}
@@ -64,20 +60,19 @@ function ModelNames(input: string = "") {
const generatorMap = {
[ResourceType.All]: [
ModelGenerator,
PageGenerator,
FormGenerator,
QueriesGenerator,
MutationsGenerator,
ModelGenerator,
MutationGenerator,
],
[ResourceType.Crud]: [MutationsGenerator, QueriesGenerator],
[ResourceType.Crud]: [MutationGenerator, QueriesGenerator],
[ResourceType.Model]: [ModelGenerator],
[ResourceType.Mutations]: [MutationGenerator],
[ResourceType.Pages]: [PageGenerator, FormGenerator],
[ResourceType.Queries]: [QueriesGenerator],
[ResourceType.Query]: [QueryGenerator],
[ResourceType.Mutations]: [MutationsGenerator],
[ResourceType.Mutation]: [MutationGenerator],
[ResourceType.Resource]: [QueriesGenerator, MutationsGenerator, ModelGenerator],
[ResourceType.Resource]: [ModelGenerator, QueriesGenerator, MutationGenerator],
}
export class Generate extends Command {
@@ -231,8 +226,7 @@ export class Generate extends Command {
parentModels: modelNames(flags.parent),
ParentModel: ModelName(flags.parent),
ParentModels: ModelNames(flags.parent),
name: uncapitalize(model),
Name: capitalize(model),
rawInput: model,
dryRun: flags["dry-run"],
context: context,
useTs: getIsTypeScript(),

View File

@@ -185,7 +185,7 @@ export class New extends Command {
)
}
postInstallSteps.push("blitz dev")
postInstallSteps.push("blitz start")
this.log("\n" + log.withBrand("Your new Blitz app is ready! Next steps:") + "\n")

View File

@@ -3,16 +3,8 @@ 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 = 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
}
const getPrismaBin = () =>
require("import-cwd")("@blitzjs/server").resolveBinAsync("@prisma/cli", "prisma")
let prismaBin: string

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