1
0
mirror of synced 2026-02-06 18:00:14 -05:00

Compare commits

...

101 Commits

Author SHA1 Message Date
Siddharth Suresh
1ecc5ca714 add changeset pre exit to fix error 2023-01-11 23:53:04 +05:30
Siddharth Suresh
80bbe8e849 Create pr-release.yml 2023-01-11 23:36:04 +05:30
Siddharth Suresh
985ce05eba update unit tests 2023-01-11 17:10:47 +05:30
Siddharth Suresh
58d7f053e6 update zod to 3.20.0 in generator 2023-01-11 13:45:18 +05:30
Siddharth Suresh
154a287aa5 pnpm lock 2023-01-11 13:32:04 +05:30
Siddharth Suresh
558d9132c5 Merge branch 'port-subtemplate' of https://github.com/blitz-js/blitz into port-subtemplate 2023-01-11 13:29:30 +05:30
Siddharth Suresh
cc13affe9c add new zod datetime
Co-authored-by: Tobias <tobias@fixmycity.de>
2023-01-11 13:26:54 +05:30
Siddharth Suresh
ad3e202ef0 Merge branch 'main' into port-subtemplate 2023-01-11 13:26:27 +05:30
Tobias
14882d43fe Update package Zod to 3.20.2 (#4039)
* Update package zod to 3.20.2

* `pnpm changeset` Update zod to 3.20.2 (was 3.19.1).

Co-authored-by: Brandon Bayer <b@bayer.ws>
Co-authored-by: Siddharth Suresh <siddh.suresh@gmail.com>
2023-01-11 11:32:02 +05:30
Tobias
989691ec87 Use src instead of app folder for blitz generate custom-template (#4033)
* Use src instead of app in `customTemplates: "src/templates"`

* Use src instead of app in `cli/commands/generate`

* Add changeset

* remove ignored packages

Co-authored-by: Brandon Bayer <b@bayer.ws>
Co-authored-by: Siddharth Suresh <siddh.suresh@gmail.com>
2023-01-11 05:25:57 +00:00
Brandon Bayer
2703618868 change rpc route basepath includeRPC in monorepos to be one folder up (#4045)
* change rpc route basepath includeRPC in monorepos to be one folder up

* changeset

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-11 00:13:11 +00:00
Blitz.js Bot
44ddf5b423 (meta) updated @bravo-kernel contributions 2023-01-10 19:06:15 -05:00
bravo-kernel
bcef81fadc Add missing MockRouter prop in generated test utils (#4043)
Co-authored-by: Brandon Bayer <b@bayer.ws>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-10 19:06:10 -05:00
Dillon Raphael
7abfb9086a Redesign the index page for newly generated blitz projects (#3987)
* redesign index page for full app template

* pnpmlock

* iteration 2

* Add padding around blitz logo

* Fix more padding

* update padding for logo and button container

* increase max-width

* final draft

* address comments

Co-authored-by: Brandon Bayer <b@bayer.ws>
2023-01-11 00:04:54 +00:00
Siddharth Suresh
57101e9adf internal: fix playwright installation error by fixing version (#4047) 2023-01-10 18:57:30 -05:00
Siddharth Suresh
1bea6b33b0 cleanup 2023-01-10 20:42:16 +05:30
Siddharth Suresh
484853edc2 fix version 2023-01-10 20:32:46 +05:30
Siddharth Suresh
ae410d5720 with deps 2023-01-10 20:26:14 +05:30
Siddharth Suresh
6c5256b262 try another way 2023-01-10 20:20:11 +05:30
Siddharth Suresh
38d16177d5 fix version 2023-01-10 20:13:51 +05:30
Siddharth Suresh
e335061157 fix path 2023-01-10 20:07:16 +05:30
Siddharth Suresh
2cb2dd2910 fix env location 2023-01-10 20:01:48 +05:30
Siddharth Suresh
2b6bd18629 try agin 2023-01-10 19:55:49 +05:30
Siddharth Suresh
f715d7fc6f Merge branch 'port-subtemplate' of https://github.com/blitz-js/blitz into port-subtemplate 2023-01-10 19:52:48 +05:30
Siddharth Suresh
ba1087c031 Update main.yml 2023-01-10 19:52:01 +05:30
Siddharth Suresh
cb7a08aea4 fix playwright issue 2023-01-10 18:42:21 +05:30
Siddharth Suresh
384ca635de Merge branch 'main' into port-subtemplate 2023-01-10 18:25:48 +05:30
Siddharth Suresh
497b87f7fb internal: Fix playwright install issue (#4044) 2023-01-10 00:02:44 +05:30
Tobias
64a98e201d prisma/field: Guard TS warning (#4032)
The previous solution did solve the if-statement but not the line below, which still had this TS error:

```
      const relationType = Relation[_fieldName]
// ^- const _fieldName: string | undefined
//    Type 'undefined' cannot be used as an index type.ts(2538)
```

Co-authored-by: Dillon Raphael <dillon@creatorsneverdie.com>
Co-authored-by: Siddharth Suresh <siddh.suresh@gmail.com>
2023-01-09 22:08:30 +05:30
Brandon Bayer
8aa22a0b2f add currentPassword to default masked fields in the logger (#4042) 2023-01-06 17:55:07 -05:00
Siddharth Suresh
ae0488335a Merge branch 'main' into port-subtemplate 2023-01-05 17:47:57 +05:30
Brandon Bayer
145d5a02b3 fix failed localstorage access to not crash the application (#4037) 2023-01-04 18:28:31 -05:00
Siddharth Suresh
3b5bff1b56 Update main.yml 2022-12-26 21:06:22 +05:30
Siddharth Suresh
959f975c3b minor fix 2022-12-25 18:50:56 +05:30
Siddharth Suresh
b4b20d5223 minor fix 2022-12-22 22:08:02 +05:30
Siddharth Suresh
74bcc213c2 initial revert changes to log and add better types 2022-12-22 22:04:49 +05:30
Siddharth Suresh
48c3b74ae9 Merge branch 'main' into port-subtemplate 2022-12-22 18:13:03 +05:30
Siddharth Suresh
4b258b0f01 Update packages/generator/src/generators/template-builders/field-values-builder.ts
Co-authored-by: John Vandivier <vandivier_john@yahoo.com>
2022-12-22 18:12:39 +05:30
Siddharth Suresh
799b7431c4 fix pnpm lock 2022-12-19 23:05:14 +05:30
Siddharth Suresh
d8d047db7e Merge branch 'main' into port-subtemplate 2022-12-19 22:53:35 +05:30
Siddharth Suresh
34a1ffea99 Merge branch 'main' into port-subtemplate 2022-12-14 18:48:30 +05:30
Siddharth Suresh
7dddab6afe preWriteFile and postWriteFile hooks 2022-12-12 13:11:24 +05:30
Siddharth Suresh
d33acf0538 Merge branch 'main' into port-subtemplate 2022-12-10 22:16:09 +05:30
Siddharth Suresh
a2ecc9f20b fix zod version 2022-12-08 17:00:27 +05:30
Siddharth Suresh
fa0d10121d move zod to devDeps 2022-12-08 16:42:11 +05:30
Siddharth Suresh
5c930dcdb3 works without prisma 2022-12-08 16:26:38 +05:30
Siddharth Suresh
4620d00640 remove error log if no prisma schema 2022-12-08 13:42:06 +05:30
Siddharth Suresh
41cf34fea7 add required test and remove error if prisma does not exist 2022-12-08 13:40:54 +05:30
Siddharth Suresh
b8ed7b9af9 fix tests and remove custom log 2022-12-08 13:28:23 +05:30
Siddharth Suresh
9c78c5f3c7 implement suggessions 2022-12-08 13:13:46 +05:30
Siddharth Suresh
fbd9b98d9f Merge branch 'main' into port-subtemplate 2022-12-08 12:13:38 +05:30
Siddharth Suresh
b721bda375 Merge branch 'main' into port-subtemplate 2022-11-26 17:33:49 +05:30
Siddharth Suresh
4918acdf59 update to latest changes 2022-11-23 23:15:48 +05:30
Siddharth Suresh
842934cf15 Merge branch 'main' into port-subtemplate 2022-11-23 16:23:40 +05:30
Siddharth Suresh
01bf82a605 Merge branch 'main' into port-subtemplate 2022-11-09 13:09:55 +05:30
Siddharth Suresh
2e8d899f81 Update apps/toolkit-app/app/blitz-server.ts 2022-11-09 13:08:41 +05:30
Siddharth Suresh
92a1b59118 Merge branch 'main' into port-subtemplate 2022-10-31 10:48:22 +05:30
Siddharth Suresh
fbd8585a3e Merge branch 'main' into port-subtemplate 2022-10-28 10:45:20 +05:30
Siddharth Suresh
646b92a439 Merge branch 'main' into port-subtemplate 2022-10-20 15:14:21 +05:30
Siddharth Suresh
a17ea51595 Merge branch 'main' into port-subtemplate 2022-10-18 15:28:20 +05:30
Siddharth Suresh
3ac434b265 Merge branch 'main' into port-subtemplate 2022-10-17 13:14:38 +05:30
Siddharth Suresh
b3782a21b4 fix default bug for string values in #2863 2022-10-16 12:55:20 +05:30
Siddharth Suresh
c36cfb7d09 fix lock file and add react-final-form select component 2022-10-14 23:22:06 +05:30
Siddharth Suresh
b502c45d74 Merge branch 'main' into port-subtemplate 2022-10-13 11:35:28 +05:30
Siddharth Suresh
f6f5468151 Update blitz-server.ts 2022-10-12 23:36:51 +05:30
Siddharth Suresh
97b64699a3 update tests 2022-10-12 23:30:01 +05:30
Siddharth Suresh
bd9bd166ca update prop type to options for --parent or belongsTo option and add LabelSelectField 2022-10-12 23:19:29 +05:30
Siddharth Suresh
f8fffc122f fix duplicate 2022-10-12 22:08:06 +05:30
Siddharth Suresh
11c4ff3272 Merge branch 'main' into port-subtemplate 2022-10-12 22:01:50 +05:30
Siddharth Suresh
b3953c445f Update getUsers.ts 2022-10-06 19:08:04 +05:30
Siddharth Suresh
2e7cb3a062 fix type 2022-10-06 11:10:05 +05:30
Siddharth Suresh
d6b760bb06 fix --parent flag 2022-10-05 23:13:26 +05:30
Siddharth Suresh
aef3bb2efc cleanup 2022-10-05 22:53:55 +05:30
Siddharth Suresh
bf4f2b9214 initial commit adding dropdown option when using belongTo and parent flag 2022-10-05 22:45:34 +05:30
Siddharth Suresh
e49f138e1d Merge branch 'main' into port-subtemplate 2022-10-04 16:03:00 +05:30
Siddharth Suresh
caac3522ec Merge branch 'main' into port-subtemplate 2022-10-04 14:15:28 +05:30
Siddharth Suresh
56b8656826 generic prisma to work on subsequent runs 2022-10-04 00:48:39 +05:30
Siddharth Suresh
2674f5dcfb update tests 2022-10-04 00:29:07 +05:30
Siddharth Suresh
e3524b8279 make prisma import generic 2022-10-04 00:12:25 +05:30
Siddharth Suresh
49b5339e4c Merge branch 'main' into port-subtemplate 2022-10-03 23:40:34 +05:30
Siddharth Suresh
93e4c29d8a Merge branch 'main' into port-subtemplate 2022-10-03 22:27:01 +05:30
Siddharth Suresh
af3dd373a8 cleanup 2022-10-03 20:43:19 +05:30
Siddharth Suresh
08d0d1e0ca show error when structure of codegen is incorrect using zod 2022-10-03 20:39:36 +05:30
Siddharth Suresh
d7e88dece5 use latest blitz logging setup in generator 2022-10-03 19:13:50 +05:30
Siddharth Suresh
4671586454 fix pnpm-lock 2022-10-03 19:01:51 +05:30
Siddharth Suresh
0fb4439fc5 Merge branch 'main' into port-subtemplate 2022-10-03 18:56:30 +05:30
Siddharth Suresh
900c19159e fix belongsTo not working 2022-10-03 18:55:29 +05:30
Siddharth Suresh
6a07489043 add types for codegen 2022-10-03 18:42:06 +05:30
Siddharth Suresh
4459fbfe4d Update .changeset/silly-peas-work.md 2022-10-03 18:13:31 +05:30
Siddharth Suresh
8d5fa13cb2 Create silly-peas-work.md 2022-10-03 18:09:20 +05:30
Siddharth Suresh
13de89321c Merge branch 'main' into port-subtemplate 2022-10-03 18:07:49 +05:30
Siddharth Suresh
85ef684e31 cleanup 2022-10-03 18:06:32 +05:30
Siddharth Suresh
948acafe6a fix parent generation in createMutation and components, and move existing pages when --parent is called 2022-10-03 18:03:20 +05:30
Siddharth Suresh
27dcfd81e1 cleanup 2022-10-02 15:49:56 +05:30
Siddharth Suresh
20d17b7561 fix blitz genrate not updating values 2022-10-02 11:33:00 +05:30
Siddharth Suresh
4afa6b5b1b make db import generic 2022-10-01 13:54:58 +05:30
Siddharth Suresh
0effae154d remove comments 2022-10-01 00:09:37 +05:30
Siddharth Suresh
aaa0b67861 add unit tests 2022-09-30 23:22:10 +05:30
Siddharth Suresh
ed54550c46 fix unit tests 2022-09-30 23:04:05 +05:30
Siddharth Suresh
ff52627cbb read custom template values from blitz-server 2022-09-30 22:38:39 +05:30
Siddharth Suresh
fe9a423862 initial port 2022-09-30 20:34:01 +05:30
81 changed files with 2680 additions and 753 deletions

View File

@@ -2941,7 +2941,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/230500?v=4",
"profile": "https://github.com/bravo-kernel",
"contributions": [
"code"
"code",
"doc"
]
},
{

View File

@@ -0,0 +1,5 @@
---
"@blitzjs/rpc": minor
---
change rpc route basepath to be one folder higher when using includeRPC in monorepos

View File

@@ -0,0 +1,5 @@
---
"toolkit-app": patch
---
Update zod to 3.20.2 (was 3.19.1).

View File

@@ -0,0 +1,5 @@
---
"blitz": patch
---
Use `src` instead of `app` folder for `blitz generate custom-template`

View File

@@ -0,0 +1,5 @@
---
"@blitzjs/generator": patch
---
Fix missing MockRouter prop in test utils

View File

@@ -0,0 +1,6 @@
---
"blitz": patch
"@blitzjs/generator": patch
---
Multiple fields forms using templates during generation - TODO

View File

@@ -0,0 +1,5 @@
---
"@blitzjs/auth": minor
---
fix failed localStorage access to not crash the application

View File

@@ -0,0 +1,5 @@
---
"blitz": minor
---
add `currentPassword` to the default fields that are masked in the logger

View File

@@ -0,0 +1,5 @@
---
"@blitzjs/generator": minor
---
Redesign the index page for newly generated blitz projects

View File

@@ -136,12 +136,13 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
shell: bash
- name: Install playwright
run: |
npm i -g playwright
PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers npx playwright install --with-deps
pnpx playwright@1.28.0 install --with-deps
shell: bash
- name: Build

47
.github/workflows/pr-release.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: PR-Release
on:
pull_request:
types: [issue_comment]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changeset:
if: ${{ contains(github.event.pull_request.labels.*.name, 'release/next') && github.event.pull_request.merged == false && contains(github.event.comment.body,'/publish') && github.event.comment.author_association == 'MEMBER' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 16
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Version
run: |
pnpm changeset pre exit
pnpm changeset version --snapshot next-${{ github.event.pull_request.number }}
- name: Publish
run: pnpm changeset publish --tag next@${{ github.event.pull_request.number }}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Comment on PR
uses: peter-evans/create-or-update-comment@v2
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
The new version of this PR is available as next in npm:
```
npm install <package>@next${{ github.event.pull_request.number }}
```

5
.kodiak.toml Normal file
View File

@@ -0,0 +1,5 @@
# .kodiak.toml
# Minimal config. version is the only required field.
version = 1
merge.automerge_label = "0 - <(^_^)> - merge it! ✌️"
approve.auto_approve_usernames = ["flybayer", "depfu"]

View File

@@ -620,7 +620,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<tr>
<td align="center"><a href="https://muyiwa.me"><img src="https://avatars.githubusercontent.com/u/6832244?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muyiwa Olu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=muyiwaolu" title="Code">💻</a></td>
<td align="center"><a href="http://2hr.me/"><img src="https://avatars.githubusercontent.com/u/4346154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rabbi Hossain</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=rabbihossain" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bravo-kernel"><img src="https://avatars.githubusercontent.com/u/230500?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bravo-kernel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=bravo-kernel" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bravo-kernel"><img src="https://avatars.githubusercontent.com/u/230500?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bravo-kernel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=bravo-kernel" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=bravo-kernel" title="Documentation">📖</a></td>
<td align="center"><a href="https://samholmes.net"><img src="https://avatars.githubusercontent.com/u/8385528?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sam Holmes</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sam3d" title="Code">💻</a></td>
<td align="center"><a href="https://doncicuto.medium.com"><img src="https://avatars.githubusercontent.com/u/30386061?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Miguel Cabrerizo</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=doncicuto" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=doncicuto" title="Documentation">📖</a></td>
<td align="center"><a href="http://zackhobson.com/"><img src="https://avatars.githubusercontent.com/u/12092?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zack Hobson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=zenhob" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=zenhob" title="Documentation">📖</a></td>

View File

@@ -38,7 +38,7 @@
"react-dom": "18.2.0",
"react-hook-form": "7.39.1",
"ts-node": "10.9.1",
"zod": "3.19.1"
"zod": "3.20.2"
},
"devDependencies": {
"@next/bundle-analyzer": "12.0.8",

View File

@@ -38,7 +38,7 @@
"react-dom": "18.2.0",
"react-hook-form": "7.39.1",
"ts-node": "10.9.1",
"zod": "3.19.1"
"zod": "3.20.2"
},
"devDependencies": {
"@next/bundle-analyzer": "12.0.8",

View File

@@ -19,5 +19,5 @@ const { gSSP, gSP, api } = setupBlitzServer({
export { gSSP, gSP, api }
export const cliConfig: BlitzCliConfig = {
customTemplates: "app/templates",
customTemplates: "src/templates",
}

View File

@@ -0,0 +1,63 @@
import { ComponentPropsWithoutRef, forwardRef, PropsWithoutRef } from "react"
import { useFormContext } from "react-hook-form"
import { ErrorMessage } from "@hookform/error-message"
export interface LabeledSelectFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["select"]> {
/** Field name. */
name: string
/** Field label. */
label: string
/** Field type. Doesn't include radio buttons and checkboxes */
options: any[]
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
labelProps?: ComponentPropsWithoutRef<"label">
}
export const LabeledSelectField = forwardRef<HTMLSelectElement, LabeledSelectFieldProps>(
({ label, outerProps, labelProps, name, options, ...props }, ref) => {
const {
register,
formState: { isSubmitting, errors },
} = useFormContext()
return (
<div {...outerProps}>
<label {...labelProps}>
{label}
<select {...register(name)} disabled={isSubmitting} {...props}>
{options &&
options.map((value) => (
<option value={value.id} key={value.id}>
{value[name]}
</option>
))}
</select>
</label>
<ErrorMessage
render={({ message }) => (
<div role="alert" style={{ color: "red" }}>
{message}
</div>
)}
errors={errors}
name={name}
/>
<style jsx>{`
label {
display: flex;
flex-direction: column;
align-items: start;
font-size: 1rem;
}
select {
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 3px;
border: 1px solid purple;
appearance: none;
margin-top: 0.5rem;
}
`}</style>
</div>
)
}
)

View File

@@ -0,0 +1,31 @@
import { paginate } from "blitz"
import { resolver } from "@blitzjs/rpc"
import db, { Prisma } from "db"
interface GetProjectsInput
extends Pick<Prisma.UserFindManyArgs, "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: users,
hasMore,
nextPage,
count,
} = await paginate({
skip,
take,
count: () => db.user.count({ where }),
query: (paginateArgs) => db.user.findMany({ ...paginateArgs, where, orderBy }),
})
return {
users,
nextPage,
hasMore,
count,
}
}
)

View File

@@ -32,7 +32,7 @@
"react-dom": "18.2.0",
"react-hook-form": "7.39.1",
"ts-node": "10.9.1",
"zod": "3.19.1"
"zod": "3.20.2"
},
"devDependencies": {
"@next/bundle-analyzer": "12.0.8",

View File

@@ -19,5 +19,5 @@ const {gSSP, gSP, api} = setupBlitzServer({
export {gSSP, gSP, api}
export const cliConfig: BlitzCliConfig = {
customTemplates: "app/templates",
customTemplates: "src/templates",
}

View File

@@ -2,6 +2,6 @@ const {withBlitz} = require("@blitzjs/next")
module.exports = withBlitz({
blitz: {
resolverPath: "root",
includeRPCFolders: ["../no-suspense/app"],
includeRPCFolders: ["../no-suspense"],
},
})

View File

@@ -49,11 +49,16 @@ function runTests(dev = false) {
it(
"monorepo query works",
async () => {
const data = await fetchViaHTTP(appPort, "/api/rpc/queries/getNoSuspenseBasic", null, {
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
body: JSON.stringify({params: {}}),
}).then((res) => res.ok && res.json())
const data = await fetchViaHTTP(
appPort,
"/api/rpc/no-suspense/app/queries/getNoSuspenseBasic",
null,
{
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
body: JSON.stringify({params: {}}),
},
).then((res) => res.ok && res.json())
expect(data).toEqual({result: "basic-result", error: null, meta: {}})
},

View File

@@ -90,7 +90,11 @@ class PublicDataStore {
clear() {
deleteCookie(COOKIE_PUBLIC_DATA_TOKEN())
localStorage.removeItem(LOCALSTORAGE_PUBLIC_DATA_TOKEN())
try {
localStorage.removeItem(LOCALSTORAGE_PUBLIC_DATA_TOKEN())
} catch (err) {
console.error("LocalStorage is not available", err)
}
this.updateState(emptyPublicData)
}
@@ -105,12 +109,17 @@ class PublicDataStore {
}
private getToken() {
const cookieValue = readCookie(COOKIE_PUBLIC_DATA_TOKEN())
if (cookieValue) {
localStorage.setItem(LOCALSTORAGE_PUBLIC_DATA_TOKEN(), cookieValue)
return cookieValue
} else {
return localStorage.getItem(LOCALSTORAGE_PUBLIC_DATA_TOKEN())
try {
const cookieValue = readCookie(COOKIE_PUBLIC_DATA_TOKEN())
if (cookieValue) {
localStorage.setItem(LOCALSTORAGE_PUBLIC_DATA_TOKEN(), cookieValue)
return cookieValue
} else {
return localStorage.getItem(LOCALSTORAGE_PUBLIC_DATA_TOKEN())
}
} catch (err) {
console.error("LocalStorage is not available", err)
return undefined
}
}
}

View File

@@ -47,7 +47,7 @@
"typescript": "^4.8.4",
"unbuild": "0.7.6",
"watch": "1.0.2",
"zod": "3.19.1"
"zod": "3.20.2"
},
"publishConfig": {
"access": "public"

View File

@@ -68,7 +68,7 @@ export function convertPageFilePathToRoutePath({
} else if (resolverBasePath === "root") {
path = path.replace(normalize(appRoot), "")
for (const extraPath of extraRpcBasePaths) {
path = path.replace(join(normalize(appRoot), extraPath.replace("/", sep)), "")
path = path.replace(join(normalize(appRoot), extraPath.replace("/", sep), ".."), "")
}
} else {
path = path.replace(/^.*?[\\/]queries[\\/]/, "/").replace(/^.*?[\\/]mutations[\\/]/, "/")

View File

@@ -98,7 +98,7 @@
"typescript": "^4.8.4",
"unbuild": "0.7.6",
"watch": "1.0.2",
"zod": "3.19.1"
"zod": "3.20.2"
},
"publishConfig": {
"access": "public"

View File

@@ -15,6 +15,10 @@ import {
MutationsGenerator,
ModelGenerator,
QueryGenerator,
ModelName,
modelName,
ModelNames,
modelNames,
customTemplatesBlitzConfig,
} from "@blitzjs/generator"
import {log} from "../../logging"
@@ -35,24 +39,11 @@ enum ResourceType {
CustomTemplates = "custom-templates",
}
function modelName(input: string = "") {
return singleCamel(input)
}
function modelNames(input: string = "") {
return pluralCamel(input)
}
function ModelName(input: string = "") {
return singlePascal(input)
}
function ModelNames(input: string = "") {
return pluralPascal(input)
}
const createCustomTemplates = async () => {
const continuePrompt = await prompts({
type: "confirm",
name: "value",
message: `This will copy the default templates to your app/templates folder. Do you want to continue?`,
message: `This will copy the default templates to your src/templates folder. Do you want to continue?`,
})
if (!continuePrompt.value) {
process.exit(0)
@@ -61,12 +52,11 @@ const createCustomTemplates = async () => {
type: "text",
name: "value",
message: `Enter the path to save the custom templates folder`,
initial: "app/templates",
initial: "src/templates",
})
const templatesPathValue: string = templatesPath.value
const isTypeScript = await getIsTypeScript()
await customTemplatesBlitzConfig(isTypeScript, templatesPathValue, true) // to run the codemod
log.success(`🚀 Custom templates path added/updated in app/blitz-server file`)
const customTemplatesPath = require("path").join(process.cwd(), templatesPathValue)
const fsExtra = await import("fs-extra")
const blitzGeneratorPath = require.resolve("@blitzjs/generator")
@@ -286,6 +276,7 @@ const generate: CliCommand = async () => {
modelNames: modelNames(singularRootContext),
ModelName: ModelName(singularRootContext),
ModelNames: ModelNames(singularRootContext),
rawParentModelName: args["--parent"],
parentModel: modelName(selectedParent),
parentModels: modelNames(selectedParent),
ParentModel: ModelName(selectedParent),

View File

@@ -41,7 +41,7 @@ export const BlitzLogger = (settings: BlitzLoggerSettings = {}) => {
bigint: "blue",
boolean: "blue",
},
maskValuesOfKeys: ["password", "passwordConfirmation"],
maskValuesOfKeys: ["password", "passwordConfirmation", "currentPassword"],
exposeErrorCodeFrame: process.env.NODE_ENV !== "production",
...settings,
})

View File

@@ -2,6 +2,16 @@ import {UrlObject} from "url"
// Context for plugins to declaration merge stuff into
export interface Ctx {}
export interface AuthenticatedMiddlewareCtx {}
export type CodegenField = {
component: string
inputType: string
zodType: string
prismaType: string
default?: string
}
export interface RouteUrlObject extends Pick<UrlObject, "pathname" | "query" | "href"> {
pathname: string
href: string
@@ -32,9 +42,33 @@ export type ResolverConfig = {
}
export type BlitzCliConfig = {
codegen?: {
fieldTypeMap?: Record<
| "string"
| "boolean"
| "int"
| "number"
| "bigint"
| "float"
| "decimal"
| "datetime"
| "uuid"
| "json",
CodegenField
>
}
customTemplates?: string
}
export type CodegenConfig = {
templateDir?: string
fieldTypeMap?: Record<string, CodegenField>
}
export interface RouteUrlObject extends Pick<UrlObject, "pathname" | "query"> {
pathname: string
}
export const isRouteUrlObject = (x: any): x is RouteUrlObject => {
return (
typeof x === "object" &&

View File

@@ -2,7 +2,7 @@ import {BuildConfig} from "unbuild"
const config: BuildConfig = {
entries: ["./src/index"],
externals: ["react"],
externals: ["react", "zod"],
declaration: true,
rollup: {
emitCJS: true,

View File

@@ -30,6 +30,7 @@
"cross-spawn": "7.0.3",
"diff": "5.0.0",
"enquirer": "2.3.6",
"fast-glob": "3.2.12",
"fs-extra": "10.0.1",
"globby": "13.1.2",
"got": "^11.8.1",
@@ -68,7 +69,8 @@
"react": "18.2.0",
"typescript": "^4.8.4",
"unbuild": "0.6.9",
"watch": "1.0.2"
"watch": "1.0.2",
"zod": "3.20.2"
},
"publishConfig": {
"access": "public"

View File

@@ -1,10 +1,13 @@
import * as babel from "@babel/core"
// @ts-ignore TS wants types for this module but none exist
import babelTransformTypescript from "@babel/plugin-transform-typescript"
import {escapePath} from "fast-glob"
import Enquirer from "enquirer"
import {EventEmitter} from "events"
import * as fs from "fs-extra"
import j from "jscodeshift"
import {CommonTemplateValues, IBuilder} from "./generators/template-builders/builder"
import {NullBuilder} from "./generators/template-builders/null-builder"
import {create as createStore, Store} from "mem-fs"
import {create as createEditor, Editor} from "mem-fs-editor"
import * as path from "path"
@@ -14,6 +17,7 @@ import {ConflictChecker} from "./conflict-checker"
import {pipe} from "./utils/pipe"
import {readdirRecursive} from "./utils/readdir-recursive"
import prettier from "prettier"
import {log} from "./utils/log"
const debug = require("debug")("blitz:generator")
export function getProjectRootSync() {
@@ -40,7 +44,7 @@ export const customTemplatesBlitzConfig = async (
if (blitzServer.length > 1) {
throw new Error("Found more than one blitz-server.js or blitz-server.ts in app or src folder")
}
const blitzServerPath = require("path").join(process.cwd(), blitzServer.at(0))
const blitzServerPath = require("path").join(process.cwd(), blitzServer[0])
const userConfigModuleSource = fs.readFileSync(blitzServerPath, {encoding: "utf-8"})
const userConfigModule = j(userConfigModuleSource, {parser: customTsParser})
const program = userConfigModule.get()
@@ -267,7 +271,16 @@ export abstract class Generator<
if (!this.options.destinationRoot) this.options.destinationRoot = process.cwd()
}
abstract getTemplateValues(): Promise<any>
public templateValuesBuilder: IBuilder<T, any> = NullBuilder
async getTemplateValues(): Promise<any> {
const values = await this.templateValuesBuilder.getTemplateValues(this.options)
return values
}
public fieldTemplateRegExp: RegExp = new RegExp(
/({?\/\*\s*template: (.*) \*\/}?|\s*\/\/\s*template: (.*))/g,
)
abstract getTargetDirectory(): string
@@ -295,6 +308,12 @@ export abstract class Generator<
replaceTemplateValues(input: string, templateValues: any) {
let result = input
for (let templateKey in templateValues) {
const token = `__${templateKey}__`
if (result.includes(token)) {
result = result.replace(new RegExp(token, "g"), templateValues[templateKey] as string)
}
}
for (let templateKey in templateValues.fieldTemplateValues) {
const token = `__${templateKey}__`
if (result.includes(token)) {
result = result.replace(new RegExp(token, "g"), templateValues[templateKey])
@@ -319,6 +338,21 @@ export abstract class Generator<
if (codeFileExtensions.test(pathEnding)) {
templatedFile = this.replaceConditionals(inputStr, templateValues, prettierOptions || {})
}
const fieldTemplateString = templatedFile
?.match(this.fieldTemplateRegExp)
?.at(0)
?.replace(this.fieldTemplateRegExp, "$2$3")
if (fieldTemplateString) {
const fieldTemplatePosition = templatedFile.search(this.fieldTemplateRegExp)
templatedFile = [
templatedFile.slice(0, fieldTemplatePosition),
...(templateValues.fieldTemplateValues?.map((values: any) =>
this.replaceTemplateValues(fieldTemplateString, values),
) || []),
templatedFile.slice(fieldTemplatePosition),
].join("")
}
templatedFile = this.replaceTemplateValues(templatedFile, templateValues)
if (!this.useTs && tsExtension.test(pathEnding)) {
templatedFile =
@@ -343,12 +377,21 @@ export abstract class Generator<
try {
templatedFile = this.prettier.format(templatedFile, options)
} catch (error) {
console.warn(`Failed trying to run prettier: ` + error)
console.error(`Failed trying to run prettier: ` + error)
}
}
return templatedFile
}
async preFileWrite(filePath: string): Promise<CommonTemplateValues> {
// allow subclasses to do something before writing a file
return this.getTemplateValues()
}
async postFileWrite(filePath: string, templateValues: CommonTemplateValues): Promise<void> {
// allow subclasses to do something after writing a file
}
async write(): Promise<void> {
debug("Generator.write...")
const sourcePath = this.sourcePath()
@@ -356,33 +399,45 @@ export abstract class Generator<
const additionalFilesToIgnore = this.filesToIgnore()
return ![...alwaysIgnoreFiles, ...additionalFilesToIgnore].includes(name)
})
const prettierOptions = await this.prettier?.resolveConfig(sourcePath)
for (let filePath of paths) {
try {
let pathSuffix = filePath
pathSuffix = path.join(this.getTargetDirectory(), pathSuffix)
const templateValues = await this.getTemplateValues()
let templateValues = await this.getTemplateValues()
const pathSuffix = path.join(this.getTargetDirectory(), filePath)
const sourcePath = this.sourcePath(filePath)
const destinationPath = this.destinationPath(pathSuffix)
let templatedPathSuffix = this.replaceTemplateValues(pathSuffix, templateValues)
const templatedDestinationPath = this.destinationPath(templatedPathSuffix)
const destinationExists = fs.existsSync(templatedDestinationPath)
const newContent = this.process(
this.fs.read(this.sourcePath(filePath), {raw: true}) as any,
pathSuffix,
templateValues,
prettierOptions ?? undefined,
)
this.fs.write(this.destinationPath(pathSuffix), newContent)
templateValues = await this.preFileWrite(templatedPathSuffix)
if (!this.useTs && tsExtension.test(this.destinationPath(pathSuffix))) {
templatedPathSuffix = templatedPathSuffix.replace(tsExtension, ".js")
}
if (templatedPathSuffix !== pathSuffix) {
this.fs.move(this.destinationPath(pathSuffix), this.destinationPath(templatedPathSuffix))
if (destinationExists) {
const newContent = this.process(
this.fs.read(templatedDestinationPath, {raw: true}) as any,
pathSuffix,
templateValues,
prettierOptions ?? undefined,
)
this.fs.write(templatedDestinationPath, newContent)
} else {
this.fs.copy(escapePath(sourcePath), escapePath(destinationPath), {
process: (input) =>
this.process(input, pathSuffix, templateValues, prettierOptions ?? undefined),
})
if (templatedPathSuffix !== pathSuffix) {
this.fs.move(destinationPath, templatedDestinationPath)
}
}
await this.postFileWrite(templatedPathSuffix, templateValues)
} catch (error) {
console.error(`Error generating ${filePath}`)
log.error(`Error generating ${filePath}`)
throw error
}
}

View File

@@ -2,7 +2,7 @@ import spawn from "cross-spawn"
import chalk from "chalk"
import {readJSONSync, writeJson} from "fs-extra"
import {join} from "path"
import username from "username"
import {AppValuesBuilder} from "./template-builders/app-values-builder"
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {baseLogger, log} from "../utils/log"
import {fetchLatestVersionsFor} from "../utils/fetch-latest-version-for"
@@ -12,6 +12,12 @@ function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
}
export interface AppTemplateValues {
name: string
safeNameSlug: string
username: string | undefined
}
type TemplateConfig = {
path: string
skipForms?: boolean
@@ -46,13 +52,7 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
return ["jsconfig.json", "package.js.json", "vitest.config.js"]
}
async getTemplateValues() {
return {
name: this.options.appName,
safeNameSlug: this.options.appName.replace(/[^a-zA-Z0-9-_]/g, "-"),
username: await username(),
}
}
templateValuesBuilder = new AppValuesBuilder(this.fs)
getTargetDirectory() {
return ""

View File

@@ -1,17 +1,21 @@
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {getTemplateRoot} from "../utils/get-template-root"
import {camelCaseToKebabCase} from "../utils/inflector"
import {
CommonTemplateValues,
createFieldTemplateValues,
FieldValuesBuilder,
ResourceGeneratorOptions,
} from ".."
import {customTsParser, Generator, SourceRootType} from "../generator"
import j from "jscodeshift"
import {
insertImportPagnatedQuery,
insertImportQuery,
insertLabeledSelectField,
updateFormWithParent,
} from "../../src/utils/codemod-utils"
export interface FormGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
}
export interface FormGeneratorOptions extends ResourceGeneratorOptions {}
export class FormGenerator extends Generator<FormGeneratorOptions> {
sourceRoot: SourceRootType
@@ -22,31 +26,35 @@ export class FormGenerator extends Generator<FormGeneratorOptions> {
static subdirectory = "queries"
private getId(input: string = "") {
if (!input) return input
return `${input}Id`
templateValuesBuilder = new FieldValuesBuilder()
async preFileWrite(): Promise<CommonTemplateValues> {
let templateValues = await this.getTemplateValues()
if (templateValues.parentModel) {
const newFieldTemplateValues = await createFieldTemplateValues(
templateValues.parentModelId,
templateValues.parentModelIdZodType,
true,
)
if (templateValues.fieldTemplateValues) {
templateValues.fieldTemplateValues.push(newFieldTemplateValues)
} else {
templateValues.fieldTemplateValues = [newFieldTemplateValues]
}
}
return templateValues
}
private getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
parentModelId: this.getId(this.options.parentModel),
parentModelParam: this.getParam(this.getId(this.options.parentModel)),
parentModel: this.options.parentModel,
parentModels: this.options.parentModels,
ParentModel: this.options.ParentModel,
ParentModels: this.options.ParentModels,
modelId: this.getId(this.options.modelName),
modelIdParam: this.getParam(this.getId(this.options.modelName)),
modelName: this.options.modelName,
modelNames: this.options.modelNames,
ModelName: this.options.ModelName,
ModelNames: this.options.ModelNames,
async postFileWrite(filePath: string, templateValues: CommonTemplateValues): Promise<void> {
if (templateValues.parentModel && filePath.match(/components/g)) {
let program = j(this.fs.read(filePath), {
parser: customTsParser,
})
program = insertLabeledSelectField(program)
program = insertImportQuery(program, templateValues)
program = insertImportPagnatedQuery(program)
program = updateFormWithParent(program, templateValues)
this.fs.write(filePath, program.toSource())
}
}

View File

@@ -1,12 +1,12 @@
import * as ast from "@mrleebo/prisma-ast"
import {spawn} from "cross-spawn"
import which from "npm-which"
import path from "path"
import {log} from "../utils/log"
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {Field} from "../prisma/field"
import {Model} from "../prisma/model"
import {getTemplateRoot} from "../utils/get-template-root"
import {getPrismaSchema} from "../utils/get-prisma-schema"
export interface ModelGeneratorOptions extends GeneratorOptions {
modelName: string
@@ -22,8 +22,7 @@ export class ModelGenerator extends Generator<ModelGeneratorOptions> {
// default subdirectory is /app/[name], we need to back out of there to generate the model
static subdirectory = "../.."
unsafe_disableConflictChecker = true
async getTemplateValues() {}
prisma = true
getTargetDirectory() {
return ""
@@ -41,73 +40,68 @@ export class ModelGenerator extends Generator<ModelGeneratorOptions> {
// eslint-disable-next-line require-await
async write() {
const pkgJson = this.fs.readJSON("package.json", {}) as {[key: string]: any}
const rawSchemaPath = pkgJson?.prisma?.schema || "db/schema.prisma"
const obj = getPrismaSchema(this.fs)
if (typeof obj !== "boolean") {
const {schema, schemaPath} = obj
const {modelName, extraArgs, dryRun} = this.options
let updatedOrCreated = "created"
const schemaPath = path.resolve(rawSchemaPath)
if (!this.fs.exists(schemaPath)) {
throw new Error("Prisma schema file was not found")
}
let fieldPromises = (
extraArgs.length === 1 && extraArgs[0]?.includes(" ") ? extraArgs[0]?.split(" ") : extraArgs
).map((input) => Field.parse(input, schema))
let fields = (await Promise.all(fieldPromises)).flatMap((fieldArray) => fieldArray)
const modelDefinition = new Model(modelName, fields)
let schema: ast.Schema
try {
schema = ast.getSchema(this.fs.read(schemaPath))
} catch (err) {
console.error(`Failed to parse ${rawSchemaPath} file`)
throw err
}
const {modelName, extraArgs, dryRun} = this.options
let updatedOrCreated = "created"
let fields = (
extraArgs.length === 1 && extraArgs[0]?.includes(" ") ? extraArgs[0]?.split(" ") : extraArgs
).flatMap((input) => Field.parse(input, schema))
const modelDefinition = new Model(modelName, fields)
let model: ast.Model | undefined
if (!dryRun) {
model = schema.list.find(function (component): component is ast.Model {
return component.type === "model" && component.name === modelDefinition.name
})
try {
if (model) {
for (const field of fields) field.appendTo(model)
this.fs.write(schemaPath, ast.printSchema(schema))
updatedOrCreated = "updated"
} else {
model = modelDefinition.appendTo(schema)
this.fs.write(schemaPath, ast.printSchema(schema))
let model: ast.Model | undefined
if (!dryRun) {
model = schema.list.find(function (component): component is ast.Model {
return component.type === "model" && component.name === modelDefinition.name
})
try {
if (model) {
for (const field of fields) field.appendTo(model)
this.fs.write(schemaPath, ast.printSchema(schema))
updatedOrCreated = "updated"
} else {
model = modelDefinition.appendTo(schema)
this.fs.write(schemaPath, ast.printSchema(schema))
}
} catch (err) {
console.error(`Failed to apply changes to model '${modelDefinition.name}'`)
throw err
}
} catch (err) {
console.error(`Failed to apply changes to model '${modelDefinition.name}'`)
throw err
}
}
if (model) {
console.log("\n")
console.log(
`Model '${modelDefinition.name}'${
dryRun ? "" : ` ${updatedOrCreated} in schema.prisma`
}:\n`,
)
ast
.printSchema({type: "schema", list: [model]})
.split("\n")
.map(log.progress)
console.log("\n")
if (model) {
console.log("\n")
console.log(
`Model '${modelDefinition.name}'${
dryRun ? "" : ` ${updatedOrCreated} in schema.prisma`
}:\n`,
)
ast
.printSchema({type: "schema", list: [model]})
.split("\n")
.map(log.progress)
console.log("\n")
}
} else {
this.prisma = false
}
}
async postWrite() {
const shouldMigrate = await this.prismaMigratePrompt()
if (shouldMigrate) {
await new Promise<void>((res, rej) => {
const prismaBin = which(process.cwd()).sync("prisma")
const child = spawn(prismaBin, ["migrate", "dev"], {stdio: "inherit"})
child.on("exit", (code) => (code === 0 ? res() : rej()))
})
if (this.prisma) {
const prismaBin = which(process.cwd()).sync("prisma")
//@ts-ignore
spawn.sync(prismaBin, ["format"], {stdio: "inherit"})
const shouldMigrate = await this.prismaMigratePrompt()
if (shouldMigrate) {
await new Promise<void>((res, rej) => {
const child = spawn(prismaBin, ["migrate", "dev"], {stdio: "inherit"})
child.on("exit", (code) => (code === 0 ? res() : rej()))
})
}
}
}
}

View File

@@ -1,17 +1,16 @@
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {getTemplateRoot} from "../utils/get-template-root"
import {camelCaseToKebabCase} from "../utils/inflector"
import j from "jscodeshift"
import {
CommonTemplateValues,
createFieldTemplateValues,
FieldValuesBuilder,
ResourceGeneratorOptions,
} from ".."
import {customTsParser, Generator, SourceRootType} from "../generator"
import {replaceImportDbWithPrismaFolder} from "../../src/utils/codemod-utils"
export interface MutationsGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
}
export interface MutationsGeneratorOptions extends ResourceGeneratorOptions {}
export class MutationsGenerator extends Generator<MutationsGeneratorOptions> {
sourceRoot: SourceRootType
@@ -21,32 +20,31 @@ export class MutationsGenerator extends Generator<MutationsGeneratorOptions> {
}
static subdirectory = "mutations"
private getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
templateValuesBuilder = new FieldValuesBuilder(this.fs)
private getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
parentModelId: this.getId(this.options.parentModel),
parentModelParam: this.getParam(this.getId(this.options.parentModel)),
parentModel: this.options.parentModel,
parentModels: this.options.parentModels,
ParentModel: this.options.ParentModel,
ParentModels: this.options.ParentModels,
modelId: this.getId(this.options.modelName),
modelIdParam: this.getParam(this.getId(this.options.modelName)),
modelName: this.options.modelName,
modelNames: this.options.modelNames,
ModelName: this.options.ModelName,
ModelNames: this.options.ModelNames,
async preFileWrite(filePath: string): Promise<CommonTemplateValues> {
let templateValues = await this.getTemplateValues()
if (templateValues.parentModel && filePath.match(/.*mutations.*create.*/g)) {
const newFieldTemplateValues = await createFieldTemplateValues(
templateValues.parentModelId,
templateValues.parentModelIdZodType,
true,
)
if (templateValues.fieldTemplateValues) {
templateValues.fieldTemplateValues.push(newFieldTemplateValues)
} else {
templateValues.fieldTemplateValues = [newFieldTemplateValues]
}
}
if (this.fs.exists(filePath)) {
let program = j(this.fs.read(filePath) as any, {
parser: customTsParser,
})
program = replaceImportDbWithPrismaFolder(program)
this.fs.write(filePath, program.toSource())
}
return templateValues
}
getTargetDirectory() {

View File

@@ -1,19 +1,12 @@
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {CommonTemplateValues, FieldValuesBuilder, ResourceGeneratorOptions} from ".."
import {Generator, SourceRootType} from "../generator"
import {getTemplateRoot} from "../utils/get-template-root"
import {camelCaseToKebabCase} from "../utils/inflector"
import {spawn} from "cross-spawn"
import which from "npm-which"
import * as fs from "fs-extra"
export interface PageGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
}
export interface PageGeneratorOptions extends ResourceGeneratorOptions {}
export class PageGenerator extends Generator<PageGeneratorOptions> {
sourceRoot: SourceRootType
@@ -23,34 +16,7 @@ export class PageGenerator extends Generator<PageGeneratorOptions> {
}
static subdirectory = "../../.."
private getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
private getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
parentModelId: this.getId(this.options.parentModel),
parentModelParam: this.getParam(this.getId(this.options.parentModel)),
parentModel: this.options.parentModel,
parentModels: this.options.parentModels,
ParentModel: this.options.ParentModel,
ParentModels: this.options.ParentModels,
modelId: this.getId(this.options.modelName),
modelIdParam: this.getParam(this.getId(this.options.modelName)),
modelName: this.options.modelName,
modelNames: this.options.modelNames,
ModelName: this.options.ModelName,
ModelNames: this.options.ModelNames,
modelNamesPath: this.getModelNamesPath(),
}
}
templateValuesBuilder = new FieldValuesBuilder(this.fs)
getModelNamesPath() {
const kebabCaseContext = this.options.context
@@ -68,6 +34,28 @@ export class PageGenerator extends Generator<PageGeneratorOptions> {
return `src/pages/${parent}${kebabCaseModelName}`
}
async preFileWrite(): Promise<CommonTemplateValues> {
const templateValues = await this.getTemplateValues()
const targetDirectory = this.getTargetDirectory().replace(
"__parentModelParam__",
templateValues.parentModelParam,
)
if (templateValues.parentModel) {
const modelPages = fs.existsSync(
`src/pages/${camelCaseToKebabCase(templateValues.modelNames)}`,
)
if (modelPages) {
if (!fs.existsSync(targetDirectory)) {
fs.moveSync(
`src/pages/${camelCaseToKebabCase(templateValues.modelNames)}`,
targetDirectory,
)
}
}
}
return templateValues
}
async postWrite() {
await new Promise<void>((res, rej) => {
const blitzBin = which(process.cwd()).sync("blitz")

View File

@@ -1,17 +1,11 @@
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {CommonTemplateValues, FieldValuesBuilder, ResourceGeneratorOptions} from ".."
import {customTsParser, Generator, SourceRootType} from "../generator"
import {getTemplateRoot} from "../utils/get-template-root"
import {camelCaseToKebabCase} from "../utils/inflector"
import j from "jscodeshift"
import {replaceImportDbWithPrismaFolder} from "../../src/utils/codemod-utils"
export interface QueriesGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
}
export interface QueriesGeneratorOptions extends ResourceGeneratorOptions {}
export class QueriesGenerator extends Generator<QueriesGeneratorOptions> {
sourceRoot: SourceRootType
@@ -21,32 +15,19 @@ export class QueriesGenerator extends Generator<QueriesGeneratorOptions> {
}
static subdirectory = "queries"
private getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
templateValuesBuilder = new FieldValuesBuilder(this.fs)
private getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
parentModelId: this.getId(this.options.parentModel),
parentModelParam: this.getParam(this.getId(this.options.parentModel)),
parentModel: this.options.parentModel,
parentModels: this.options.parentModels,
ParentModel: this.options.ParentModel,
ParentModels: this.options.ParentModels,
modelId: this.getId(this.options.modelName),
modelIdParam: this.getParam(this.getId(this.options.modelName)),
modelName: this.options.modelName,
modelNames: this.options.modelNames,
ModelName: this.options.ModelName,
ModelNames: this.options.ModelNames,
async preFileWrite(filePath: string): Promise<CommonTemplateValues> {
let templateValues = await this.getTemplateValues()
if (this.fs.exists(filePath)) {
let program = j(this.fs.read(filePath) as any, {
parser: customTsParser,
})
program = replaceImportDbWithPrismaFolder(program)
this.fs.write(filePath, program.toSource())
}
return templateValues
}
getTargetDirectory() {

View File

@@ -0,0 +1,14 @@
import username from "username"
import {AppGeneratorOptions, AppTemplateValues} from "../.."
import {Builder} from "./builder"
export class AppValuesBuilder extends Builder<AppGeneratorOptions, AppTemplateValues> {
public async getTemplateValues(options: AppGeneratorOptions): Promise<AppTemplateValues> {
const values = {
name: options.appName,
safeNameSlug: options.appName.replace(/[^a-zA-Z0-9-_]/g, "-"),
username: await username(),
}
return values
}
}

View File

@@ -0,0 +1,139 @@
import {Editor} from "mem-fs-editor"
import {GeneratorOptions} from "../../generator"
import {getCodegen, getResourceValueFromCodegen} from "../../utils/get-codegen"
import {CodegenField} from "../../utils/get-codegen"
import {
addSpaceBeforeCapitals,
camelCaseToKebabCase,
singleCamel,
singlePascal,
} from "../../utils/inflector"
export interface IBuilder<T, U> {
getTemplateValues(Options: T): Promise<U>
}
const defaultFieldConfig: CodegenField = {
component: "LabeledTextField",
inputType: "text",
zodType: "string",
prismaType: "String",
}
export async function createFieldTemplateValues(
valueName: string | undefined,
typeName: string | undefined,
parent = false,
): Promise<{[x: string]: any}> {
{
let values: {[x: string]: any} = {
attributeName: singleCamel(valueName),
fieldName: singleCamel(valueName),
FieldName: singlePascal(valueName),
field_name: addSpaceBeforeCapitals(`${valueName}`).toLocaleLowerCase(), // field name
Field_name: singlePascal(addSpaceBeforeCapitals(`${valueName}`).toLocaleLowerCase()), // Field name
Field_Name: singlePascal(addSpaceBeforeCapitals(`${valueName}`)), // Field Name
}
const codegen = await getCodegen()
// iterate over resources defined for this field type
const fieldConfig =
codegen.fieldTypeMap?.[typeName as keyof typeof codegen.fieldTypeMap] || defaultFieldConfig
values = {...values, ...fieldConfig}
if (parent) {
values.inputType = singleCamel(valueName).replace("Id", "s")
values.component = "LabeledSelectField"
values.fieldName = "id"
return values
}
values.inputType = fieldConfig.inputType
return values
}
}
export interface ResourceGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
rawParentModelName?: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
extraArgs?: string[]
}
export interface CommonTemplateValues {
prismaFolder?: string
parentModelId: string
parentModelParam: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
parentModelIdZodType?: string
modelId: string
modelIdZodType?: string
modelIdParam: string
modelName: string
modelNames: string
ModelName: string
ModelNames: string
modelNamesPath: string
fieldTemplateValues?: {[x: string]: any}
}
export abstract class Builder<T, U> implements IBuilder<T, U> {
public constructor(fs?: Editor) {
this.fs = fs
}
abstract getTemplateValues(Options: T): Promise<U>
public fs: Editor | undefined
public defaultFieldConfig = defaultFieldConfig
public getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
public getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
public getModelNamesPath(context: string | undefined, modelNames: string) {
const kebabCaseContext = context ? `${camelCaseToKebabCase(context)}/` : ""
const kebabCaseModelNames = camelCaseToKebabCase(modelNames)
return kebabCaseContext + kebabCaseModelNames
}
// eslint-disable-next-line require-await
public async getZodType(type: string = "") {
return getResourceValueFromCodegen(type, "zodType")
}
// eslint-disable-next-line require-await
public async getComponentForType(type: string = "") {
return getResourceValueFromCodegen(type, "component")
}
// eslint-disable-next-line require-await
public async getInputType(type: string = "") {
return getResourceValueFromCodegen(type, "inputType")
}
// eslint-disable-next-line require-await
public async getFieldTemplateValues(args: string[]) {
const argsPromises = args.map(async (arg: string) => {
let [valueName, typeName] = arg.split(":")
if (typeName?.includes("?")) {
typeName = typeName.replace("?", "")
}
const values = await createFieldTemplateValues(valueName, typeName)
return values
})
return Promise.all(argsPromises)
}
}

View File

@@ -0,0 +1,120 @@
import * as ast from "@mrleebo/prisma-ast"
import {create as createStore} from "mem-fs"
import {create as createEditor, Editor} from "mem-fs-editor"
import {getResourceValueFromCodegen} from "../../utils/get-codegen"
import {getPrismaSchema} from "../../utils/get-prisma-schema"
import {ModelName, modelName, ModelNames, modelNames} from "../../utils/model-names"
import {Builder, CommonTemplateValues, ResourceGeneratorOptions} from "./builder"
export class FieldValuesBuilder extends Builder<ResourceGeneratorOptions, CommonTemplateValues> {
private getEditor = (): Editor => {
if (this.fs !== undefined) {
return this.fs
}
const store = createStore()
this.fs = createEditor(store)
return this.fs
}
// eslint-disable-next-line require-await
public async getTemplateValues(options: ResourceGeneratorOptions): Promise<CommonTemplateValues> {
const prismaFolder = getPrismaSchema(this.getEditor())
let values: CommonTemplateValues = {
prismaFolder: typeof prismaFolder !== "boolean" ? prismaFolder.dbFolder : "db",
parentModelId: this.getId(options.parentModel),
parentModelIdZodType: undefined,
parentModelParam: this.getParam(this.getId(options.parentModel)),
parentModel: options.parentModel,
parentModels: options.parentModels,
ParentModel: options.ParentModel,
ParentModels: options.ParentModels,
modelId: this.getId(options.modelName),
modelIdZodType: "number",
modelIdParam: this.getParam(this.getId(options.modelName)),
modelName: options.modelName,
modelNames: options.modelNames,
ModelName: options.ModelName,
ModelNames: options.ModelNames,
modelNamesPath: this.getModelNamesPath(options.context, options.modelNames),
}
if (options.extraArgs) {
// specialArgs - these are arguments like 'id' or 'belongsTo', which are not meant to
// be processed as fields but have their own special logic
const specialArgs: {[key in string]: string} = {}
const processSpecialArgs: Promise<void>[] = options.extraArgs.map(async (arg) => {
const [valueName, typeName] = arg.split(":")
if (valueName === "id") {
values.modelIdZodType = await this.getZodType(typeName)
specialArgs[arg] = "present"
}
if (valueName === "belongsTo") {
specialArgs[arg] = "present"
process.env.parentModel = typeName
options.rawParentModelName = typeName
options.parentModel = modelName(typeName)
options.parentModels = modelNames(typeName)
options.ParentModel = ModelName(typeName)
options.ParentModels = ModelNames(typeName)
values.parentModelId = this.getId(modelName(typeName))
}
})
await Promise.all(processSpecialArgs)
// Filter out special args by making sure the argument isn't present in the list
const nonSpecialArgs = options.extraArgs.filter((arg) => specialArgs[arg] !== "present")
// Get the parent model it type if options.parentModel exists
if (options.parentModel !== undefined && options.parentModel.length > 0) {
const obj = getPrismaSchema(this.getEditor())
if (typeof obj !== "boolean") {
const schema = obj.schema
// O(N) - N is total ast Blocks
const model = schema.list.find(function (component): component is ast.Model {
return component.type === "model" && component.name === options.rawParentModelName
})
if (model !== undefined) {
// O(N) - N is number of properties in parent model
const idField = model.properties.find(function (property): property is ast.Field {
return (
property.type === "field" &&
property.attributes?.findIndex((attr) => attr.name === "id") !== -1
)
})
// TODO: Do we want a map between prisma types and "user types", we can then use that map instead of these conditionals
// We have a map from "user types" (which are what users type into the blitz generate command)
// to primsa type and other types, but we dont have a reverse map 1:1. This is because we lose
// some information for certain maps. E.g.: fieldname:uuid will be converted into a Prisma field with
// the String type, and the uuid portion is added to a decorator at the end of the field.
// This means it is more complicated to extract the original "user specified type" than creating a reverse map
if (idField?.fieldType === "Int") {
// TODO: Check if ints have decorators that make them a different type, like Bigint, etc.
// And see if that has to map to a different user specified type
values.parentModelIdZodType = await getResourceValueFromCodegen("int", "zodType")
} else if (idField?.fieldType === "String") {
if (
idField.attributes?.find(
(attr) =>
attr.name === "default" &&
attr.args?.findIndex((arg) => arg.value === "uuid") !== -1,
)
) {
values.parentModelIdZodType = await getResourceValueFromCodegen("uuid", "zodType")
} else {
values.parentModelIdZodType = await getResourceValueFromCodegen("string", "zodType")
}
}
}
} else {
// TODO: handle scenario where parent wasnt found in existing schema. Should we throw an error, or a warning asking the user to verify that the parent model exists?
}
}
if (nonSpecialArgs.length > 0) {
const ftv = await this.getFieldTemplateValues(nonSpecialArgs)
return {...values, fieldTemplateValues: ftv}
}
}
return values
}
}

View File

@@ -0,0 +1,8 @@
import {IBuilder} from "./builder"
export const NullBuilder: IBuilder<any, any> = {
// eslint-disable-next-line require-await
getTemplateValues: async () => {
return {}
},
}

View File

@@ -7,6 +7,11 @@ export * from "./generators/queries-generator"
export * from "./generators/query-generator"
export * from "./generators/form-generator"
export * from "./generator"
export * from "./generators/template-builders/builder"
export * from "./generators/template-builders/null-builder"
export * from "./generators/template-builders/app-values-builder"
export * from "./generators/template-builders/field-values-builder"
export * from "./utils/model-names"
export * from "./conflict-checker"
export {getLatestVersion} from "./utils/get-latest-version"
export * from "./utils/npm-fetch"
@@ -18,4 +23,5 @@ export {
pluralPascal,
capitalize,
uncapitalize,
addSpaceBeforeCapitals,
} from "./utils/inflector"

View File

@@ -1,5 +1,7 @@
import * as ast from "@mrleebo/prisma-ast"
import {getResourceConfigFromCodegen} from "../utils/get-codegen"
import {capitalize, singlePascal, uncapitalize} from "../utils/inflector"
const debug = require("debug")("blitz:field")
export enum FieldType {
Boolean = "Boolean",
@@ -54,7 +56,8 @@ export class Field {
relationToFields?: string[]
// 'name:type?[]:attribute' => Field
static parse(input: string, schema?: ast.Schema): Field[] {
static async parse(input: string, schema?: ast.Schema): Promise<Field[]> {
debug(`parsing "Field" for input ${input}`)
const [_fieldName, _fieldType = "String", _attribute] = input.split(":")
let attribute = _attribute as string
let fieldName = uncapitalize(_fieldName as string)
@@ -73,6 +76,16 @@ export class Field {
fieldType = fieldType.replace("?", "")
isRequired = false
}
const {prismaType, default: defaultConfigValue} =
(await Field.getConfigForPrismaType(fieldType)) ?? {}
if (prismaType) {
fieldType = prismaType
}
if (defaultConfigValue) {
attribute = `default=${defaultConfigValue}`
}
if (fieldType.includes("[]")) {
fieldType = fieldType.replace("[]", "")
fieldName = uncapitalize(fieldName)
@@ -84,9 +97,9 @@ export class Field {
}
// use original unmodified field name in case the list handling code
// has modified fieldName
if (isRelation(_fieldName as string)) {
if (typeof _fieldName === "string" && isRelation(_fieldName)) {
// this field is an object type, not a scalar type
const relationType = Relation[_fieldName]
const relationType = Relation[_fieldName as keyof typeof Relation]
// translate the type into the name since they should stay in sync
fieldName = uncapitalize(fieldType)
fieldType = singlePascal(fieldType)
@@ -159,6 +172,10 @@ export class Field {
}
}
public static getConfigForPrismaType = async (fieldType: string) => {
return await getResourceConfigFromCodegen(fieldType.toLowerCase())
}
constructor(name: string, options: FieldArgs) {
if (!name) throw new MissingFieldNameError("[PrismaField]: A field name is required")
if (!options.type) {
@@ -214,7 +231,12 @@ export class Field {
args: [
{
type: "attributeArgument",
value: typeof this.default === "object" ? `${this.default.name}()` : String(this.default),
value:
typeof this.default === "object"
? `${this.default.name}()`
: this.type === FieldType.String
? `"${this.default}"`
: this.default,
},
],
}

View File

@@ -0,0 +1,155 @@
import j, {Collection} from "jscodeshift"
import {capitalize, singleCamel} from "./inflector"
export function replaceImportDbWithPrismaFolder(program: Collection<any>) {
const importDb = program.find(j.ImportDeclaration).filter((path) => {
return path.value.source.value === "db"
})
importDb.replaceWith((path) => {
path.value.source.value = "__prismaFolder__"
return path.value
})
return program
}
export function insertLabeledSelectField(program: Collection<any>) {
const importExists = program
.find(j.ImportDeclaration)
.filter((path) => path.node.source.value === "src/core/components/LabelSelectField")
.size()
if (!importExists) {
program
.find(j.ImportDeclaration)
.at(-1)
.insertAfter(
j.importDeclaration(
[j.importSpecifier(j.identifier("LabeledSelectField"))],
j.literal("src/core/components/LabelSelectField"),
),
)
}
return program
}
export function insertImportQuery(program: Collection<any>, templateValues: any) {
program
.find(j.ImportDeclaration)
.at(-1)
.insertAfter(
j.importDeclaration(
[
j.importDefaultSpecifier(
j.identifier(
singleCamel("get" + capitalize(templateValues.parentModelId)).replace("Id", "s"),
),
),
],
j.literal(
`src/${templateValues.parentModelId.replace("Id", "s")}/queries/${
"get" + capitalize(templateValues.parentModelId).replace("Id", "s")
}`,
),
),
)
return program
}
export function insertImportPagnatedQuery(program: Collection<any>) {
const importPaginatedQuery = program
.find(j.ImportDeclaration)
.filter((path) => {
if (path.node.specifiers) {
return (
path.node.source.value === "@blitzjs/rpc" &&
path.node.specifiers.filter((specifier) => {
if (specifier.type === "ImportSpecifier") {
return specifier.imported.name === "usePaginatedQuery"
} else {
return false
}
}).length > 0
)
} else {
return false
}
})
.size()
if (!importPaginatedQuery) {
program
.find(j.ImportDeclaration)
.at(-1)
.insertAfter(
j.importDeclaration(
[j.importSpecifier(j.identifier("usePaginatedQuery"))],
j.literal("@blitzjs/rpc"),
),
)
}
return program
}
export function updateFormWithParent(program: Collection<any>, templateValues: any) {
const modalFormSuspense = program.find(j.FunctionDeclaration).filter((path) => {
if (path.node.id?.name) {
return path.node.id.name.includes("Form")
}
return false
})
if (modalFormSuspense.size()) {
modalFormSuspense
.find(j.ReturnStatement)
.at(-1)
.insertBefore(
j.variableDeclaration("const", [
j.variableDeclarator(
j.arrayPattern([
j.objectPattern([
j.objectProperty(
j.identifier(`${templateValues.parentModelId.replace("Id", "s")}`),
j.identifier(`${templateValues.parentModelId.replace("Id", "s")}`),
),
]),
]),
j.callExpression(j.identifier("usePaginatedQuery"), [
j.identifier("get" + capitalize(templateValues.parentModelId).replace("Id", "s")),
j.objectExpression([
j.objectProperty(
j.identifier("orderBy"),
j.objectExpression([
j.objectProperty(j.identifier("id"), j.stringLiteral("asc")),
]),
),
]),
]),
),
]),
)
modalFormSuspense
.find(j.JSXElement)
.filter((path) => {
return (
path.node.openingElement.name.type === "JSXIdentifier" &&
path.node.openingElement.name.name === "LabeledSelectField"
)
})
.forEach((path) => {
let value = ""
path.node.openingElement.attributes = path.node.openingElement.attributes?.filter(
(attribute) => {
if (attribute.type === "JSXAttribute" && attribute.name.name === "type") {
if (attribute.value?.type === "StringLiteral") {
value = attribute.value.value
}
return false
}
return true
},
)
path.node.openingElement.attributes?.push(
j.jsxAttribute(j.jsxIdentifier("options"), j.jsxExpressionContainer(j.identifier(value))),
)
})
}
return program
}

View File

@@ -0,0 +1,165 @@
import {log} from "../utils/log"
import * as z from "zod"
export type CodegenField = {
component: string
inputType: string
zodType: string
prismaType: string
default?: string
}
export type CodegenConfig = {
fieldTypeMap?: Record<
| "string"
| "boolean"
| "int"
| "number"
| "bigint"
| "float"
| "decimal"
| "datetime"
| "uuid"
| "json",
CodegenField
>
}
const CodegenSchema = z.object({
fieldTypeMap: z.record(
z.union([
z.literal("string"),
z.literal("boolean"),
z.literal("int"),
z.literal("number"),
z.literal("bigint"),
z.literal("float"),
z.literal("decimal"),
z.literal("datetime"),
z.literal("uuid"),
z.literal("json"),
]),
z.object({
component: z.string(),
inputType: z.string(),
zodType: z.string(),
prismaType: z.string(),
default: z.string().optional(),
}),
),
})
export const defaultCodegenConfig: CodegenConfig = {
fieldTypeMap: {
string: {
component: "LabeledTextField",
inputType: "text",
zodType: "string",
prismaType: "String",
},
boolean: {
component: "LabeledTextField",
inputType: "text",
zodType: "boolean",
prismaType: "Boolean",
},
int: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Int",
},
number: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Int",
},
bigint: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "BigInt",
},
float: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Float",
},
decimal: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Decimal",
},
datetime: {
component: "LabeledTextField",
inputType: "text",
zodType: "string().datetime()",
prismaType: "DateTime",
},
uuid: {
component: "LabeledTextField",
inputType: "text",
zodType: "string().uuid",
prismaType: "String",
default: "uuid",
},
json: {
component: "LabeledTextField",
inputType: "text",
zodType: "any",
prismaType: "Json",
},
},
}
export const getResourceValueFromCodegen = async (
fieldType: string,
resource: keyof CodegenField,
): Promise<string | undefined> => {
const codegen = await getCodegen()
const templateValue = codegen.fieldTypeMap?.[fieldType]?.[resource]
return templateValue
}
export const getResourceConfigFromCodegen = async (
fieldType: string,
): Promise<CodegenField | undefined> => {
const codegen = await getCodegen()
const config = codegen.fieldTypeMap?.[fieldType]
return config
}
const getIsTypeScript = async () =>
require("fs").existsSync(require("path").join(process.cwd(), "tsconfig.json"))
export const getCodegen = async () => {
try {
const isTypeScript = await getIsTypeScript()
const blitzServerPath = isTypeScript ? "app/blitz-server.ts" : "app/blitz-server.js"
const blitzServer = require("path").join(process.cwd(), blitzServerPath)
const {register} = require("esbuild-register/dist/node")
const {unregister} = register({
target: "es6",
})
const blitzConfig = require(blitzServer)
const config = blitzConfig
const {cliConfig} = config
unregister()
if (cliConfig.codegen !== undefined) {
const result = CodegenSchema.safeParse(cliConfig.codegen)
if (!result.success) {
log.error("Failed parsing codegen config. Check if it is well formed. Using default config")
return defaultCodegenConfig
}
return cliConfig.codegen
}
return defaultCodegenConfig
} catch (ex) {
log.debug("Failed loading config" + ex)
return defaultCodegenConfig
}
}

View File

@@ -0,0 +1,65 @@
import * as ast from "@mrleebo/prisma-ast"
import {Editor} from "mem-fs-editor"
import {log} from "../utils/log"
import fs from "fs"
import path from "path"
//create a custom error class when prisma does not exist
export class PrismaSchemaNotFoundError extends Error {
constructor() {
super("Prisma schema not found.")
this.name = "PrismaSchemaNotFoundError"
}
}
function getDbFolder() {
try {
const packageJsonPath = path.join(process.cwd(), "package.json")
if (fs.existsSync(packageJsonPath)) {
const packageJson = fs.readFileSync(packageJsonPath, "utf8")
const packageJsonObj = JSON.parse(packageJson)
if (!packageJsonObj.prisma || !packageJsonObj.prisma.schema) {
throw new PrismaSchemaNotFoundError()
}
const prismaSchemaPath = path.join(process.cwd(), packageJsonObj.prisma.schema)
if (!fs.existsSync(prismaSchemaPath)) {
throw new PrismaSchemaNotFoundError()
}
const folder = packageJsonObj.prisma.schema.split("/")[0] as string
return folder
} else {
throw new PrismaSchemaNotFoundError()
}
} catch (e) {
if (fs.existsSync(path.join(process.cwd(), "db/schema.prisma"))) {
return "db"
}
throw e
}
}
export const getPrismaSchema = (
memFsEditor: Editor,
): {schema: ast.Schema; schemaPath: string; dbFolder: string} | boolean => {
let dbFolder: string, schemaPath: string
try {
dbFolder = getDbFolder()
schemaPath = path.join(process.cwd(), dbFolder, "schema.prisma")
} catch (e) {
if (e instanceof PrismaSchemaNotFoundError) {
return false
}
throw e
}
if (!fs.existsSync(schemaPath)) {
return {schema: {type: "schema", list: []}, schemaPath, dbFolder}
}
let schema: ast.Schema
try {
schema = ast.getSchema(memFsEditor.read(schemaPath))
} catch (err) {
log.debug("Failed to parse schema.prisma file")
throw err
}
return {schema, schemaPath, dbFolder}
}

View File

@@ -5,6 +5,10 @@ export function plural(input: string): string {
return pluralize.isPlural(input) ? input : pluralize.plural(input)
}
export function addSpaceBeforeCapitals(input: string): string {
return singleCamel(input).replace(/(?!^)([A-Z])/g, " $1")
}
export function singular(input: string): string {
return pluralize.isSingular(input) ? input : pluralize.singular(input)
}

View File

@@ -167,6 +167,19 @@ const withProgress = (str: string) => {
return withCaret(str)
}
const greenText = (str: string) => {
return `${c.green(str)}`
}
/**
* Logs a red error message to stdout.
*
* @param {string} msg
*/
const error = (msg: string) => {
console.log(`${c.red(msg)}`)
}
/**
* Logs a branded purple message to stdout.
*
@@ -248,6 +261,8 @@ export const log = {
progress,
spinner,
success,
greenText,
error,
variable,
debug,
Table,

View File

@@ -0,0 +1,14 @@
import {pluralCamel, pluralPascal, singleCamel, singlePascal} from "../index"
export function modelName(input: string = "") {
return singleCamel(input)
}
export function modelNames(input: string = "") {
return pluralCamel(input)
}
export function ModelName(input: string = "") {
return singlePascal(input)
}
export function ModelNames(input: string = "") {
return pluralPascal(input)
}

View File

@@ -0,0 +1,60 @@
import { forwardRef, PropsWithoutRef } from "react"
import { useField } from "react-final-form"
export interface LabeledSelectFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["select"]> {
/** Field name. */
name: string
/** Field label. */
label: string
type?: "number" | "string"
options: any
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
}
export const LabeledSelectField = forwardRef<HTMLSelectElement, LabeledSelectFieldProps>(
({ name, label, outerProps, options, type="number", ...props }, ref) => {
const {
input,
meta: { touched, error, submitError, submitting },
} = useField(name, {
parse: type === "number" ? Number : undefined,
})
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
return (
<div {...outerProps}>
<label>
{label}
<select {...input} disabled={submitting} {...props} ref={ref}>
{options && options.map((value) => <option value={value.id}>{value[name]}</option>)}
</select>
</label>
{touched && normalizedError && (
<div role="alert" style={{ color: "red" }}>
{normalizedError}
</div>
)}
<style jsx>{`
label {
display: flex;
flex-direction: column;
align-items: start;
font-size: 1rem;
}
select {
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 3px;
appearance: none;
margin-top: 0.5rem;
}
`}</style>
</div>
)
}
)
export default LabeledSelectField

View File

@@ -0,0 +1,68 @@
import { forwardRef, PropsWithoutRef } from "react";
import { useFormikContext, ErrorMessage, Field } from "formik";
export interface LabeledSelectFieldProps
extends PropsWithoutRef<JSX.IntrinsicElements["select"]> {
/** Field name. */
name: string;
/** Field label. */
label: string;
/** Field options. */
options: any;
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>;
}
export const LabeledSelectField = forwardRef<
HTMLSelectElement,
LabeledSelectFieldProps
>(({ name, label, outerProps, options, ...props }, ref) => {
const { isSubmitting } = useFormikContext();
return (
<div {...outerProps}>
<label
style={{
display: "flex",
flexDirection: "column",
alignItems: "start",
fontSize: "1rem",
}}
>
{label}
<Field
{...props}
disabled={isSubmitting}
ref={ref}
name={name}
as="select"
style={{
fontSize: "1rem",
padding: " 0.25rem 0.4rem",
borderRadius: "3px",
border: "1px solid purple",
marginTop: "0.5rem",
backgroundColor: "white",
}}
>
<option value="" selected disabled hidden>
Select {label}
</option>
{options.map((option, index) => (
<option key={index} value={option.id}>
{option[name]}
</option>
))}
</Field>
</label>
<ErrorMessage name={name}>
{(msg) => (
<div role="alert" style={{ color: "red" }}>
{msg}
</div>
)}
</ErrorMessage>
</div>
);
});
export default LabeledSelectField;

View File

@@ -0,0 +1,58 @@
import { ComponentPropsWithoutRef, forwardRef, PropsWithoutRef } from "react"
import { useFormContext } from "react-hook-form"
import { ErrorMessage } from "@hookform/error-message"
export interface LabeledSelectFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["select"]> {
/** Field name. */
name: string
/** Field label. */
label: string
/** Field type. Doesn't include radio buttons and checkboxes */
options: any[]
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
labelProps?: ComponentPropsWithoutRef<"label">
}
export const LabeledSelectField = forwardRef<HTMLSelectElement, LabeledSelectFieldProps>(
({ label, outerProps, labelProps, name, options, ...props }, ref) => {
const {
register,
formState: { isSubmitting, errors },
} = useFormContext()
return (
<div {...outerProps}>
<label {...labelProps}>
{label}
<select {...register(name)} disabled={isSubmitting} {...props}>
{options && options.map((value) => <option value={value.id}>{value[name]}</option>)}
</select>
</label>
<ErrorMessage
render={({ message }) => (
<div role="alert" style={{ color: "red" }}>
{message}
</div>
)}
errors={errors}
name={name}
/>
<style jsx>{`
label {
display: flex;
flex-direction: column;
align-items: start;
font-size: 1rem;
}
select {
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 3px;
border: 1px solid purple;
appearance: none;
margin-top: 0.5rem;
}
`}</style>
</div>
)
}
)

View File

@@ -27,7 +27,7 @@
"@blitzjs/rpc": "latest",
"@prisma/client": "4.6.0",
"blitz": "latest",
"next": "12.2.5",
"next": "13.1",
"prisma": "4.6.0",
"react": "18.2.0",
"react-dom": "18.2.0",

View File

@@ -27,11 +27,11 @@
"@blitzjs/rpc": "latest",
"@prisma/client": "4.6.0",
"blitz": "latest",
"next": "12.2.5",
"next": "13.1",
"prisma": "4.6.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"zod": "3.19.1"
"zod": "3.20.2"
},
"devDependencies": {
"@next/bundle-analyzer": "12.0.8",

View File

@@ -1,9 +1,10 @@
import {ErrorFallbackProps, ErrorComponent, ErrorBoundary, AppProps} from "@blitzjs/next"
import {AuthenticationError, AuthorizationError} from "blitz"
import { ErrorFallbackProps, ErrorComponent, ErrorBoundary, AppProps } from "@blitzjs/next"
import { AuthenticationError, AuthorizationError } from "blitz"
import React from "react"
import {withBlitz} from "src/blitz-client"
import { withBlitz } from "src/blitz-client"
import 'src/styles/globals.css'
function RootErrorFallback({error}: ErrorFallbackProps) {
function RootErrorFallback({ error }: ErrorFallbackProps) {
if (error instanceof AuthenticationError) {
return <div>Error: You are not authenticated</div>
} else if (error instanceof AuthorizationError) {
@@ -23,7 +24,7 @@ function RootErrorFallback({error}: ErrorFallbackProps) {
}
}
function MyApp({Component, pageProps}: AppProps) {
function MyApp({ Component, pageProps }: AppProps) {
const getLayout = Component.getLayout || ((page) => page)
return (
<ErrorBoundary FallbackComponent={RootErrorFallback}>

View File

@@ -1,12 +1,11 @@
import { Suspense } from "react"
import Image from "next/image"
import Link from "next/link"
import Layout from "src/core/layouts/Layout"
import { useCurrentUser } from "src/users/hooks/useCurrentUser"
import logout from "src/auth/mutations/logout"
import logo from "public/logo.png"
import { useMutation } from "@blitzjs/rpc"
import { Routes, BlitzPage } from "@blitzjs/next"
import styles from 'src/styles/Home.module.css'
/*
* This file is just for a pleasant getting started page for your new app.
@@ -21,7 +20,7 @@ const UserInfo = () => {
return (
<>
<button
className="button small"
className={styles.button}
onClick={async () => {
await logoutMutation()
}}
@@ -38,15 +37,11 @@ const UserInfo = () => {
} else {
return (
<>
<Link href={Routes.SignupPage()}>
<a className="button small">
<strong>Sign Up</strong>
</a>
<Link href={Routes.SignupPage()} className={styles.button}>
<strong>Sign Up</strong>
</Link>
<Link href={Routes.LoginPage()}>
<a className="button small">
<strong>Login</strong>
</a>
<Link href={Routes.LoginPage()} className={styles.loginButton}>
<strong>Login</strong>
</Link>
</>
)
@@ -56,216 +51,144 @@ const UserInfo = () => {
const Home: BlitzPage = () => {
return (
<Layout title="Home">
<div className="container">
<main>
<div className="logo">
<Image src={`${logo.src}`} alt="blitzjs" width="256px" height="118px" layout="fixed" />
</div>
<div className={styles.globe} />
<div className={styles.container}>
<div className={styles.toastContainer}>
<p>
<strong>Congrats!</strong> Your app is ready, including user sign-up and log-in.
</p>
<div className="buttons" style={{ marginTop: "1rem", marginBottom: "1rem" }}>
<Suspense fallback="Loading...">
<UserInfo />
</Suspense>
</div>
<p>
<strong>
To add a new model to your app, <br />
run the following in your terminal:
</strong>
</p>
<pre>
<code>blitz generate all project name:string</code>
</pre>
<div style={{ marginBottom: "1rem" }}>(And select Yes to run prisma migrate)</div>
<div>
<p>
Then <strong>restart the server</strong>
</p>
<pre>
<code>Ctrl + c</code>
</pre>
<pre>
<code>blitz dev</code>
</pre>
<p>
and go to{" "}
<Link href="/projects">
<a>/projects</a>
</Link>
</p>
</div>
<div className="buttons" style={{ marginTop: "5rem" }}>
<a
className="button"
href="https://blitzjs.com/docs/getting-started?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
<a
className="button-outline"
href="https://github.com/blitz-js/blitz"
target="_blank"
rel="noopener noreferrer"
>
Github Repo
</a>
<a
className="button-outline"
href="https://discord.blitzjs.com"
target="_blank"
rel="noopener noreferrer"
>
Discord Community
</a>
</div>
<main className={styles.main}>
<div className={styles.wrapper}>
<div className={styles.header}>
<div className={styles.logo}>
<svg viewBox="0 0 165 66"><path d="M104.292 56.033C104.292 56.408 104.206 56.6636 104.036 56.8C103.9 56.9363 103.627 57.0045 103.218 57.0045H99.7409C99.4001 57.0045 99.1615 56.9533 99.0251 56.8511C98.8888 56.7147 98.8206 56.4932 98.8206 56.1864L98.9229 19.8324C98.9229 19.3211 99.1444 19.0654 99.5876 19.0654H103.627C103.839 19.0654 104.292 19.0672 104.292 19.0672V19.8324V56.033ZM64.3531 57.0081C64.1145 57.0081 63.927 56.9399 63.7906 56.8035C63.6543 56.6672 63.5861 56.4968 63.5861 56.2922V19.9383C63.5861 19.3588 63.8588 19.069 64.4042 19.069H76.829C81.533 19.069 85.1463 19.9212 87.6687 21.6256C90.1912 23.2958 91.4524 25.7331 91.4524 28.9373C91.4524 30.9484 90.924 32.6528 89.8673 34.0504C88.8106 35.4138 87.1063 36.5217 84.7543 37.3739C84.6179 37.4079 84.5497 37.4932 84.5497 37.6295C84.5497 37.7318 84.6179 37.7999 84.7543 37.834C87.2767 38.5158 89.1686 39.5895 90.4298 41.0553C91.7251 42.521 92.3727 44.4469 92.3727 46.833C92.3727 50.2418 91.0945 52.7983 88.5379 54.5027C85.9814 56.1729 82.2318 57.0081 77.2892 57.0081H64.3531ZM77.5448 35.5843C79.6923 35.5843 81.516 35.1071 83.0158 34.1526C84.5157 33.1982 85.2656 31.6983 85.2656 29.6531C85.2656 27.6079 84.5157 26.0569 83.0158 25.0002C81.5501 23.9435 79.5219 23.4151 76.9313 23.4151H70.5399C70.0286 23.4151 69.7729 23.6367 69.7729 24.0798V34.8684C69.7729 35.3457 69.9604 35.5843 70.3354 35.5843H77.5448ZM77.0335 52.662C82.9647 52.662 85.9303 50.5997 85.9303 46.4751C85.9303 44.3276 85.1633 42.7255 83.6294 41.6688C82.0955 40.6121 80.0673 40.0838 77.5448 40.0838H70.591C70.2843 40.0838 70.0627 40.1349 69.9263 40.2372C69.8241 40.3394 69.7729 40.5099 69.7729 40.7485V51.895C69.7729 52.4063 69.9604 52.662 70.3354 52.662H77.0335ZM142.707 56.8624C142.81 56.9647 142.997 57.0158 143.27 57.0158H163.876C164.387 57.0158 164.643 56.7772 164.643 56.3V53.948V53.3344H163.978H149.866C149.593 53.3344 149.457 53.2492 149.457 53.0788C149.457 52.9765 149.508 52.8572 149.61 52.7208L163.876 33.8536C164.251 33.2741 164.438 32.7628 164.438 32.3197V30.479V29.9144C164.438 29.9144 164.051 29.9165 163.876 29.9165H144.241C143.866 29.9165 143.679 30.121 143.679 30.5301V32.831C143.679 33.1037 143.713 33.2911 143.781 33.3934C143.883 33.4957 144.071 33.5468 144.344 33.5468H157.075C157.382 33.5468 157.535 33.632 157.535 33.8025L157.382 34.1092L143.219 52.9765C142.946 53.3515 142.759 53.6412 142.656 53.8457C142.588 54.0502 142.554 54.3059 142.554 54.6127V56.3C142.554 56.5727 142.605 56.7602 142.707 56.8624ZM116.929 19.0676H111.51V27.7684C114.503 27.7684 116.929 25.3419 116.929 22.3486V19.0676ZM116.926 56.0308C116.926 56.4058 116.841 56.6614 116.67 56.7978C116.534 56.9341 116.278 57.0023 115.903 57.0023H112.427C112.086 57.0023 111.847 56.9512 111.711 56.8489C111.574 56.7126 111.506 56.491 111.506 56.1842V30.6699C111.506 30.3972 111.557 30.2098 111.66 30.1075C111.762 29.9712 111.949 29.903 112.222 29.903H117.028L116.926 56.0308ZM132.183 34.3137C132.183 33.9728 132.336 33.8024 132.643 33.8024H138.779C139.256 33.8024 139.495 33.5979 139.495 33.1888V30.4789V29.9165H138.881H132.745C132.439 29.9165 132.285 29.7631 132.285 29.4563V21.531V20.713L131.621 20.7129H128.093C127.752 20.7129 127.547 20.9515 127.479 21.4288L126.865 29.4563C126.865 29.7631 126.729 29.9165 126.456 29.9165H122.366C121.957 29.9165 121.752 30.1039 121.752 30.4789V33.1888C121.752 33.5979 121.974 33.8024 122.417 33.8024H126.252C126.593 33.8024 126.763 34.0069 126.763 34.416V50.6244C126.763 52.806 127.309 54.4252 128.399 55.4819C129.49 56.5045 131.16 57.0158 133.41 57.0158C135.796 57.0158 137.535 56.9306 138.625 56.7601C139.137 56.6579 139.392 56.3681 139.392 55.8909V53.6923V53.0787H138.779H135.507C134.348 53.0787 133.495 52.806 132.95 52.2606C132.439 51.7152 132.183 50.7267 132.183 49.295V34.3137Z"></path><path d="M0.241243 33.2639H10.9742C15.0585 33.2639 18.9054 35.1835 21.3612 38.4471L31.9483 52.5165C32.1484 52.7824 32.1786 53.1393 32.026 53.435L25.9232 65.2592C25.6304 65.8265 24.8455 65.8932 24.4612 65.3835L0.241243 33.2639Z"></path><path d="M42.4727 33.2822H31.7398C27.6555 33.2822 23.8086 31.3626 21.3528 28.0991L10.7656 14.0297C10.5656 13.7638 10.5354 13.4068 10.688 13.1111L16.7908 1.28696C17.0836 0.719654 17.8684 0.652924 18.2528 1.16266L42.4727 33.2822Z"></path></svg>
</div>
<h1>Your database & authentication is ready. Try it by signing up.</h1>
{/* Auth */}
<div className={styles.buttonContainer}>
<Suspense fallback="Loading...">
<UserInfo />
</Suspense>
</div>
</div>
<div className={styles.body}>
{/* Instructions */}
<div className={styles.instructions}>
<p>
<strong>
Add a new model by running the following in your terminal:
</strong>
</p>
<div>
<div className={styles.code}>
<span>1</span>
<pre>
<code>blitz generate all project</code>
</pre>
</div>
<div className={styles.code}>
<span>2</span>
<pre>
<code>Ctrl + c</code>
</pre>
</div>
<div className={styles.code}>
<span>3</span>
<pre>
<code>blitz dev</code>
</pre>
</div>
<div className={styles.code}>
<span>4</span>
<pre>
<code>Go to{" "}
<Link href="/projects" className={styles.textLink}>
/projects
</Link></code>
</pre>
</div>
</div>
</div>
{/* Links */}
<div className={styles.linkGrid}>
<a
href="https://blitzjs.com/docs/getting-started?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Blitz Docs
<span className={styles.arrowIcon} />
</a>
<a
href="https://nextjs.org/docs/getting-started"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Next.js Docs
<span className={styles.arrowIcon} />
</a>
<a
href="https://github.com/blitz-js/blitz"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Github Repo
<span className={styles.arrowIcon} />
</a>
<a
href="https://twitter.com/blitz_js"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Blitz Twitter
<span className={styles.arrowIcon} />
</a>
<a
href="https://discord.blitzjs.com"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Discord Community
<span className={styles.arrowIcon} />
</a>
</div>
</div>
</div>
</main>
<footer>
<footer className={styles.footer}>
<span>Powered by</span>
<a
href="https://blitzjs.com?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
className={styles.textLink}
>
Powered by Blitz.js
Blitz.js
</a>
</footer>
<style jsx global>{`
@import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;700&display=swap");
html,
body {
padding: 0;
margin: 0;
font-family: "Libre Franklin", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
p {
text-align: center;
}
footer {
width: 100%;
height: 60px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
background-color: #45009d;
}
footer a {
display: flex;
justify-content: center;
align-items: center;
}
footer a {
color: #f4f4f4;
text-decoration: none;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
width: 300px;
}
.buttons {
display: grid;
grid-auto-flow: column;
grid-gap: 0.5rem;
}
.button {
font-size: 1rem;
background-color: #6700eb;
padding: 1rem 2rem;
color: #f4f4f4;
text-align: center;
}
.button.small {
padding: 0.5rem 1rem;
}
.button:hover {
background-color: #45009d;
}
.button-outline {
border: 2px solid #6700eb;
padding: 1rem 2rem;
color: #6700eb;
text-align: center;
}
.button-outline:hover {
border-color: #45009d;
color: #45009d;
}
pre {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
text-align: center;
}
code {
font-size: 0.9rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
`}</style>
</div>
</Layout>
)

View File

@@ -0,0 +1,317 @@
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.main {
flex: 1;
padding: 0rem 1rem;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.wrapper {
display: flex;
flex-direction: column;
gap: 2rem;
}
.frost {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.31);
}
.header {
display: flex;
flex-direction: column;
padding: 4rem 0rem 2rem 0rem;
text-align: center;
gap: 2rem;
}
.header h1 {
max-width: 620px;
align-self: center;
}
.body {
composes: frost;
border-radius: var(--border-radius);
display: flex;
padding: 1rem;
width: 100%;
flex-direction: row;
align-self: center;
max-width: var(--screen-width);
}
.instructions {
display: inline-flex;
flex-direction: column;
padding: 1rem;
width: 100%;
}
.globe {
position: fixed;
width: 350vmin;
height: 75vmin;
left: 20%;
top: 50%;
transform: translate(-50%, calc(-50% + 40px));
z-index: -1;
border-radius: 100%;
background-image: radial-gradient(95.63% 95.63% at 95.92% 0%, rgba(255, 255, 255, 0.62) 0%, #8155ff38 60.42%, #002fff5c 169%);
filter: blur(8vmin);
}
.footer {
display: flex;
padding: 2rem 0;
justify-content: center;
align-items: center;
}
.footer span {
margin-right: 0.2rem;
}
.code {
display: flex;
align-items: center;
gap: 0.5rem;
}
.code span {
background: rgba(124, 58, 237, 50%);
border-radius: 50rem;
font-size: 14px;
font-weight: 500;
padding: 17px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: rgb(57, 33, 97);
}
.code pre {
background: rgba(124, 58, 237, 12%);
border-radius: 4px;
padding: 0.7em 1.4em;
text-align: center;
}
.code code {
font-size: 0.86em;
font-weight: bold;
color: rgb(124, 58, 237);
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.toastContainer {
border: 1px solid #edff;
padding: 0 1rem;
background: #eeff;
color: #62af;
text-align: center;
}
.toastContainer strong {
color: rgb(124, 58, 237);
}
.textLink {
color: rgb(124, 58, 237);
background:
linear-gradient(
to right,
rgba(231, 216, 246, 1),
rgba(231, 216, 246, 1)
),
linear-gradient(
to right,
rgba(99, 1, 235, 1),
rgba(124, 58, 237, 1),
rgba(231, 216, 246, 1)
);
background-size: 100% 1px, 0 1px;
background-position: 100% 100%, 0 100%;
background-repeat: no-repeat;
transition: background-size 400ms;
}
.textLink:hover,
.textLink:focus,
.textLink:active {
background-size: 0 1px, 100% 1px;
}
.arrowIcon {
box-sizing: border-box;
display: block;
width: 8px;
height: 8px;
border-top: 2px solid;
transform: scale(var(--ggs,1));
border-right: 2px solid;
position: absolute;
right: 6px;
top: 6px;
color:#b1a5c4;
}
.arrowIcon::after {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
height: 2px;
background: currentColor;
transform: rotate(-45deg);
top: 2px;
right: -1px
}
.buttonContainer {
display: flex;
flex-direction: row;
gap: 1rem;
justify-content: center;
align-items: center;
flex: 1;
}
.button {
background: linear-gradient(to top, rgb(124, 58, 237), rgb(117, 81, 236));
border: 1px solid rgb(231, 216, 246);
color: white;
text-shadow: rgba(0, 0, 0, 0.25) 0px 3px 8px;
padding: 0 24px;
height: 48px;
width: 200px;
max-width: 300px;
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
user-select: none;
white-space: nowrap;
border-radius: 0.75rem;
border-bottom-left-radius: 0px;
font-size: 15px;
transition: all 0.3s ease 0s;
cursor: pointer;
}
.button:hover {
color: white;
text-shadow: rgb(0 0 0 / 56%) 0px 3px 12px;
box-shadow: rgb(80 63 205 / 50%) 0px 1px 40px;
}
.loginButton {
composes: button;
background: rgb(248 250 252);
border: 1px solid rgb(231, 216, 246);
color: rgb(30 41 59);
text-shadow: none;
}
.loginButton:hover {
color: rgb(30 41 59);
text-shadow: none;
}
.card:hover .arrowIcon {
color: #7450ec;
}
.linkGrid {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 1rem;
}
.card {
composes: frost;
padding: 1rem 0rem;
text-align: center;
color: inherit;
text-decoration: none;
border-radius: 10px;
border-bottom-left-radius: 0px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 200px;
min-width: 200px;
display: flex;
flex-direction: row;
justify-content: center;
}
.card:hover,
.card:focus,
.card:active {
color: #7450ec;
border-color: #7450ec;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
flex: 1;
padding: 1rem 2rem;
}
.logo svg {
height: 100%;
width: 200px;
fill: #7450ec;
}
/* MOBILE */
@media(max-width:800px) {
.linkGrid {
width: 100%;
}
.card {
max-width: 100%;
}
.body {
flex-wrap: wrap;
}
.buttonContainer {
flex-wrap: wrap;
}
}

View File

@@ -0,0 +1,26 @@
:root {
--border-radius: 0.75rem;
--screen-width: 90vmin;
}
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
i {
font-size: 0.8rem;
}
* {
box-sizing: border-box;
}

View File

@@ -84,6 +84,7 @@ export const mockRouter: NextRouter = {
isReady: true,
isLocaleDomain: false,
isPreview: false,
forward: vi.fn(),
push: vi.fn(),
replace: vi.fn(),
reload: vi.fn(),

View File

@@ -1,13 +1,14 @@
import React, {Suspense} from "react"
import {Form, FormProps} from "src/core/components/Form"
import {LabeledTextField} from "src/core/components/LabeledTextField"
import {z} from "zod"
export {FORM_ERROR} from "src/core/components/Form"
export function __ModelName__Form<S extends z.ZodType<any, any>>(props: FormProps<S>) {
return (
<Form<S> {...props}>
<LabeledTextField name="name" label="Name" placeholder="Name" />
{/* template: <__component__ name="__fieldName__" label="__Field_Name__" placeholder="__Field_Name__" type="__inputType__" /> */}
</Form>
)
}

View File

@@ -20,7 +20,7 @@
"dependencies": {
"@blitzjs/next": "latest",
"blitz": "latest",
"next": "12.2.5",
"next": "13.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"ts-node": "10.9.1"

View File

@@ -20,7 +20,7 @@
"dependencies": {
"@blitzjs/next": "latest",
"blitz": "latest",
"next": "12.2.5",
"next": "13.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"ts-node": "10.9.1"

View File

@@ -1,195 +1,108 @@
import Image from 'next/image'
import Layout from "src/core/layouts/Layout"
import { BlitzPage } from "@blitzjs/next"
import logo from "public/logo.png"
import styles from 'src/styles/Home.module.css'
/*
* This file is just for a pleasant getting started page for your new app.
* You can delete everything in here and start from scratch if you like.
*/
const Home: BlitzPage = () => {
return (
<div className="container">
<main>
<div className="logo">
<Image src={logo} alt="blitzjs" />
<Layout title="Home">
<div className={styles.globe} />
<div className={styles.container}>
<div className={styles.toastContainer}>
<p>
<strong>Congrats!</strong> Your app is ready.
</p>
</div>
<p>
<strong>Congrats!</strong> Your app is ready.
</p>
<div className="buttons" style={{ marginTop: "5rem" }}>
<main className={styles.main}>
<div className={styles.wrapper}>
<div className={styles.header}>
<div className={styles.logo}>
<svg viewBox="0 0 165 66"><path d="M104.292 56.033C104.292 56.408 104.206 56.6636 104.036 56.8C103.9 56.9363 103.627 57.0045 103.218 57.0045H99.7409C99.4001 57.0045 99.1615 56.9533 99.0251 56.8511C98.8888 56.7147 98.8206 56.4932 98.8206 56.1864L98.9229 19.8324C98.9229 19.3211 99.1444 19.0654 99.5876 19.0654H103.627C103.839 19.0654 104.292 19.0672 104.292 19.0672V19.8324V56.033ZM64.3531 57.0081C64.1145 57.0081 63.927 56.9399 63.7906 56.8035C63.6543 56.6672 63.5861 56.4968 63.5861 56.2922V19.9383C63.5861 19.3588 63.8588 19.069 64.4042 19.069H76.829C81.533 19.069 85.1463 19.9212 87.6687 21.6256C90.1912 23.2958 91.4524 25.7331 91.4524 28.9373C91.4524 30.9484 90.924 32.6528 89.8673 34.0504C88.8106 35.4138 87.1063 36.5217 84.7543 37.3739C84.6179 37.4079 84.5497 37.4932 84.5497 37.6295C84.5497 37.7318 84.6179 37.7999 84.7543 37.834C87.2767 38.5158 89.1686 39.5895 90.4298 41.0553C91.7251 42.521 92.3727 44.4469 92.3727 46.833C92.3727 50.2418 91.0945 52.7983 88.5379 54.5027C85.9814 56.1729 82.2318 57.0081 77.2892 57.0081H64.3531ZM77.5448 35.5843C79.6923 35.5843 81.516 35.1071 83.0158 34.1526C84.5157 33.1982 85.2656 31.6983 85.2656 29.6531C85.2656 27.6079 84.5157 26.0569 83.0158 25.0002C81.5501 23.9435 79.5219 23.4151 76.9313 23.4151H70.5399C70.0286 23.4151 69.7729 23.6367 69.7729 24.0798V34.8684C69.7729 35.3457 69.9604 35.5843 70.3354 35.5843H77.5448ZM77.0335 52.662C82.9647 52.662 85.9303 50.5997 85.9303 46.4751C85.9303 44.3276 85.1633 42.7255 83.6294 41.6688C82.0955 40.6121 80.0673 40.0838 77.5448 40.0838H70.591C70.2843 40.0838 70.0627 40.1349 69.9263 40.2372C69.8241 40.3394 69.7729 40.5099 69.7729 40.7485V51.895C69.7729 52.4063 69.9604 52.662 70.3354 52.662H77.0335ZM142.707 56.8624C142.81 56.9647 142.997 57.0158 143.27 57.0158H163.876C164.387 57.0158 164.643 56.7772 164.643 56.3V53.948V53.3344H163.978H149.866C149.593 53.3344 149.457 53.2492 149.457 53.0788C149.457 52.9765 149.508 52.8572 149.61 52.7208L163.876 33.8536C164.251 33.2741 164.438 32.7628 164.438 32.3197V30.479V29.9144C164.438 29.9144 164.051 29.9165 163.876 29.9165H144.241C143.866 29.9165 143.679 30.121 143.679 30.5301V32.831C143.679 33.1037 143.713 33.2911 143.781 33.3934C143.883 33.4957 144.071 33.5468 144.344 33.5468H157.075C157.382 33.5468 157.535 33.632 157.535 33.8025L157.382 34.1092L143.219 52.9765C142.946 53.3515 142.759 53.6412 142.656 53.8457C142.588 54.0502 142.554 54.3059 142.554 54.6127V56.3C142.554 56.5727 142.605 56.7602 142.707 56.8624ZM116.929 19.0676H111.51V27.7684C114.503 27.7684 116.929 25.3419 116.929 22.3486V19.0676ZM116.926 56.0308C116.926 56.4058 116.841 56.6614 116.67 56.7978C116.534 56.9341 116.278 57.0023 115.903 57.0023H112.427C112.086 57.0023 111.847 56.9512 111.711 56.8489C111.574 56.7126 111.506 56.491 111.506 56.1842V30.6699C111.506 30.3972 111.557 30.2098 111.66 30.1075C111.762 29.9712 111.949 29.903 112.222 29.903H117.028L116.926 56.0308ZM132.183 34.3137C132.183 33.9728 132.336 33.8024 132.643 33.8024H138.779C139.256 33.8024 139.495 33.5979 139.495 33.1888V30.4789V29.9165H138.881H132.745C132.439 29.9165 132.285 29.7631 132.285 29.4563V21.531V20.713L131.621 20.7129H128.093C127.752 20.7129 127.547 20.9515 127.479 21.4288L126.865 29.4563C126.865 29.7631 126.729 29.9165 126.456 29.9165H122.366C121.957 29.9165 121.752 30.1039 121.752 30.4789V33.1888C121.752 33.5979 121.974 33.8024 122.417 33.8024H126.252C126.593 33.8024 126.763 34.0069 126.763 34.416V50.6244C126.763 52.806 127.309 54.4252 128.399 55.4819C129.49 56.5045 131.16 57.0158 133.41 57.0158C135.796 57.0158 137.535 56.9306 138.625 56.7601C139.137 56.6579 139.392 56.3681 139.392 55.8909V53.6923V53.0787H138.779H135.507C134.348 53.0787 133.495 52.806 132.95 52.2606C132.439 51.7152 132.183 50.7267 132.183 49.295V34.3137Z"></path><path d="M0.241243 33.2639H10.9742C15.0585 33.2639 18.9054 35.1835 21.3612 38.4471L31.9483 52.5165C32.1484 52.7824 32.1786 53.1393 32.026 53.435L25.9232 65.2592C25.6304 65.8265 24.8455 65.8932 24.4612 65.3835L0.241243 33.2639Z"></path><path d="M42.4727 33.2822H31.7398C27.6555 33.2822 23.8086 31.3626 21.3528 28.0991L10.7656 14.0297C10.5656 13.7638 10.5354 13.4068 10.688 13.1111L16.7908 1.28696C17.0836 0.719654 17.8684 0.652924 18.2528 1.16266L42.4727 33.2822Z"></path></svg>
</div>
</div>
<div className={styles.body}>
{/* Links */}
<div className={styles.linkGrid}>
<a
href="https://blitzjs.com/docs/getting-started?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Blitz Docs
<span className={styles.arrowIcon} />
</a>
<a
href="https://nextjs.org/docs/getting-started"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Next.js Docs
<span className={styles.arrowIcon} />
</a>
<a
href="https://github.com/blitz-js/blitz"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Github Repo
<span className={styles.arrowIcon} />
</a>
<a
href="https://twitter.com/blitz_js"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Blitz Twitter
<span className={styles.arrowIcon} />
</a>
<a
href="https://discord.blitzjs.com"
target="_blank"
rel="noopener noreferrer"
className={styles.card}
>
Discord Community
<span className={styles.arrowIcon} />
</a>
</div>
</div>
</div>
</main>
<footer className={styles.footer}>
<span>Powered by</span>
<a
className="button"
href="https://blitzjs.com/docs/getting-started?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
href="https://blitzjs.com?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
className={styles.textLink}
>
Documentation
Blitz.js
</a>
<a
className="button-outline"
href="https://github.com/blitz-js/blitz"
target="_blank"
rel="noopener noreferrer"
>
Github Repo
</a>
<a
className="button-outline"
href="https://discord.blitzjs.com"
target="_blank"
rel="noopener noreferrer"
>
Discord Community
</a>
</div>
</main>
<footer>
<a
href="https://blitzjs.com?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
>
Powered by Blitz.js
</a>
</footer>
<style jsx global>{`
@import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;700&display=swap");
html,
body {
padding: 0;
margin: 0;
font-family: "Libre Franklin", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
p {
text-align: center;
}
footer {
width: 100%;
height: 60px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
background-color: #45009d;
}
footer a {
display: flex;
justify-content: center;
align-items: center;
}
footer a {
color: #f4f4f4;
text-decoration: none;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
width: 300px;
}
.buttons {
display: grid;
grid-auto-flow: column;
grid-gap: 0.5rem;
}
.button {
font-size: 1rem;
background-color: #6700eb;
padding: 1rem 2rem;
color: #f4f4f4;
text-align: center;
}
.button.small {
padding: 0.5rem 1rem;
}
.button:hover {
background-color: #45009d;
}
.button-outline {
border: 2px solid #6700eb;
padding: 1rem 2rem;
color: #6700eb;
text-align: center;
}
.button-outline:hover {
border-color: #45009d;
color: #45009d;
}
pre {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
text-align: center;
}
code {
font-size: 0.9rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
`}</style>
</div>
</footer>
</div>
</Layout>
)
}
export default Home

View File

@@ -0,0 +1,232 @@
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.main {
flex: 1;
padding: 0rem 1rem;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.wrapper {
display: flex;
flex-direction: column;
gap: 2rem;
}
.frost {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.31);
}
.header {
display: flex;
flex-direction: column;
padding: 4rem 0rem 2rem 0rem;
text-align: center;
gap: 2rem;
}
.header h1 {
max-width: 620px;
align-self: center;
}
.body {
composes: frost;
border-radius: var(--border-radius);
display: flex;
padding: 1rem;
width: 100%;
flex-direction: row;
align-self: center;
max-width: var(--screen-width);
}
.globe {
position: fixed;
width: 350vmin;
height: 75vmin;
left: 20%;
top: 50%;
transform: translate(-50%, calc(-50% + 40px));
z-index: -1;
border-radius: 100%;
background-image: radial-gradient(95.63% 95.63% at 95.92% 0%, rgba(255, 255, 255, 0.62) 0%, #8155ff38 60.42%, #002fff5c 169%);
filter: blur(8vmin);
}
.footer {
display: flex;
padding: 2rem 0;
justify-content: center;
align-items: center;
}
.footer span {
margin-right: 0.2rem;
}
.toastContainer {
border: 1px solid #edff;
padding: 0 1rem;
background: #eeff;
color: #62af;
text-align: center;
}
.toastContainer strong {
color: rgb(124, 58, 237);
}
.textLink {
color: rgb(124, 58, 237);
background:
linear-gradient(
to right,
rgba(231, 216, 246, 1),
rgba(231, 216, 246, 1)
),
linear-gradient(
to right,
rgba(99, 1, 235, 1),
rgba(124, 58, 237, 1),
rgba(231, 216, 246, 1)
);
background-size: 100% 1px, 0 1px;
background-position: 100% 100%, 0 100%;
background-repeat: no-repeat;
transition: background-size 400ms;
}
.textLink:hover,
.textLink:focus,
.textLink:active {
background-size: 0 1px, 100% 1px;
}
.arrowIcon {
box-sizing: border-box;
display: block;
width: 8px;
height: 8px;
border-top: 2px solid;
transform: scale(var(--ggs,1));
border-right: 2px solid;
position: absolute;
right: 6px;
top: 6px;
color:#b1a5c4;
}
.arrowIcon::after {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
height: 2px;
background: currentColor;
transform: rotate(-45deg);
top: 2px;
right: -1px
}
.buttonContainer {
display: flex;
flex-direction: row;
gap: 1rem;
justify-content: center;
align-items: center;
flex: 1;
}
.card:hover .arrowIcon {
color: #7450ec;
}
.linkGrid {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 1rem;
width: 100%;
}
.card {
composes: frost;
padding: 1rem 0rem;
text-align: center;
color: inherit;
text-decoration: none;
border-radius: 10px;
border-bottom-left-radius: 0px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 100%;
min-width: 200px;
display: flex;
flex-direction: row;
justify-content: center;
}
.card:hover,
.card:focus,
.card:active {
color: #7450ec;
border-color: #7450ec;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
flex: 1;
padding: 1rem 2rem;
}
.logo svg {
height: 100%;
width: 200px;
fill: #7450ec;
}
/* MOBILE */
@media(max-width:800px) {
.linkGrid {
width: 100%;
}
.card {
max-width: 100%;
}
.body {
flex-wrap: wrap;
}
.buttonContainer {
flex-wrap: wrap;
}
}

View File

@@ -0,0 +1,26 @@
:root {
--border-radius: 0.75rem;
--screen-width: 90vmin;
}
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
i {
font-size: 0.8rem;
}
* {
box-sizing: border-box;
}

View File

@@ -1,5 +1,5 @@
import { resolver } from "@blitzjs/rpc"
import db from "db"
import db from "__prismaFolder__"
import {z} from "zod"
const __Name__ = z.object({

View File

@@ -1,15 +1,15 @@
import { resolver } from "@blitzjs/rpc"
import db from "db"
import db from "__prismaFolder__"
import {z} from "zod"
if (process.env.parentModel) {
const Create__ModelName__ = z.object({
name: z.string(),
__parentModelId__: z.number()
__parentModelId__: z.__parentModelIdZodType__(),
// template: __fieldName__: z.__zodType__(),
})
} else {
const Create__ModelName__ = z.object({
name: z.string(),
// template: __fieldName__: z.__zodType__(),
})
}

View File

@@ -1,9 +1,9 @@
import { resolver } from "@blitzjs/rpc"
import db from "db"
import db from "__prismaFolder__"
import {z} from "zod"
const Delete__ModelName__ = z.object({
id: z.number(),
id: z.__modelIdZodType__(),
})
export default resolver.pipe(

View File

@@ -1,10 +1,10 @@
import { resolver } from "@blitzjs/rpc"
import db from "db"
import db from "__prismaFolder__"
import {z} from "zod"
const Update__ModelName__ = z.object({
id: z.number(),
name: z.string(),
id: z.__modelIdZodType__(),
// template: __fieldName__: z.__zodType__(),
})
export default resolver.pipe(

View File

@@ -36,7 +36,7 @@ export const Edit__ModelName__ = () => {
<div>
<h1>Edit __ModelName__ {__modelName__.id}</h1>
<pre>{JSON.stringify(__modelName__, null, 2)}</pre>
<Suspense fallback={<div>Loading...</div>}>
<__ModelName__Form
submitText="Update __ModelName__"
// TODO use a zod schema for form validation
@@ -64,6 +64,7 @@ export const Edit__ModelName__ = () => {
}
}}
/>
</Suspense>
</div>
</>
)

View File

@@ -12,6 +12,7 @@ if (process.env.parentModel) {
import Layout from "src/core/layouts/Layout"
import create__ModelName__ from "src/__modelNamesPath__/mutations/create__ModelName__"
import {__ModelName__Form, FORM_ERROR} from "src/__modelNamesPath__/components/__ModelName__Form"
import { Suspense } from 'react'
const New__ModelName__Page = () => {
const router = useRouter()
@@ -23,7 +24,7 @@ const New__ModelName__Page = () => {
return (
<Layout title={"Create New __ModelName__"}>
<h1>Create New __ModelName__</h1>
<Suspense fallback={<div>Loading...</div>}>
<__ModelName__Form
submitText="Create __ModelName__"
// TODO use a zod schema for form validation
@@ -51,7 +52,7 @@ const New__ModelName__Page = () => {
}
}}
/>
</Suspense>
<p>
<if condition="parentModel">
<Link href={Routes.__ModelNames__Page({ __parentModelId__: __parentModelId__! })}>

View File

@@ -1,6 +1,6 @@
import {NotFoundError} from "blitz"
import { resolver } from "@blitzjs/rpc"
import db from "db"
import db from "__prismaFolder__"
import {z} from "zod"
const Get__ModelName__ = z.object({

View File

@@ -1,6 +1,6 @@
import {paginate} from "blitz"
import { resolver } from "@blitzjs/rpc"
import db, {Prisma} from "db"
import db, {Prisma} from "__prismaFolder__"
interface Get__ModelNames__Input
extends Pick<Prisma.__ModelName__FindManyArgs, "where" | "orderBy" | "skip" | "take"> {}

View File

@@ -1,5 +1,5 @@
import { resolver } from "@blitzjs/rpc"
import db from "db"
import db from "__prismaFolder__"
import {z} from "zod"
const __Name__ = z.object({

View File

@@ -0,0 +1,254 @@
import {FieldValuesBuilder} from "../../../src/generators/template-builders/field-values-builder"
import {describe, it, expect} from "vitest"
describe("Form Generator", () => {
process.env.BLITZ_APP_DIR = process.cwd()
const generator = new FieldValuesBuilder()
it("Should work with two word field names", async () => {
expect(
await generator.getFieldTemplateValues(["orgName:string", "orgId:integer"]),
).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "OrgName",
Field_Name: "Org Name",
Field_name: "Org name",
attributeName: "orgName",
fieldName: "orgName",
field_name: "org name",
zodType: "string",
prismaType: "String",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "OrgId",
Field_Name: "Org Id",
Field_name: "Org id",
attributeName: "orgId",
fieldName: "orgId",
field_name: "org id",
zodType: "string",
prismaType: "String",
inputType: "text",
},
])
})
it("Should work with simple types", async () => {
expect(
await generator.getFieldTemplateValues(["field1:string", "field2:string"]),
).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "Field1",
Field_Name: "Field1",
Field_name: "Field1",
attributeName: "field1",
fieldName: "field1",
field_name: "field1",
zodType: "string",
prismaType: "String",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field2",
Field_Name: "Field2",
Field_name: "Field2",
attributeName: "field2",
fieldName: "field2",
field_name: "field2",
zodType: "string",
prismaType: "String",
inputType: "text",
},
])
})
it("Should work with optional types", async () => {
expect(
await generator.getFieldTemplateValues(["field1:string?", "field2:number"]),
).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "Field1",
Field_Name: "Field1",
Field_name: "Field1",
attributeName: "field1",
fieldName: "field1",
field_name: "field1",
zodType: "string",
prismaType: "String",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field2",
Field_Name: "Field2",
Field_name: "Field2",
attributeName: "field2",
fieldName: "field2",
field_name: "field2",
zodType: "number",
prismaType: "Int",
inputType: "number",
},
])
})
it("Should work with default values", async () => {
expect(await generator.getFieldTemplateValues(["field1:string:default=test"])).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "Field1",
Field_Name: "Field1",
Field_name: "Field1",
attributeName: "field1",
fieldName: "field1",
field_name: "field1",
zodType: "string",
prismaType: "String",
inputType: "text",
},
])
})
it("Should work with different input types", async () => {
const fields = [
"field1:string?",
"field2:boolean",
"field3:int",
"field4:number",
"field5:bigint?",
"field6:float",
"field7:decimal",
"field8:datetime",
"field9:uuid",
"field10:json?",
]
expect(await generator.getFieldTemplateValues(fields)).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "Field1",
Field_Name: "Field1",
Field_name: "Field1",
attributeName: "field1",
fieldName: "field1",
field_name: "field1",
zodType: "string",
prismaType: "String",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field2",
Field_Name: "Field2",
Field_name: "Field2",
attributeName: "field2",
fieldName: "field2",
field_name: "field2",
zodType: "boolean",
prismaType: "Boolean",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field3",
Field_Name: "Field3",
Field_name: "Field3",
attributeName: "field3",
fieldName: "field3",
field_name: "field3",
zodType: "number",
prismaType: "Int",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field4",
Field_Name: "Field4",
Field_name: "Field4",
attributeName: "field4",
fieldName: "field4",
field_name: "field4",
zodType: "number",
prismaType: "Int",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field5",
Field_Name: "Field5",
Field_name: "Field5",
attributeName: "field5",
fieldName: "field5",
field_name: "field5",
zodType: "number",
prismaType: "BigInt",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field6",
Field_Name: "Field6",
Field_name: "Field6",
attributeName: "field6",
fieldName: "field6",
field_name: "field6",
zodType: "number",
prismaType: "Float",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field7",
Field_Name: "Field7",
Field_name: "Field7",
attributeName: "field7",
fieldName: "field7",
field_name: "field7",
zodType: "number",
prismaType: "Decimal",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field8",
Field_Name: "Field8",
Field_name: "Field8",
attributeName: "field8",
fieldName: "field8",
field_name: "field8",
zodType: "string().datetime()",
prismaType: "DateTime",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field9",
Field_Name: "Field9",
Field_name: "Field9",
attributeName: "field9",
fieldName: "field9",
field_name: "field9",
zodType: "string().uuid",
prismaType: "String",
inputType: "text",
default: "uuid",
},
{
component: "LabeledTextField",
FieldName: "Field10",
Field_Name: "Field10",
Field_name: "Field10",
attributeName: "field10",
fieldName: "field10",
field_name: "field10",
zodType: "any",
prismaType: "Json",
inputType: "text",
},
])
})
})

View File

@@ -0,0 +1,50 @@
import {FormGenerator} from "../../src/generators/form-generator"
import {describe, it, expect} from "vitest"
describe("Form Generator", () => {
process.env.BLITZ_APP_DIR = process.cwd()
const generator = new FormGenerator({
ModelName: "project",
ModelNames: "projects",
modelName: "project",
modelNames: "projects",
extraArgs: ["myProjectName:string"],
})
it("Correctly generates field names", async () => {
const templateValues = await generator.getTemplateValues()
expect(templateValues.fieldTemplateValues[0].fieldName).toEqual("myProjectName")
expect(templateValues.fieldTemplateValues[0].FieldName).toEqual("MyProjectName")
expect(templateValues.fieldTemplateValues[0].field_name).toEqual("my project name")
expect(templateValues.fieldTemplateValues[0].Field_name).toEqual("My project name")
expect(templateValues.fieldTemplateValues[0].Field_Name).toEqual("My Project Name")
})
it("matches template comments correctly", () => {
const regex = generator.fieldTemplateRegExp
const curlyBraceComment1 = `{/* template: <__component__ name="__fieldName__" label="__Field_Name__" placeholder="__Field_Name__" /> */}`
expect(curlyBraceComment1.match(regex)?.[0]?.replace(regex, "$2$3")).toBe(
`<__component__ name="__fieldName__" label="__Field_Name__" placeholder="__Field_Name__" />`,
)
expect(curlyBraceComment1.match(regex)?.[0]?.replace(regex, "$2$3")).not.toBe(
`something Random`,
)
const normalComment1 = `// template: __fieldName__: z.__zodType__(),`
expect(normalComment1.match(regex)?.[0]?.replace(regex, "$2$3")).toBe(
`__fieldName__: z.__zodType__(),`,
)
expect(normalComment1.match(regex)?.[0]?.replace(regex, "$2$3")).not.toBe(`something Random`)
const commentWithSpacing = `// template: __fieldName__: z.__zodType__(),`
const commentWithNoSpacing = `//template: __fieldName__: z.__zodType__(),`
expect(commentWithSpacing.match(regex)?.[0]?.replace(regex, "$2$3")).toBe(
`__fieldName__: z.__zodType__(),`,
)
expect(commentWithNoSpacing.match(regex)?.[0]?.replace(regex, "$2$3")).toBe(
`__fieldName__: z.__zodType__(),`,
)
})
})

View File

@@ -39,6 +39,7 @@ describe("PageGenerator", () => {
ParentModels: undefined,
modelNamesPath: "projects",
modelId: "projectId",
modelIdZodType: "number",
modelIdParam: "[projectId]",
modelName: "project",
modelNames: "projects",
@@ -46,6 +47,7 @@ describe("PageGenerator", () => {
parentModelId: "",
parentModelParam: "",
parentModels: undefined,
prismaFolder: "db",
})
})

View File

@@ -3,75 +3,75 @@ import {Schema} from "@mrleebo/prisma-ast"
import {Field} from "../../src/prisma/field"
describe("Field", () => {
it("parses optional types", () => {
const [field] = Field.parse("name:string?")
it("parses optional types", async () => {
const [field] = await Field.parse("name:string?")
expect(field?.isRequired).toBe(false)
})
it("appends unique attribute", () => {
const [field] = Field.parse("email:string?:unique")
it("appends unique attribute", async () => {
const [field] = await Field.parse("email:string?:unique")
expect(field?.isUnique).toBe(true)
})
it("appends updatedAt attribute", () => {
const [field] = Field.parse("updatedAt:DateTime:updatedAt")
it("appends updatedAt attribute", async () => {
const [field] = await Field.parse("updatedAt:DateTime:updatedAt")
expect(field?.isUpdatedAt).toBe(true)
})
it("handles default simple attribute", () => {
const [field] = Field.parse("isActive:boolean:default=true")
it("handles default simple attribute", async () => {
const [field] = await Field.parse("isActive:boolean:default=true")
expect(field?.default).toBe("true")
})
it("handles default uuid attribute", () => {
const [field] = Field.parse("id:string:default=uuid")
it("handles default uuid attribute", async () => {
const [field] = await Field.parse("id:string:default=uuid")
expect(field?.default).toMatchObject({name: "uuid"})
})
it("handles uuid convenience syntax", () => {
const [field] = Field.parse("someSpecialToken:uuid")
it("handles uuid convenience syntax", async () => {
const [field] = await Field.parse("someSpecialToken:uuid")
expect(field?.type).toBe("String")
expect(field?.default).toMatchObject({name: "uuid"})
})
it("handles default autoincrement attribute", () => {
const [field] = Field.parse("id:int:default=autoincrement")
it("handles default autoincrement attribute", async () => {
const [field] = await Field.parse("id:int:default=autoincrement")
expect(field?.default).toMatchObject({name: "autoincrement"})
})
it("has default field type", () => {
const [field] = Field.parse("name")
it("has default field type", async () => {
const [field] = await Field.parse("name")
expect(field?.type).toBe("String")
})
it("allow number characters in model name", () => {
const [field] = Field.parse("name2")
it("allow number characters in model name", async () => {
const [field] = await Field.parse("name2")
expect(field?.name).toBe("name2")
})
it("allow underscore characters in model name", () => {
const [field] = Field.parse("first_name")
it("allow underscore characters in model name", async () => {
const [field] = await Field.parse("first_name")
expect(field?.name).toBe("first_name")
})
it("disallows number as a first character in model name", () => {
expect(() => Field.parse("2first")).toThrow()
it("disallows number as a first character in model name", async () => {
await expect(async () => await Field.parse("2first")).rejects.toThrow()
})
it("disallows underscore as a first character in model name", () => {
expect(() => Field.parse("_first")).toThrow()
it("disallows underscore as a first character in model name", async () => {
await expect(async () => await Field.parse("_first")).rejects.toThrow()
})
it("disallows special characters in model name", () => {
expect(() => Field.parse("app-user:int")).toThrow()
it("disallows special characters in model name", async () => {
await expect(async () => await Field.parse("app-user:int")).rejects.toThrow()
})
it("disallows optional list fields", () => {
expect(() => Field.parse("users:int?[]")).toThrow()
it("disallows optional list fields", async () => {
await expect(async () => await Field.parse("users:int?[]")).rejects.toThrow()
})
it("requires a name", () => {
expect(() => Field.parse(":int")).toThrow()
it("requires a name", async () => {
await expect(async () => await Field.parse(":int")).rejects.toThrow()
})
describe("belongsTo", () => {
@@ -103,8 +103,8 @@ describe("Field", () => {
],
}
it("simple relation", () => {
const [relation, foreignKey] = Field.parse("belongsTo:task")
it("simple relation", async () => {
const [relation, foreignKey] = await Field.parse("belongsTo:task")
expect(relation).toMatchObject({
name: "task",
type: "Task",
@@ -114,8 +114,8 @@ describe("Field", () => {
expect(foreignKey).toMatchObject({name: "taskId", type: "Int"})
})
it("relation with schema", () => {
const [relation, foreignKey] = Field.parse("belongsTo:project?", schema)
it("relation with schema", async () => {
const [relation, foreignKey] = await Field.parse("belongsTo:project?", schema)
expect(relation).toMatchObject({
name: "project",
type: "Project",
@@ -126,8 +126,8 @@ describe("Field", () => {
expect(foreignKey).toMatchObject({name: "projectId", type: "String", isRequired: false})
})
it("relation with list directive", () => {
const [relation, foreignKey] = Field.parse("belongsTo:tasks[]", schema)
it("relation with list directive", async () => {
const [relation, foreignKey] = await Field.parse("belongsTo:tasks[]", schema)
expect(relation).toMatchObject({name: "tasks", type: "Task", isList: false})
expect(foreignKey).toMatchObject({name: "tasksId", type: "Int", isList: false})
})

View File

@@ -3,18 +3,13 @@ import {Field} from "../../src/prisma/field"
import {Model} from "../../src/prisma/model"
describe("Prisma Model", () => {
it("generates a proper model", () => {
expect(
new Model(
"user",
[
Field.parse("email:string:unique"),
Field.parse("updated:dateTime:updatedAt"),
Field.parse("recentLogins:dateTime[]"),
Field.parse("twoFactorEnabled:boolean"),
Field.parse("twoFactorMethod:string?"),
].flat(),
).toString(),
).toMatchSnapshot()
it("generates a proper model", async () => {
const email = await Field.parse("email:string:unique")
const updated = await Field.parse("updated:dateTime:updatedAt")
const recentLogins = await Field.parse("recentLogins:dateTime[]")
const twoFactorEnabled = await Field.parse("twoFactorEnabled:boolean")
const twoFactorMethod = await Field.parse("twoFactorMethod:string?")
const fields = [email, updated, recentLogins, twoFactorEnabled, twoFactorMethod].flat()
expect(new Model("user", fields).toString()).toMatchSnapshot()
})
})

43
pnpm-lock.yaml generated
View File

@@ -71,7 +71,7 @@ importers:
typescript: ^4.8.4
vite-tsconfig-paths: 3.6.0
vitest: 0.25.3
zod: 3.19.1
zod: 3.20.2
dependencies:
"@blitzjs/auth": link:../../packages/blitz-auth
"@blitzjs/config": link:../../packages/config
@@ -87,7 +87,7 @@ importers:
react-dom: 18.2.0_react@18.2.0
react-hook-form: 7.39.1_react@18.2.0
ts-node: 10.9.1_cbe7ovvae6zqfnmtgctpgpys54
zod: 3.19.1
zod: 3.20.2
devDependencies:
"@next/bundle-analyzer": 12.0.8
"@testing-library/jest-dom": 5.16.5
@@ -149,7 +149,7 @@ importers:
react-hook-form: 7.39.1
ts-node: 10.9.1
typescript: ^4.8.4
zod: 3.19.1
zod: 3.20.2
dependencies:
"@blitzjs/auth": link:../../packages/blitz-auth
"@blitzjs/config": link:../../packages/config
@@ -166,7 +166,7 @@ importers:
react-dom: 18.2.0_react@18.2.0
react-hook-form: 7.39.1_react@18.2.0
ts-node: 10.9.1_cbe7ovvae6zqfnmtgctpgpys54
zod: 3.19.1
zod: 3.20.2
devDependencies:
"@next/bundle-analyzer": 12.0.8
"@testing-library/react": 13.4.0_biqbaboplfbrettd7655fr4n2y
@@ -327,7 +327,7 @@ importers:
typescript: ^4.8.4
vite-tsconfig-paths: 3.6.0
vitest: 0.25.3
zod: 3.19.1
zod: 3.20.2
dependencies:
"@blitzjs/auth": link:../../packages/blitz-auth
"@blitzjs/config": link:../../packages/config
@@ -344,7 +344,7 @@ importers:
react-dom: 18.2.0_react@18.2.0
react-hook-form: 7.39.1_react@18.2.0
ts-node: 10.9.1_cbe7ovvae6zqfnmtgctpgpys54
zod: 3.19.1
zod: 3.20.2
devDependencies:
"@next/bundle-analyzer": 12.0.8
"@testing-library/jest-dom": 5.16.5
@@ -849,7 +849,7 @@ importers:
unbuild: 0.7.6
watch: 1.0.2
watchpack: 2.1.1
zod: 3.19.1
zod: 3.20.2
dependencies:
"@blitzjs/generator": link:../generator
"@mrleebo/prisma-ast": 0.2.6
@@ -923,7 +923,7 @@ importers:
typescript: 4.8.4
unbuild: 0.7.6_supports-color@8.1.1
watch: 1.0.2
zod: 3.19.1
zod: 3.20.2
packages/blitz-auth:
specifiers:
@@ -1077,7 +1077,7 @@ importers:
typescript: ^4.8.4
unbuild: 0.7.6
watch: 1.0.2
zod: 3.19.1
zod: 3.20.2
dependencies:
"@swc/core": 1.3.7
"@tanstack/react-query": 4.0.10_biqbaboplfbrettd7655fr4n2y
@@ -1100,7 +1100,7 @@ importers:
typescript: 4.8.4
unbuild: 0.7.6_supports-color@8.1.1
watch: 1.0.2
zod: 3.19.1
zod: 3.20.2
packages/codemod:
specifiers:
@@ -1194,6 +1194,7 @@ importers:
diff: 5.0.0
enquirer: 2.3.6
eslint: 8.27.0
fast-glob: 3.2.12
fs-extra: 10.0.1
globby: 13.1.2
got: ^11.8.1
@@ -1213,6 +1214,7 @@ importers:
username: 5.1.0
vinyl: 2.2.1
watch: 1.0.2
zod: 3.20.2
dependencies:
"@babel/core": 7.12.10_supports-color@8.1.1
"@babel/plugin-transform-typescript": 7.12.1_ps3yxa7qdojvlda5ukda3zlwie
@@ -1224,6 +1226,7 @@ importers:
cross-spawn: 7.0.3
diff: 5.0.0
enquirer: 2.3.6
fast-glob: 3.2.12
fs-extra: 10.0.1
globby: 13.1.2
got: 11.8.1
@@ -1262,6 +1265,7 @@ importers:
typescript: 4.8.4
unbuild: 0.6.9_supports-color@8.1.1
watch: 1.0.2
zod: 3.20.2
packages/pkg-template:
specifiers:
@@ -11971,6 +11975,19 @@ packages:
merge2: 1.4.1
micromatch: 4.0.5
/fast-glob/3.2.12:
resolution:
{
integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==,
}
engines: {node: ">=8.6.0"}
dependencies:
"@nodelib/fs.stat": 2.0.5
"@nodelib/fs.walk": 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.5
/fast-json-stable-stringify/2.1.0:
resolution:
{
@@ -12651,7 +12668,7 @@ packages:
dependencies:
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.2.11
fast-glob: 3.2.12
ignore: 5.2.0
merge2: 1.4.1
slash: 3.0.0
@@ -21306,8 +21323,8 @@ packages:
"@types/yoga-layout": 1.9.2
dev: false
/zod/3.19.1:
/zod/3.20.2:
resolution:
{
integrity: sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==,
integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==,
}