├── .babelrc ├── .env ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── update.yml ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── components ├── AuthGuard.tsx ├── Avatar.tsx ├── BreadCrumb.tsx ├── ManageMembers.tsx ├── NavBar.tsx ├── SpaceMembers.tsx ├── Spaces.tsx ├── TimeInfo.tsx ├── Todo.tsx ├── TodoList.tsx └── WithNavBar.tsx ├── lib ├── context.ts └── hooks │ ├── __model_meta.ts │ ├── account.ts │ ├── index.ts │ ├── list.ts │ ├── space-user.ts │ ├── space.ts │ ├── todo.ts │ └── user.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ ├── .DS_Store │ ├── auth │ │ └── [...nextauth].ts │ └── model │ │ └── [...path].ts ├── create-space.tsx ├── index.tsx ├── signin.tsx ├── signup.tsx └── space │ └── [slug] │ ├── [listId] │ └── index.tsx │ └── index.tsx ├── postcss.config.js ├── prisma ├── migrations │ ├── 20221014084317_init │ │ └── migration.sql │ ├── 20221020094651_upate_cli │ │ └── migration.sql │ ├── 20221103144245_drop_account_session │ │ └── migration.sql │ ├── 20221126150023_add_account │ │ └── migration.sql │ ├── 20221126151212_email_password_optional │ │ └── migration.sql │ ├── 20221126151510_refresh_token_expires │ │ └── migration.sql │ ├── 20221127033222_email_required │ │ └── migration.sql │ ├── 20230306121228_update │ │ └── migration.sql │ ├── 20230905035233_drop_aux_fields │ │ └── migration.sql │ ├── 20241222114017_add_space_owner │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── auth-bg.jpg ├── avatar.jpg └── logo.png ├── schema.zmodel ├── server ├── auth.ts ├── db.ts └── enhanced-db.ts ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── types ├── next-auth.d.ts └── next.d.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | // "superjson-next" plugin uses superjson for serialization between getServerSideProps and client, 4 | // so that types like Date and BigInt are properly handled 5 | "plugins": ["superjson-next"] 6 | } 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NEXTAUTH_SECRET=abc123 2 | DATABASE_URL="postgresql://postgres:abc123@localhost:5432/todo?schema=public" 3 | GITHUB_ID= 4 | GITHUB_SECRET= 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint"], 9 | "parserOptions": { 10 | "ecmaVersion": 2020, 11 | "project": ["tsconfig.json"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: CI 5 | 6 | env: 7 | DO_NOT_TRACK: '1' 8 | 9 | on: 10 | push: 11 | branches: ['main'] 12 | pull_request: 13 | branches: ['main'] 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Use Node.js 20.x 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 20.x 25 | cache: 'npm' 26 | - run: npm ci 27 | - run: npm run build 28 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Update ZenStack 5 | 6 | env: 7 | DO_NOT_TRACK: '1' 8 | 9 | on: 10 | workflow_dispatch: 11 | repository_dispatch: 12 | types: [zenstack-release] 13 | 14 | jobs: 15 | update: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Update to latest ZenStack 21 | run: | 22 | git config --global user.name ymc9 23 | git config --global user.email yiming@whimslab.io 24 | npm ci 25 | npm run up 26 | 27 | - name: Build 28 | run: | 29 | npm run build 30 | 31 | - name: Commit and push 32 | run: | 33 | git add . 34 | git commit -m "chore: update to latest ZenStack" || true 35 | git push || true 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | node_modules/ 4 | .env.local 5 | .next 6 | .zenstack_repl_history 7 | .idea 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug server-side", 9 | "type": "node-terminal", 10 | "request": "launch", 11 | "command": "npm run dev" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ZenStack Repositories 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

ZenStack SaaS Demo

4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | # A Collaborative Todo Sample - ZenStack + Next.js 13 | 14 | This project is a collaborative Todo app built with [Next.js](https://nextjs.org), [Next-Auth](nextauth.org), and [ZenStack](https://zenstack.dev). 15 | 16 | In this fictitious app, users can be invited to workspaces where they can collaborate on todos. Public todo lists are visible to all members in the workspace. 17 | 18 | See a live deployment at: https://zenstack-todo.vercel.app/. 19 | 20 | ## Features 21 | 22 | - User signup/signin 23 | - Creating workspaces and inviting members 24 | - Data segregation and permission control 25 | 26 | ## Implementation 27 | 28 | - Data model is located at `/schema.zmodel`. 29 | - An automatic CRUD API is mounted at `/api/model` by `pages/api/model/[...path].ts`. 30 | - [SWR](https://swr.vercel.app/) CRUD hooks are generated under `lib/hooks` folder. 31 | 32 | ## Running the sample 33 | 34 | 1. Setup a new PostgreSQL database 35 | 36 | You can launch a PostgreSQL instance locally, or create one from a hoster like [Supabase](https://supabase.com). Create a new database for this app, and set the connection string in .env file. 37 | 38 | 1. Install dependencies 39 | 40 | ```bash 41 | npm install 42 | ``` 43 | 44 | 1. Generate server and client-side code from model 45 | 46 | ```bash 47 | npm run generate 48 | ``` 49 | 50 | 1. Synchronize database schema 51 | 52 | ```bash 53 | npm run db:push 54 | ``` 55 | 56 | 1. Start dev server 57 | 58 | ```bash 59 | npm run dev 60 | ``` 61 | 62 | For more information on using ZenStack, visit [https://zenstack.dev](https://zenstack.dev). 63 | -------------------------------------------------------------------------------- /components/AuthGuard.tsx: -------------------------------------------------------------------------------- 1 | import { useSession } from 'next-auth/react'; 2 | import { useRouter } from 'next/router'; 3 | 4 | type Props = { 5 | children: JSX.Element | JSX.Element[]; 6 | }; 7 | 8 | export default function AuthGuard({ children }: Props) { 9 | const { status } = useSession(); 10 | const router = useRouter(); 11 | 12 | if (router.pathname === '/signup' || router.pathname === '/signin') { 13 | return <>{children}; 14 | } 15 | 16 | if (status === 'loading') { 17 | return

Loading...

; 18 | } else if (status === 'unauthenticated') { 19 | void router.push('/signin'); 20 | return <>; 21 | } else { 22 | return <>{children}; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { User } from 'next-auth'; 2 | import Image from 'next/image'; 3 | 4 | type Props = { 5 | user: User; 6 | size?: number; 7 | }; 8 | 9 | export default function Avatar({ user, size }: Props) { 10 | if (!user) { 11 | return <>; 12 | } 13 | return ( 14 |
15 | avatar 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/BreadCrumb.tsx: -------------------------------------------------------------------------------- 1 | import { List, Space } from '@prisma/client'; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/router'; 4 | 5 | type Props = { 6 | space: Space; 7 | list?: List; 8 | }; 9 | 10 | export default function BreadCrumb({ space, list }: Props) { 11 | const router = useRouter(); 12 | 13 | const parts = router.asPath.split('/').filter((p) => p); 14 | const [base] = parts; 15 | if (base !== 'space') { 16 | return <>; 17 | } 18 | 19 | const items: Array<{ text: string; link: string }> = []; 20 | 21 | items.push({ text: 'Home', link: '/' }); 22 | items.push({ text: space.name || '', link: `/space/${space.slug}` }); 23 | 24 | if (list) { 25 | items.push({ 26 | text: list?.title || '', 27 | link: `/space/${space.slug}/${list.id}`, 28 | }); 29 | } 30 | 31 | return ( 32 |
33 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /components/ManageMembers.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; 3 | import { useCurrentUser } from '@lib/context'; 4 | import { useCreateSpaceUser, useDeleteSpaceUser, useFindManySpaceUser } from '@lib/hooks'; 5 | import { Space, SpaceUserRole } from '@prisma/client'; 6 | import { ChangeEvent, KeyboardEvent, useState } from 'react'; 7 | import { toast } from 'react-toastify'; 8 | import Avatar from './Avatar'; 9 | 10 | type Props = { 11 | space: Space; 12 | }; 13 | 14 | export default function ManageMembers({ space }: Props) { 15 | const [email, setEmail] = useState(''); 16 | const [role, setRole] = useState(SpaceUserRole.USER); 17 | const user = useCurrentUser(); 18 | const { trigger: createSpaceUser } = useCreateSpaceUser(); 19 | const { trigger: deleteSpaceUser } = useDeleteSpaceUser(); 20 | 21 | const { data: members } = useFindManySpaceUser({ 22 | where: { 23 | spaceId: space.id, 24 | }, 25 | include: { 26 | user: true, 27 | }, 28 | orderBy: { 29 | role: 'desc', 30 | }, 31 | }); 32 | 33 | const inviteUser = async () => { 34 | try { 35 | const r = await createSpaceUser({ 36 | data: { 37 | user: { 38 | connect: { 39 | email, 40 | }, 41 | }, 42 | space: { 43 | connect: { 44 | id: space.id, 45 | }, 46 | }, 47 | role, 48 | }, 49 | }); 50 | console.log('SpaceUser created:', r); 51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 52 | } catch (err: any) { 53 | console.error(err); 54 | if (err.info?.prisma === true) { 55 | if (err.info.code === 'P2002') { 56 | toast.error('User is already a member of the space'); 57 | } else if (err.info.code === 'P2025') { 58 | toast.error('User is not found for this email'); 59 | } else { 60 | toast.error(`Unexpected Prisma error: ${err.info.code}`); 61 | } 62 | } else { 63 | toast.error(`Error occurred: ${JSON.stringify(err)}`); 64 | } 65 | } 66 | }; 67 | 68 | const removeMember = (id: string) => { 69 | if (confirm(`Are you sure to remove this member from space?`)) { 70 | void deleteSpaceUser({ where: { id } }); 71 | } 72 | }; 73 | 74 | return ( 75 |
76 |
77 | ) => { 83 | setEmail(e.currentTarget.value); 84 | }} 85 | onKeyUp={(e: KeyboardEvent) => { 86 | if (e.key === 'Enter') { 87 | void inviteUser(); 88 | } 89 | }} 90 | /> 91 | 92 | 102 | 103 | 106 |
107 | 108 | 131 |
132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { Space } from '@prisma/client'; 2 | import { User } from 'next-auth'; 3 | import { signOut } from 'next-auth/react'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | import Avatar from './Avatar'; 7 | 8 | type Props = { 9 | space: Space | undefined; 10 | user: User | undefined; 11 | }; 12 | 13 | export default function NavBar({ user, space }: Props) { 14 | const onSignout = () => { 15 | void signOut({ callbackUrl: '/signin' }); 16 | }; 17 | 18 | return ( 19 |
20 |
21 | 22 | Logo 23 |
24 | {space?.name || 'Welcome Todo App'} 25 |
26 |

Powered by ZenStack

27 | 28 |
29 |
30 |
31 | 34 |
    38 |
  • {user &&
    {user.name || user.email}
    }
  • 39 |
  • 40 | Logout 41 |
  • 42 |
43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /components/SpaceMembers.tsx: -------------------------------------------------------------------------------- 1 | import { PlusIcon } from '@heroicons/react/24/outline'; 2 | import { useCurrentSpace } from '@lib/context'; 3 | import { useFindManySpaceUser } from '@lib/hooks'; 4 | import { Space } from '@prisma/client'; 5 | import Avatar from './Avatar'; 6 | import ManageMembers from './ManageMembers'; 7 | 8 | function ManagementDialog(space?: Space) { 9 | if (!space) return undefined; 10 | return ( 11 | <> 12 | 15 | 16 | 17 |
18 |
19 |

Manage Members of {space.name}

20 | 21 |
22 | 23 |
24 | 25 |
26 | 29 |
30 |
31 |
32 | 33 | ); 34 | } 35 | 36 | export default function SpaceMembers() { 37 | const space = useCurrentSpace(); 38 | 39 | const { data: members } = useFindManySpaceUser( 40 | { 41 | where: { 42 | spaceId: space?.id, 43 | }, 44 | include: { 45 | user: true, 46 | }, 47 | orderBy: { 48 | role: 'desc', 49 | }, 50 | }, 51 | { disabled: !space } 52 | ); 53 | 54 | return ( 55 |
56 | {ManagementDialog(space)} 57 | {members && ( 58 | 63 | )} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/Spaces.tsx: -------------------------------------------------------------------------------- 1 | import { useCountList } from '@lib/hooks'; 2 | import { Space } from '@prisma/client'; 3 | import Link from 'next/link'; 4 | 5 | type Props = { 6 | spaces: Space[]; 7 | }; 8 | 9 | function SpaceItem({ space }: { space: Space }) { 10 | const { data: listCount } = useCountList({ 11 | where: { spaceId: space.id }, 12 | }); 13 | return ( 14 |
15 |
{listCount}
16 | 17 |
18 |

{space.name}

19 |
20 | 21 |
22 | ); 23 | } 24 | 25 | export default function Spaces({ spaces }: Props) { 26 | return ( 27 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/TimeInfo.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | type Props = { 4 | value: { createdAt: Date; updatedAt: Date; completedAt?: Date | null }; 5 | }; 6 | 7 | export default function TimeInfo({ value }: Props) { 8 | return ( 9 |

10 | {value.completedAt 11 | ? `Completed ${moment(value.completedAt).fromNow()}` 12 | : value.createdAt === value.updatedAt 13 | ? `Created ${moment(value.createdAt).fromNow()}` 14 | : `Updated ${moment(value.updatedAt).fromNow()}`} 15 |

16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/Todo.tsx: -------------------------------------------------------------------------------- 1 | import { TrashIcon } from '@heroicons/react/24/outline'; 2 | import { useDeleteTodo, useUpdateTodo } from '@lib/hooks'; 3 | import { Todo, User } from '@prisma/client'; 4 | import { ChangeEvent } from 'react'; 5 | import Avatar from './Avatar'; 6 | import TimeInfo from './TimeInfo'; 7 | 8 | type Props = { 9 | value: Todo & { owner: User }; 10 | optimistic?: boolean; 11 | }; 12 | 13 | export default function TodoComponent({ value, optimistic }: Props) { 14 | const { trigger: updateTodo } = useUpdateTodo({ optimisticUpdate: true }); 15 | const { trigger: deleteTodo } = useDeleteTodo({ optimisticUpdate: true }); 16 | 17 | const onDeleteTodo = () => { 18 | void deleteTodo({ where: { id: value.id } }); 19 | }; 20 | 21 | const toggleCompleted = (completed: boolean) => { 22 | if (completed === !!value.completedAt) { 23 | return; 24 | } 25 | void updateTodo({ 26 | where: { id: value.id }, 27 | data: { completedAt: completed ? new Date() : null }, 28 | }); 29 | }; 30 | 31 | return ( 32 |
33 |
34 |

39 | {value.title} 40 | {optimistic && } 41 |

42 |
43 | ) => toggleCompleted(e.currentTarget.checked)} 49 | /> 50 | { 55 | !optimistic && onDeleteTodo(); 56 | }} 57 | /> 58 |
59 |
60 |
61 | 62 | 63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import { LockClosedIcon, TrashIcon } from '@heroicons/react/24/outline'; 2 | import { useCheckList, useDeleteList } from '@lib/hooks'; 3 | import { List } from '@prisma/client'; 4 | import { customAlphabet } from 'nanoid'; 5 | import { User } from 'next-auth'; 6 | import Image from 'next/image'; 7 | import Link from 'next/link'; 8 | import { useRouter } from 'next/router'; 9 | import Avatar from './Avatar'; 10 | import TimeInfo from './TimeInfo'; 11 | 12 | type Props = { 13 | value: List & { owner: User }; 14 | deleted?: (value: List) => void; 15 | }; 16 | 17 | export default function TodoList({ value }: Props) { 18 | const router = useRouter(); 19 | 20 | // check if the current user can delete the list (based on its owner) 21 | const { data: canDelete } = useCheckList({ operation: 'delete', where: { ownerId: value.ownerId } }); 22 | 23 | const { trigger: deleteList } = useDeleteList(); 24 | 25 | const onDeleteList = () => { 26 | if (confirm('Are you sure to delete this list?')) { 27 | void deleteList({ where: { id: value.id } }); 28 | } 29 | }; 30 | 31 | return ( 32 |
33 | 34 |
35 | Cover 42 |
43 | 44 |
45 | 46 |

{value.title || 'Missing Title'}

47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 | {value.private && ( 55 |
56 | 57 |
58 | )} 59 | 60 | {canDelete && ( 61 | { 64 | onDeleteList(); 65 | }} 66 | /> 67 | )} 68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /components/WithNavBar.tsx: -------------------------------------------------------------------------------- 1 | import { useCurrentSpace, useCurrentUser } from '@lib/context'; 2 | import NavBar from './NavBar'; 3 | 4 | type Props = { 5 | children: JSX.Element | JSX.Element[] | undefined; 6 | }; 7 | 8 | export default function WithNavBar({ children }: Props) { 9 | const user = useCurrentUser(); 10 | const space = useCurrentSpace(); 11 | 12 | return ( 13 | <> 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/context.ts: -------------------------------------------------------------------------------- 1 | import { Space } from '@prisma/client'; 2 | import { User } from 'next-auth'; 3 | import { useSession } from 'next-auth/react'; 4 | import { useRouter } from 'next/router'; 5 | import { createContext } from 'react'; 6 | import { useFindManySpace } from './hooks'; 7 | 8 | export const UserContext = createContext(undefined); 9 | 10 | export function useCurrentUser() { 11 | const { data: session } = useSession(); 12 | return session?.user; 13 | } 14 | 15 | export const SpaceContext = createContext(undefined); 16 | 17 | export function useCurrentSpace() { 18 | const router = useRouter(); 19 | const { data: spaces } = useFindManySpace( 20 | { 21 | where: { 22 | slug: router.query.slug as string, 23 | }, 24 | }, 25 | { 26 | disabled: !router.query.slug, 27 | } 28 | ); 29 | 30 | return spaces?.[0]; 31 | } 32 | -------------------------------------------------------------------------------- /lib/hooks/__model_meta.ts: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * This file was generated by ZenStack CLI. 3 | ******************************************************************************/ 4 | 5 | /* eslint-disable */ 6 | // @ts-nocheck 7 | 8 | const metadata = { 9 | models: { 10 | space: { 11 | name: 'Space', fields: { 12 | id: { 13 | name: "id", 14 | type: "String", 15 | isId: true, 16 | attributes: [{ "name": "@default", "args": [] }], 17 | }, createdAt: { 18 | name: "createdAt", 19 | type: "DateTime", 20 | attributes: [{ "name": "@default", "args": [] }], 21 | }, updatedAt: { 22 | name: "updatedAt", 23 | type: "DateTime", 24 | attributes: [{ "name": "@updatedAt", "args": [] }], 25 | }, owner: { 26 | name: "owner", 27 | type: "User", 28 | isDataModel: true, 29 | backLink: 'ownedSpaces', 30 | isRelationOwner: true, 31 | onDeleteAction: 'Cascade', 32 | foreignKeyMapping: { "id": "ownerId" }, 33 | }, ownerId: { 34 | name: "ownerId", 35 | type: "String", 36 | attributes: [{ "name": "@default", "args": [] }], 37 | defaultValueProvider: $default$Space$ownerId, 38 | isForeignKey: true, 39 | relationField: 'owner', 40 | }, name: { 41 | name: "name", 42 | type: "String", 43 | }, slug: { 44 | name: "slug", 45 | type: "String", 46 | }, members: { 47 | name: "members", 48 | type: "SpaceUser", 49 | isDataModel: true, 50 | isArray: true, 51 | backLink: 'space', 52 | }, lists: { 53 | name: "lists", 54 | type: "List", 55 | isDataModel: true, 56 | isArray: true, 57 | backLink: 'space', 58 | }, 59 | }, uniqueConstraints: { 60 | id: { 61 | name: "id", 62 | fields: ["id"] 63 | }, slug: { 64 | name: "slug", 65 | fields: ["slug"] 66 | }, 67 | }, 68 | }, 69 | spaceUser: { 70 | name: 'SpaceUser', fields: { 71 | id: { 72 | name: "id", 73 | type: "String", 74 | isId: true, 75 | attributes: [{ "name": "@default", "args": [] }], 76 | }, createdAt: { 77 | name: "createdAt", 78 | type: "DateTime", 79 | attributes: [{ "name": "@default", "args": [] }], 80 | }, updatedAt: { 81 | name: "updatedAt", 82 | type: "DateTime", 83 | attributes: [{ "name": "@updatedAt", "args": [] }], 84 | }, space: { 85 | name: "space", 86 | type: "Space", 87 | isDataModel: true, 88 | backLink: 'members', 89 | isRelationOwner: true, 90 | onDeleteAction: 'Cascade', 91 | foreignKeyMapping: { "id": "spaceId" }, 92 | }, spaceId: { 93 | name: "spaceId", 94 | type: "String", 95 | isForeignKey: true, 96 | relationField: 'space', 97 | }, user: { 98 | name: "user", 99 | type: "User", 100 | isDataModel: true, 101 | backLink: 'memberships', 102 | isRelationOwner: true, 103 | onDeleteAction: 'Cascade', 104 | foreignKeyMapping: { "id": "userId" }, 105 | }, userId: { 106 | name: "userId", 107 | type: "String", 108 | isForeignKey: true, 109 | relationField: 'user', 110 | }, role: { 111 | name: "role", 112 | type: "SpaceUserRole", 113 | }, 114 | }, uniqueConstraints: { 115 | id: { 116 | name: "id", 117 | fields: ["id"] 118 | }, userId_spaceId: { 119 | name: "userId_spaceId", 120 | fields: ["userId", "spaceId"] 121 | }, 122 | }, 123 | }, 124 | user: { 125 | name: 'User', fields: { 126 | id: { 127 | name: "id", 128 | type: "String", 129 | isId: true, 130 | attributes: [{ "name": "@default", "args": [] }], 131 | }, createdAt: { 132 | name: "createdAt", 133 | type: "DateTime", 134 | attributes: [{ "name": "@default", "args": [] }], 135 | }, updatedAt: { 136 | name: "updatedAt", 137 | type: "DateTime", 138 | attributes: [{ "name": "@updatedAt", "args": [] }], 139 | }, email: { 140 | name: "email", 141 | type: "String", 142 | }, emailVerified: { 143 | name: "emailVerified", 144 | type: "DateTime", 145 | isOptional: true, 146 | }, password: { 147 | name: "password", 148 | type: "String", 149 | isOptional: true, 150 | }, name: { 151 | name: "name", 152 | type: "String", 153 | isOptional: true, 154 | }, ownedSpaces: { 155 | name: "ownedSpaces", 156 | type: "Space", 157 | isDataModel: true, 158 | isArray: true, 159 | backLink: 'owner', 160 | }, memberships: { 161 | name: "memberships", 162 | type: "SpaceUser", 163 | isDataModel: true, 164 | isArray: true, 165 | backLink: 'user', 166 | }, image: { 167 | name: "image", 168 | type: "String", 169 | isOptional: true, 170 | }, lists: { 171 | name: "lists", 172 | type: "List", 173 | isDataModel: true, 174 | isArray: true, 175 | backLink: 'owner', 176 | }, todos: { 177 | name: "todos", 178 | type: "Todo", 179 | isDataModel: true, 180 | isArray: true, 181 | backLink: 'owner', 182 | }, accounts: { 183 | name: "accounts", 184 | type: "Account", 185 | isDataModel: true, 186 | isArray: true, 187 | backLink: 'user', 188 | }, 189 | }, uniqueConstraints: { 190 | id: { 191 | name: "id", 192 | fields: ["id"] 193 | }, email: { 194 | name: "email", 195 | fields: ["email"] 196 | }, 197 | }, 198 | }, 199 | list: { 200 | name: 'List', fields: { 201 | id: { 202 | name: "id", 203 | type: "String", 204 | isId: true, 205 | attributes: [{ "name": "@default", "args": [] }], 206 | }, createdAt: { 207 | name: "createdAt", 208 | type: "DateTime", 209 | attributes: [{ "name": "@default", "args": [] }], 210 | }, updatedAt: { 211 | name: "updatedAt", 212 | type: "DateTime", 213 | attributes: [{ "name": "@updatedAt", "args": [] }], 214 | }, space: { 215 | name: "space", 216 | type: "Space", 217 | isDataModel: true, 218 | backLink: 'lists', 219 | isRelationOwner: true, 220 | onDeleteAction: 'Cascade', 221 | foreignKeyMapping: { "id": "spaceId" }, 222 | }, spaceId: { 223 | name: "spaceId", 224 | type: "String", 225 | isForeignKey: true, 226 | relationField: 'space', 227 | }, owner: { 228 | name: "owner", 229 | type: "User", 230 | isDataModel: true, 231 | backLink: 'lists', 232 | isRelationOwner: true, 233 | onDeleteAction: 'Cascade', 234 | foreignKeyMapping: { "id": "ownerId" }, 235 | }, ownerId: { 236 | name: "ownerId", 237 | type: "String", 238 | attributes: [{ "name": "@default", "args": [] }], 239 | defaultValueProvider: $default$List$ownerId, 240 | isForeignKey: true, 241 | relationField: 'owner', 242 | }, title: { 243 | name: "title", 244 | type: "String", 245 | }, private: { 246 | name: "private", 247 | type: "Boolean", 248 | attributes: [{ "name": "@default", "args": [{ "value": false }] }], 249 | }, todos: { 250 | name: "todos", 251 | type: "Todo", 252 | isDataModel: true, 253 | isArray: true, 254 | backLink: 'list', 255 | }, 256 | }, uniqueConstraints: { 257 | id: { 258 | name: "id", 259 | fields: ["id"] 260 | }, 261 | }, 262 | }, 263 | todo: { 264 | name: 'Todo', fields: { 265 | id: { 266 | name: "id", 267 | type: "String", 268 | isId: true, 269 | attributes: [{ "name": "@default", "args": [] }], 270 | }, createdAt: { 271 | name: "createdAt", 272 | type: "DateTime", 273 | attributes: [{ "name": "@default", "args": [] }], 274 | }, updatedAt: { 275 | name: "updatedAt", 276 | type: "DateTime", 277 | attributes: [{ "name": "@updatedAt", "args": [] }], 278 | }, owner: { 279 | name: "owner", 280 | type: "User", 281 | isDataModel: true, 282 | backLink: 'todos', 283 | isRelationOwner: true, 284 | onDeleteAction: 'Cascade', 285 | foreignKeyMapping: { "id": "ownerId" }, 286 | }, ownerId: { 287 | name: "ownerId", 288 | type: "String", 289 | attributes: [{ "name": "@default", "args": [] }], 290 | defaultValueProvider: $default$Todo$ownerId, 291 | isForeignKey: true, 292 | relationField: 'owner', 293 | }, list: { 294 | name: "list", 295 | type: "List", 296 | isDataModel: true, 297 | backLink: 'todos', 298 | isRelationOwner: true, 299 | onDeleteAction: 'Cascade', 300 | foreignKeyMapping: { "id": "listId" }, 301 | }, listId: { 302 | name: "listId", 303 | type: "String", 304 | isForeignKey: true, 305 | relationField: 'list', 306 | }, title: { 307 | name: "title", 308 | type: "String", 309 | }, completedAt: { 310 | name: "completedAt", 311 | type: "DateTime", 312 | isOptional: true, 313 | }, 314 | }, uniqueConstraints: { 315 | id: { 316 | name: "id", 317 | fields: ["id"] 318 | }, 319 | }, 320 | }, 321 | account: { 322 | name: 'Account', fields: { 323 | id: { 324 | name: "id", 325 | type: "String", 326 | isId: true, 327 | attributes: [{ "name": "@default", "args": [] }], 328 | }, userId: { 329 | name: "userId", 330 | type: "String", 331 | isForeignKey: true, 332 | relationField: 'user', 333 | }, type: { 334 | name: "type", 335 | type: "String", 336 | }, provider: { 337 | name: "provider", 338 | type: "String", 339 | }, providerAccountId: { 340 | name: "providerAccountId", 341 | type: "String", 342 | }, refresh_token: { 343 | name: "refresh_token", 344 | type: "String", 345 | isOptional: true, 346 | }, refresh_token_expires_in: { 347 | name: "refresh_token_expires_in", 348 | type: "Int", 349 | isOptional: true, 350 | }, access_token: { 351 | name: "access_token", 352 | type: "String", 353 | isOptional: true, 354 | }, expires_at: { 355 | name: "expires_at", 356 | type: "Int", 357 | isOptional: true, 358 | }, token_type: { 359 | name: "token_type", 360 | type: "String", 361 | isOptional: true, 362 | }, scope: { 363 | name: "scope", 364 | type: "String", 365 | isOptional: true, 366 | }, id_token: { 367 | name: "id_token", 368 | type: "String", 369 | isOptional: true, 370 | }, session_state: { 371 | name: "session_state", 372 | type: "String", 373 | isOptional: true, 374 | }, user: { 375 | name: "user", 376 | type: "User", 377 | isDataModel: true, 378 | backLink: 'accounts', 379 | isRelationOwner: true, 380 | onDeleteAction: 'Cascade', 381 | foreignKeyMapping: { "id": "userId" }, 382 | }, 383 | }, uniqueConstraints: { 384 | id: { 385 | name: "id", 386 | fields: ["id"] 387 | }, provider_providerAccountId: { 388 | name: "provider_providerAccountId", 389 | fields: ["provider", "providerAccountId"] 390 | }, 391 | }, 392 | }, 393 | 394 | }, 395 | deleteCascade: { 396 | space: ['SpaceUser', 'List'], 397 | user: ['Space', 'SpaceUser', 'List', 'Todo', 'Account'], 398 | list: ['Todo'], 399 | 400 | }, 401 | authModel: 'User' 402 | 403 | }; 404 | 405 | function $default$Space$ownerId(user: any): unknown { 406 | return user?.id; 407 | } 408 | 409 | function $default$List$ownerId(user: any): unknown { 410 | return user?.id; 411 | } 412 | 413 | function $default$Todo$ownerId(user: any): unknown { 414 | return user?.id; 415 | } 416 | export default metadata; 417 | -------------------------------------------------------------------------------- /lib/hooks/account.ts: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * This file was generated by ZenStack CLI. 3 | ******************************************************************************/ 4 | 5 | /* eslint-disable */ 6 | // @ts-nocheck 7 | 8 | import type { Prisma } from "@zenstackhq/runtime/models"; 9 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime'; 10 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 11 | import metadata from './__model_meta'; 12 | import * as request from '@zenstackhq/swr/runtime'; 13 | 14 | export function useCreateAccount(options?: MutationOptions | undefined, unknown, Prisma.AccountCreateArgs>) { 15 | const mutation = request.useModelMutation('Account', 'POST', 'create', metadata, options, true); 16 | return { 17 | ...mutation, 18 | trigger: (args: Prisma.SelectSubset) => { 19 | return mutation.trigger(args, options as any) as Promise | undefined>; 20 | } 21 | }; 22 | } 23 | 24 | export function useCreateManyAccount(options?: MutationOptions) { 25 | const mutation = request.useModelMutation('Account', 'POST', 'createMany', metadata, options, false); 26 | return { 27 | ...mutation, 28 | trigger: (args: Prisma.SelectSubset) => { 29 | return mutation.trigger(args, options as any) as Promise; 30 | } 31 | }; 32 | } 33 | 34 | export function useFindManyAccount(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) { 35 | return request.useModelQuery('Account', 'findMany', args, options); 36 | } 37 | 38 | export function useInfiniteFindManyAccount>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) { 39 | return request.useInfiniteModelQuery('Account', 'findMany', getNextArgs, options); 40 | } 41 | 42 | export function useFindUniqueAccount(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 43 | return request.useModelQuery('Account', 'findUnique', args, options); 44 | } 45 | 46 | export function useFindFirstAccount(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 47 | return request.useModelQuery('Account', 'findFirst', args, options); 48 | } 49 | 50 | export function useUpdateAccount(options?: MutationOptions | undefined, unknown, Prisma.AccountUpdateArgs>) { 51 | const mutation = request.useModelMutation('Account', 'PUT', 'update', metadata, options, true); 52 | return { 53 | ...mutation, 54 | trigger: (args: Prisma.SelectSubset) => { 55 | return mutation.trigger(args, options as any) as Promise | undefined>; 56 | } 57 | }; 58 | } 59 | 60 | export function useUpdateManyAccount(options?: MutationOptions) { 61 | const mutation = request.useModelMutation('Account', 'PUT', 'updateMany', metadata, options, false); 62 | return { 63 | ...mutation, 64 | trigger: (args: Prisma.SelectSubset) => { 65 | return mutation.trigger(args, options as any) as Promise; 66 | } 67 | }; 68 | } 69 | 70 | export function useUpsertAccount(options?: MutationOptions | undefined, unknown, Prisma.AccountUpsertArgs>) { 71 | const mutation = request.useModelMutation('Account', 'POST', 'upsert', metadata, options, true); 72 | return { 73 | ...mutation, 74 | trigger: (args: Prisma.SelectSubset) => { 75 | return mutation.trigger(args, options as any) as Promise | undefined>; 76 | } 77 | }; 78 | } 79 | 80 | export function useDeleteAccount(options?: MutationOptions | undefined, unknown, Prisma.AccountDeleteArgs>) { 81 | const mutation = request.useModelMutation('Account', 'DELETE', 'delete', metadata, options, true); 82 | return { 83 | ...mutation, 84 | trigger: (args: Prisma.SelectSubset) => { 85 | return mutation.trigger(args, options as any) as Promise | undefined>; 86 | } 87 | }; 88 | } 89 | 90 | export function useDeleteManyAccount(options?: MutationOptions) { 91 | const mutation = request.useModelMutation('Account', 'DELETE', 'deleteMany', metadata, options, false); 92 | return { 93 | ...mutation, 94 | trigger: (args: Prisma.SelectSubset) => { 95 | return mutation.trigger(args, options as any) as Promise; 96 | } 97 | }; 98 | } 99 | 100 | export function useAggregateAccount(args?: Prisma.Subset, options?: QueryOptions>) { 101 | return request.useModelQuery('Account', 'aggregate', args, options); 102 | } 103 | 104 | export function useGroupByAccount>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.AccountGroupByArgs['orderBy'] } : { orderBy?: Prisma.AccountGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 105 | ? `Error: "by" must not be empty.` 106 | : HavingValid extends Prisma.False 107 | ? { 108 | [P in HavingFields]: P extends ByFields 109 | ? never 110 | : P extends string 111 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 112 | : [ 113 | Error, 114 | 'Field ', 115 | P, 116 | ` in "having" needs to be provided in "by"`, 117 | ] 118 | }[HavingFields] 119 | : 'take' extends Prisma.Keys 120 | ? 'orderBy' extends Prisma.Keys 121 | ? ByValid extends Prisma.True 122 | ? {} 123 | : { 124 | [P in OrderFields]: P extends ByFields 125 | ? never 126 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 127 | }[OrderFields] 128 | : 'Error: If you provide "take", you also need to provide "orderBy"' 129 | : 'skip' extends Prisma.Keys 130 | ? 'orderBy' extends Prisma.Keys 131 | ? ByValid extends Prisma.True 132 | ? {} 133 | : { 134 | [P in OrderFields]: P extends ByFields 135 | ? never 136 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 137 | }[OrderFields] 138 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 139 | : ByValid extends Prisma.True 140 | ? {} 141 | : { 142 | [P in OrderFields]: P extends ByFields 143 | ? never 144 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 145 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ? 146 | Array & 147 | { 148 | [P in ((keyof T) & (keyof Prisma.AccountGroupByOutputType))]: P extends '_count' 149 | ? T[P] extends boolean 150 | ? number 151 | : Prisma.GetScalarType 152 | : Prisma.GetScalarType 153 | } 154 | > : InputErrors>) { 155 | return request.useModelQuery('Account', 'groupBy', args, options); 156 | } 157 | 158 | export function useCountAccount(args?: Prisma.Subset, options?: QueryOptions : number>) { 159 | return request.useModelQuery('Account', 'count', args, options); 160 | } 161 | 162 | export function useCheckAccount(args: { operation: PolicyCrudKind; where?: { id?: string; userId?: string; type?: string; provider?: string; providerAccountId?: string; refresh_token?: string; refresh_token_expires_in?: number; access_token?: string; expires_at?: number; token_type?: string; scope?: string; id_token?: string; session_state?: string }; }, options?: QueryOptions) { 163 | return request.useModelQuery('Account', 'check', args, options); 164 | } 165 | -------------------------------------------------------------------------------- /lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * This file was generated by ZenStack CLI. 3 | ******************************************************************************/ 4 | 5 | /* eslint-disable */ 6 | // @ts-nocheck 7 | 8 | export * from './space'; 9 | export * from './space-user'; 10 | export * from './user'; 11 | export * from './list'; 12 | export * from './todo'; 13 | export * from './account'; 14 | export { Provider } from '@zenstackhq/swr/runtime'; 15 | export { default as metadata } from './__model_meta'; 16 | -------------------------------------------------------------------------------- /lib/hooks/list.ts: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * This file was generated by ZenStack CLI. 3 | ******************************************************************************/ 4 | 5 | /* eslint-disable */ 6 | // @ts-nocheck 7 | 8 | import type { Prisma } from "@zenstackhq/runtime/models"; 9 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime'; 10 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 11 | import metadata from './__model_meta'; 12 | import * as request from '@zenstackhq/swr/runtime'; 13 | 14 | export function useCreateList(options?: MutationOptions | undefined, unknown, Prisma.ListCreateArgs>) { 15 | const mutation = request.useModelMutation('List', 'POST', 'create', metadata, options, true); 16 | return { 17 | ...mutation, 18 | trigger: (args: Prisma.SelectSubset) => { 19 | return mutation.trigger(args, options as any) as Promise | undefined>; 20 | } 21 | }; 22 | } 23 | 24 | export function useCreateManyList(options?: MutationOptions) { 25 | const mutation = request.useModelMutation('List', 'POST', 'createMany', metadata, options, false); 26 | return { 27 | ...mutation, 28 | trigger: (args: Prisma.SelectSubset) => { 29 | return mutation.trigger(args, options as any) as Promise; 30 | } 31 | }; 32 | } 33 | 34 | export function useFindManyList(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) { 35 | return request.useModelQuery('List', 'findMany', args, options); 36 | } 37 | 38 | export function useInfiniteFindManyList>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) { 39 | return request.useInfiniteModelQuery('List', 'findMany', getNextArgs, options); 40 | } 41 | 42 | export function useFindUniqueList(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 43 | return request.useModelQuery('List', 'findUnique', args, options); 44 | } 45 | 46 | export function useFindFirstList(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 47 | return request.useModelQuery('List', 'findFirst', args, options); 48 | } 49 | 50 | export function useUpdateList(options?: MutationOptions | undefined, unknown, Prisma.ListUpdateArgs>) { 51 | const mutation = request.useModelMutation('List', 'PUT', 'update', metadata, options, true); 52 | return { 53 | ...mutation, 54 | trigger: (args: Prisma.SelectSubset) => { 55 | return mutation.trigger(args, options as any) as Promise | undefined>; 56 | } 57 | }; 58 | } 59 | 60 | export function useUpdateManyList(options?: MutationOptions) { 61 | const mutation = request.useModelMutation('List', 'PUT', 'updateMany', metadata, options, false); 62 | return { 63 | ...mutation, 64 | trigger: (args: Prisma.SelectSubset) => { 65 | return mutation.trigger(args, options as any) as Promise; 66 | } 67 | }; 68 | } 69 | 70 | export function useUpsertList(options?: MutationOptions | undefined, unknown, Prisma.ListUpsertArgs>) { 71 | const mutation = request.useModelMutation('List', 'POST', 'upsert', metadata, options, true); 72 | return { 73 | ...mutation, 74 | trigger: (args: Prisma.SelectSubset) => { 75 | return mutation.trigger(args, options as any) as Promise | undefined>; 76 | } 77 | }; 78 | } 79 | 80 | export function useDeleteList(options?: MutationOptions | undefined, unknown, Prisma.ListDeleteArgs>) { 81 | const mutation = request.useModelMutation('List', 'DELETE', 'delete', metadata, options, true); 82 | return { 83 | ...mutation, 84 | trigger: (args: Prisma.SelectSubset) => { 85 | return mutation.trigger(args, options as any) as Promise | undefined>; 86 | } 87 | }; 88 | } 89 | 90 | export function useDeleteManyList(options?: MutationOptions) { 91 | const mutation = request.useModelMutation('List', 'DELETE', 'deleteMany', metadata, options, false); 92 | return { 93 | ...mutation, 94 | trigger: (args: Prisma.SelectSubset) => { 95 | return mutation.trigger(args, options as any) as Promise; 96 | } 97 | }; 98 | } 99 | 100 | export function useAggregateList(args?: Prisma.Subset, options?: QueryOptions>) { 101 | return request.useModelQuery('List', 'aggregate', args, options); 102 | } 103 | 104 | export function useGroupByList>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.ListGroupByArgs['orderBy'] } : { orderBy?: Prisma.ListGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 105 | ? `Error: "by" must not be empty.` 106 | : HavingValid extends Prisma.False 107 | ? { 108 | [P in HavingFields]: P extends ByFields 109 | ? never 110 | : P extends string 111 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 112 | : [ 113 | Error, 114 | 'Field ', 115 | P, 116 | ` in "having" needs to be provided in "by"`, 117 | ] 118 | }[HavingFields] 119 | : 'take' extends Prisma.Keys 120 | ? 'orderBy' extends Prisma.Keys 121 | ? ByValid extends Prisma.True 122 | ? {} 123 | : { 124 | [P in OrderFields]: P extends ByFields 125 | ? never 126 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 127 | }[OrderFields] 128 | : 'Error: If you provide "take", you also need to provide "orderBy"' 129 | : 'skip' extends Prisma.Keys 130 | ? 'orderBy' extends Prisma.Keys 131 | ? ByValid extends Prisma.True 132 | ? {} 133 | : { 134 | [P in OrderFields]: P extends ByFields 135 | ? never 136 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 137 | }[OrderFields] 138 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 139 | : ByValid extends Prisma.True 140 | ? {} 141 | : { 142 | [P in OrderFields]: P extends ByFields 143 | ? never 144 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 145 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ? 146 | Array & 147 | { 148 | [P in ((keyof T) & (keyof Prisma.ListGroupByOutputType))]: P extends '_count' 149 | ? T[P] extends boolean 150 | ? number 151 | : Prisma.GetScalarType 152 | : Prisma.GetScalarType 153 | } 154 | > : InputErrors>) { 155 | return request.useModelQuery('List', 'groupBy', args, options); 156 | } 157 | 158 | export function useCountList(args?: Prisma.Subset, options?: QueryOptions : number>) { 159 | return request.useModelQuery('List', 'count', args, options); 160 | } 161 | 162 | export function useCheckList(args: { operation: PolicyCrudKind; where?: { id?: string; spaceId?: string; ownerId?: string; title?: string; private?: boolean }; }, options?: QueryOptions) { 163 | return request.useModelQuery('List', 'check', args, options); 164 | } 165 | -------------------------------------------------------------------------------- /lib/hooks/space-user.ts: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * This file was generated by ZenStack CLI. 3 | ******************************************************************************/ 4 | 5 | /* eslint-disable */ 6 | // @ts-nocheck 7 | 8 | import type { Prisma } from "@zenstackhq/runtime/models"; 9 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime'; 10 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 11 | import metadata from './__model_meta'; 12 | import * as request from '@zenstackhq/swr/runtime'; 13 | 14 | export function useCreateSpaceUser(options?: MutationOptions | undefined, unknown, Prisma.SpaceUserCreateArgs>) { 15 | const mutation = request.useModelMutation('SpaceUser', 'POST', 'create', metadata, options, true); 16 | return { 17 | ...mutation, 18 | trigger: (args: Prisma.SelectSubset) => { 19 | return mutation.trigger(args, options as any) as Promise | undefined>; 20 | } 21 | }; 22 | } 23 | 24 | export function useCreateManySpaceUser(options?: MutationOptions) { 25 | const mutation = request.useModelMutation('SpaceUser', 'POST', 'createMany', metadata, options, false); 26 | return { 27 | ...mutation, 28 | trigger: (args: Prisma.SelectSubset) => { 29 | return mutation.trigger(args, options as any) as Promise; 30 | } 31 | }; 32 | } 33 | 34 | export function useFindManySpaceUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) { 35 | return request.useModelQuery('SpaceUser', 'findMany', args, options); 36 | } 37 | 38 | export function useInfiniteFindManySpaceUser>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) { 39 | return request.useInfiniteModelQuery('SpaceUser', 'findMany', getNextArgs, options); 40 | } 41 | 42 | export function useFindUniqueSpaceUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 43 | return request.useModelQuery('SpaceUser', 'findUnique', args, options); 44 | } 45 | 46 | export function useFindFirstSpaceUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 47 | return request.useModelQuery('SpaceUser', 'findFirst', args, options); 48 | } 49 | 50 | export function useUpdateSpaceUser(options?: MutationOptions | undefined, unknown, Prisma.SpaceUserUpdateArgs>) { 51 | const mutation = request.useModelMutation('SpaceUser', 'PUT', 'update', metadata, options, true); 52 | return { 53 | ...mutation, 54 | trigger: (args: Prisma.SelectSubset) => { 55 | return mutation.trigger(args, options as any) as Promise | undefined>; 56 | } 57 | }; 58 | } 59 | 60 | export function useUpdateManySpaceUser(options?: MutationOptions) { 61 | const mutation = request.useModelMutation('SpaceUser', 'PUT', 'updateMany', metadata, options, false); 62 | return { 63 | ...mutation, 64 | trigger: (args: Prisma.SelectSubset) => { 65 | return mutation.trigger(args, options as any) as Promise; 66 | } 67 | }; 68 | } 69 | 70 | export function useUpsertSpaceUser(options?: MutationOptions | undefined, unknown, Prisma.SpaceUserUpsertArgs>) { 71 | const mutation = request.useModelMutation('SpaceUser', 'POST', 'upsert', metadata, options, true); 72 | return { 73 | ...mutation, 74 | trigger: (args: Prisma.SelectSubset) => { 75 | return mutation.trigger(args, options as any) as Promise | undefined>; 76 | } 77 | }; 78 | } 79 | 80 | export function useDeleteSpaceUser(options?: MutationOptions | undefined, unknown, Prisma.SpaceUserDeleteArgs>) { 81 | const mutation = request.useModelMutation('SpaceUser', 'DELETE', 'delete', metadata, options, true); 82 | return { 83 | ...mutation, 84 | trigger: (args: Prisma.SelectSubset) => { 85 | return mutation.trigger(args, options as any) as Promise | undefined>; 86 | } 87 | }; 88 | } 89 | 90 | export function useDeleteManySpaceUser(options?: MutationOptions) { 91 | const mutation = request.useModelMutation('SpaceUser', 'DELETE', 'deleteMany', metadata, options, false); 92 | return { 93 | ...mutation, 94 | trigger: (args: Prisma.SelectSubset) => { 95 | return mutation.trigger(args, options as any) as Promise; 96 | } 97 | }; 98 | } 99 | 100 | export function useAggregateSpaceUser(args?: Prisma.Subset, options?: QueryOptions>) { 101 | return request.useModelQuery('SpaceUser', 'aggregate', args, options); 102 | } 103 | 104 | export function useGroupBySpaceUser>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.SpaceUserGroupByArgs['orderBy'] } : { orderBy?: Prisma.SpaceUserGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 105 | ? `Error: "by" must not be empty.` 106 | : HavingValid extends Prisma.False 107 | ? { 108 | [P in HavingFields]: P extends ByFields 109 | ? never 110 | : P extends string 111 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 112 | : [ 113 | Error, 114 | 'Field ', 115 | P, 116 | ` in "having" needs to be provided in "by"`, 117 | ] 118 | }[HavingFields] 119 | : 'take' extends Prisma.Keys 120 | ? 'orderBy' extends Prisma.Keys 121 | ? ByValid extends Prisma.True 122 | ? {} 123 | : { 124 | [P in OrderFields]: P extends ByFields 125 | ? never 126 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 127 | }[OrderFields] 128 | : 'Error: If you provide "take", you also need to provide "orderBy"' 129 | : 'skip' extends Prisma.Keys 130 | ? 'orderBy' extends Prisma.Keys 131 | ? ByValid extends Prisma.True 132 | ? {} 133 | : { 134 | [P in OrderFields]: P extends ByFields 135 | ? never 136 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 137 | }[OrderFields] 138 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 139 | : ByValid extends Prisma.True 140 | ? {} 141 | : { 142 | [P in OrderFields]: P extends ByFields 143 | ? never 144 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 145 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ? 146 | Array & 147 | { 148 | [P in ((keyof T) & (keyof Prisma.SpaceUserGroupByOutputType))]: P extends '_count' 149 | ? T[P] extends boolean 150 | ? number 151 | : Prisma.GetScalarType 152 | : Prisma.GetScalarType 153 | } 154 | > : InputErrors>) { 155 | return request.useModelQuery('SpaceUser', 'groupBy', args, options); 156 | } 157 | 158 | export function useCountSpaceUser(args?: Prisma.Subset, options?: QueryOptions : number>) { 159 | return request.useModelQuery('SpaceUser', 'count', args, options); 160 | } 161 | import type { SpaceUserRole } from '@zenstackhq/runtime/models'; 162 | 163 | export function useCheckSpaceUser(args: { operation: PolicyCrudKind; where?: { id?: string; spaceId?: string; userId?: string; role?: SpaceUserRole }; }, options?: QueryOptions) { 164 | return request.useModelQuery('SpaceUser', 'check', args, options); 165 | } 166 | -------------------------------------------------------------------------------- /lib/hooks/space.ts: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * This file was generated by ZenStack CLI. 3 | ******************************************************************************/ 4 | 5 | /* eslint-disable */ 6 | // @ts-nocheck 7 | 8 | import type { Prisma } from "@zenstackhq/runtime/models"; 9 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime'; 10 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 11 | import metadata from './__model_meta'; 12 | import * as request from '@zenstackhq/swr/runtime'; 13 | 14 | export function useCreateSpace(options?: MutationOptions | undefined, unknown, Prisma.SpaceCreateArgs>) { 15 | const mutation = request.useModelMutation('Space', 'POST', 'create', metadata, options, true); 16 | return { 17 | ...mutation, 18 | trigger: (args: Prisma.SelectSubset) => { 19 | return mutation.trigger(args, options as any) as Promise | undefined>; 20 | } 21 | }; 22 | } 23 | 24 | export function useCreateManySpace(options?: MutationOptions) { 25 | const mutation = request.useModelMutation('Space', 'POST', 'createMany', metadata, options, false); 26 | return { 27 | ...mutation, 28 | trigger: (args: Prisma.SelectSubset) => { 29 | return mutation.trigger(args, options as any) as Promise; 30 | } 31 | }; 32 | } 33 | 34 | export function useFindManySpace(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) { 35 | return request.useModelQuery('Space', 'findMany', args, options); 36 | } 37 | 38 | export function useInfiniteFindManySpace>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) { 39 | return request.useInfiniteModelQuery('Space', 'findMany', getNextArgs, options); 40 | } 41 | 42 | export function useFindUniqueSpace(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 43 | return request.useModelQuery('Space', 'findUnique', args, options); 44 | } 45 | 46 | export function useFindFirstSpace(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 47 | return request.useModelQuery('Space', 'findFirst', args, options); 48 | } 49 | 50 | export function useUpdateSpace(options?: MutationOptions | undefined, unknown, Prisma.SpaceUpdateArgs>) { 51 | const mutation = request.useModelMutation('Space', 'PUT', 'update', metadata, options, true); 52 | return { 53 | ...mutation, 54 | trigger: (args: Prisma.SelectSubset) => { 55 | return mutation.trigger(args, options as any) as Promise | undefined>; 56 | } 57 | }; 58 | } 59 | 60 | export function useUpdateManySpace(options?: MutationOptions) { 61 | const mutation = request.useModelMutation('Space', 'PUT', 'updateMany', metadata, options, false); 62 | return { 63 | ...mutation, 64 | trigger: (args: Prisma.SelectSubset) => { 65 | return mutation.trigger(args, options as any) as Promise; 66 | } 67 | }; 68 | } 69 | 70 | export function useUpsertSpace(options?: MutationOptions | undefined, unknown, Prisma.SpaceUpsertArgs>) { 71 | const mutation = request.useModelMutation('Space', 'POST', 'upsert', metadata, options, true); 72 | return { 73 | ...mutation, 74 | trigger: (args: Prisma.SelectSubset) => { 75 | return mutation.trigger(args, options as any) as Promise | undefined>; 76 | } 77 | }; 78 | } 79 | 80 | export function useDeleteSpace(options?: MutationOptions | undefined, unknown, Prisma.SpaceDeleteArgs>) { 81 | const mutation = request.useModelMutation('Space', 'DELETE', 'delete', metadata, options, true); 82 | return { 83 | ...mutation, 84 | trigger: (args: Prisma.SelectSubset) => { 85 | return mutation.trigger(args, options as any) as Promise | undefined>; 86 | } 87 | }; 88 | } 89 | 90 | export function useDeleteManySpace(options?: MutationOptions) { 91 | const mutation = request.useModelMutation('Space', 'DELETE', 'deleteMany', metadata, options, false); 92 | return { 93 | ...mutation, 94 | trigger: (args: Prisma.SelectSubset) => { 95 | return mutation.trigger(args, options as any) as Promise; 96 | } 97 | }; 98 | } 99 | 100 | export function useAggregateSpace(args?: Prisma.Subset, options?: QueryOptions>) { 101 | return request.useModelQuery('Space', 'aggregate', args, options); 102 | } 103 | 104 | export function useGroupBySpace>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.SpaceGroupByArgs['orderBy'] } : { orderBy?: Prisma.SpaceGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 105 | ? `Error: "by" must not be empty.` 106 | : HavingValid extends Prisma.False 107 | ? { 108 | [P in HavingFields]: P extends ByFields 109 | ? never 110 | : P extends string 111 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 112 | : [ 113 | Error, 114 | 'Field ', 115 | P, 116 | ` in "having" needs to be provided in "by"`, 117 | ] 118 | }[HavingFields] 119 | : 'take' extends Prisma.Keys 120 | ? 'orderBy' extends Prisma.Keys 121 | ? ByValid extends Prisma.True 122 | ? {} 123 | : { 124 | [P in OrderFields]: P extends ByFields 125 | ? never 126 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 127 | }[OrderFields] 128 | : 'Error: If you provide "take", you also need to provide "orderBy"' 129 | : 'skip' extends Prisma.Keys 130 | ? 'orderBy' extends Prisma.Keys 131 | ? ByValid extends Prisma.True 132 | ? {} 133 | : { 134 | [P in OrderFields]: P extends ByFields 135 | ? never 136 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 137 | }[OrderFields] 138 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 139 | : ByValid extends Prisma.True 140 | ? {} 141 | : { 142 | [P in OrderFields]: P extends ByFields 143 | ? never 144 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 145 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ? 146 | Array & 147 | { 148 | [P in ((keyof T) & (keyof Prisma.SpaceGroupByOutputType))]: P extends '_count' 149 | ? T[P] extends boolean 150 | ? number 151 | : Prisma.GetScalarType 152 | : Prisma.GetScalarType 153 | } 154 | > : InputErrors>) { 155 | return request.useModelQuery('Space', 'groupBy', args, options); 156 | } 157 | 158 | export function useCountSpace(args?: Prisma.Subset, options?: QueryOptions : number>) { 159 | return request.useModelQuery('Space', 'count', args, options); 160 | } 161 | 162 | export function useCheckSpace(args: { operation: PolicyCrudKind; where?: { id?: string; ownerId?: string; name?: string; slug?: string }; }, options?: QueryOptions) { 163 | return request.useModelQuery('Space', 'check', args, options); 164 | } 165 | -------------------------------------------------------------------------------- /lib/hooks/todo.ts: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * This file was generated by ZenStack CLI. 3 | ******************************************************************************/ 4 | 5 | /* eslint-disable */ 6 | // @ts-nocheck 7 | 8 | import type { Prisma } from "@zenstackhq/runtime/models"; 9 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime'; 10 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 11 | import metadata from './__model_meta'; 12 | import * as request from '@zenstackhq/swr/runtime'; 13 | 14 | export function useCreateTodo(options?: MutationOptions | undefined, unknown, Prisma.TodoCreateArgs>) { 15 | const mutation = request.useModelMutation('Todo', 'POST', 'create', metadata, options, true); 16 | return { 17 | ...mutation, 18 | trigger: (args: Prisma.SelectSubset) => { 19 | return mutation.trigger(args, options as any) as Promise | undefined>; 20 | } 21 | }; 22 | } 23 | 24 | export function useCreateManyTodo(options?: MutationOptions) { 25 | const mutation = request.useModelMutation('Todo', 'POST', 'createMany', metadata, options, false); 26 | return { 27 | ...mutation, 28 | trigger: (args: Prisma.SelectSubset) => { 29 | return mutation.trigger(args, options as any) as Promise; 30 | } 31 | }; 32 | } 33 | 34 | export function useFindManyTodo(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) { 35 | return request.useModelQuery('Todo', 'findMany', args, options); 36 | } 37 | 38 | export function useInfiniteFindManyTodo>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) { 39 | return request.useInfiniteModelQuery('Todo', 'findMany', getNextArgs, options); 40 | } 41 | 42 | export function useFindUniqueTodo(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 43 | return request.useModelQuery('Todo', 'findUnique', args, options); 44 | } 45 | 46 | export function useFindFirstTodo(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 47 | return request.useModelQuery('Todo', 'findFirst', args, options); 48 | } 49 | 50 | export function useUpdateTodo(options?: MutationOptions | undefined, unknown, Prisma.TodoUpdateArgs>) { 51 | const mutation = request.useModelMutation('Todo', 'PUT', 'update', metadata, options, true); 52 | return { 53 | ...mutation, 54 | trigger: (args: Prisma.SelectSubset) => { 55 | return mutation.trigger(args, options as any) as Promise | undefined>; 56 | } 57 | }; 58 | } 59 | 60 | export function useUpdateManyTodo(options?: MutationOptions) { 61 | const mutation = request.useModelMutation('Todo', 'PUT', 'updateMany', metadata, options, false); 62 | return { 63 | ...mutation, 64 | trigger: (args: Prisma.SelectSubset) => { 65 | return mutation.trigger(args, options as any) as Promise; 66 | } 67 | }; 68 | } 69 | 70 | export function useUpsertTodo(options?: MutationOptions | undefined, unknown, Prisma.TodoUpsertArgs>) { 71 | const mutation = request.useModelMutation('Todo', 'POST', 'upsert', metadata, options, true); 72 | return { 73 | ...mutation, 74 | trigger: (args: Prisma.SelectSubset) => { 75 | return mutation.trigger(args, options as any) as Promise | undefined>; 76 | } 77 | }; 78 | } 79 | 80 | export function useDeleteTodo(options?: MutationOptions | undefined, unknown, Prisma.TodoDeleteArgs>) { 81 | const mutation = request.useModelMutation('Todo', 'DELETE', 'delete', metadata, options, true); 82 | return { 83 | ...mutation, 84 | trigger: (args: Prisma.SelectSubset) => { 85 | return mutation.trigger(args, options as any) as Promise | undefined>; 86 | } 87 | }; 88 | } 89 | 90 | export function useDeleteManyTodo(options?: MutationOptions) { 91 | const mutation = request.useModelMutation('Todo', 'DELETE', 'deleteMany', metadata, options, false); 92 | return { 93 | ...mutation, 94 | trigger: (args: Prisma.SelectSubset) => { 95 | return mutation.trigger(args, options as any) as Promise; 96 | } 97 | }; 98 | } 99 | 100 | export function useAggregateTodo(args?: Prisma.Subset, options?: QueryOptions>) { 101 | return request.useModelQuery('Todo', 'aggregate', args, options); 102 | } 103 | 104 | export function useGroupByTodo>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.TodoGroupByArgs['orderBy'] } : { orderBy?: Prisma.TodoGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 105 | ? `Error: "by" must not be empty.` 106 | : HavingValid extends Prisma.False 107 | ? { 108 | [P in HavingFields]: P extends ByFields 109 | ? never 110 | : P extends string 111 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 112 | : [ 113 | Error, 114 | 'Field ', 115 | P, 116 | ` in "having" needs to be provided in "by"`, 117 | ] 118 | }[HavingFields] 119 | : 'take' extends Prisma.Keys 120 | ? 'orderBy' extends Prisma.Keys 121 | ? ByValid extends Prisma.True 122 | ? {} 123 | : { 124 | [P in OrderFields]: P extends ByFields 125 | ? never 126 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 127 | }[OrderFields] 128 | : 'Error: If you provide "take", you also need to provide "orderBy"' 129 | : 'skip' extends Prisma.Keys 130 | ? 'orderBy' extends Prisma.Keys 131 | ? ByValid extends Prisma.True 132 | ? {} 133 | : { 134 | [P in OrderFields]: P extends ByFields 135 | ? never 136 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 137 | }[OrderFields] 138 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 139 | : ByValid extends Prisma.True 140 | ? {} 141 | : { 142 | [P in OrderFields]: P extends ByFields 143 | ? never 144 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 145 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ? 146 | Array & 147 | { 148 | [P in ((keyof T) & (keyof Prisma.TodoGroupByOutputType))]: P extends '_count' 149 | ? T[P] extends boolean 150 | ? number 151 | : Prisma.GetScalarType 152 | : Prisma.GetScalarType 153 | } 154 | > : InputErrors>) { 155 | return request.useModelQuery('Todo', 'groupBy', args, options); 156 | } 157 | 158 | export function useCountTodo(args?: Prisma.Subset, options?: QueryOptions : number>) { 159 | return request.useModelQuery('Todo', 'count', args, options); 160 | } 161 | 162 | export function useCheckTodo(args: { operation: PolicyCrudKind; where?: { id?: string; ownerId?: string; listId?: string; title?: string }; }, options?: QueryOptions) { 163 | return request.useModelQuery('Todo', 'check', args, options); 164 | } 165 | -------------------------------------------------------------------------------- /lib/hooks/user.ts: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * This file was generated by ZenStack CLI. 3 | ******************************************************************************/ 4 | 5 | /* eslint-disable */ 6 | // @ts-nocheck 7 | 8 | import type { Prisma } from "@zenstackhq/runtime/models"; 9 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime'; 10 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 11 | import metadata from './__model_meta'; 12 | import * as request from '@zenstackhq/swr/runtime'; 13 | 14 | export function useCreateUser(options?: MutationOptions | undefined, unknown, Prisma.UserCreateArgs>) { 15 | const mutation = request.useModelMutation('User', 'POST', 'create', metadata, options, true); 16 | return { 17 | ...mutation, 18 | trigger: (args: Prisma.SelectSubset) => { 19 | return mutation.trigger(args, options as any) as Promise | undefined>; 20 | } 21 | }; 22 | } 23 | 24 | export function useCreateManyUser(options?: MutationOptions) { 25 | const mutation = request.useModelMutation('User', 'POST', 'createMany', metadata, options, false); 26 | return { 27 | ...mutation, 28 | trigger: (args: Prisma.SelectSubset) => { 29 | return mutation.trigger(args, options as any) as Promise; 30 | } 31 | }; 32 | } 33 | 34 | export function useFindManyUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) { 35 | return request.useModelQuery('User', 'findMany', args, options); 36 | } 37 | 38 | export function useInfiniteFindManyUser>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) { 39 | return request.useInfiniteModelQuery('User', 'findMany', getNextArgs, options); 40 | } 41 | 42 | export function useFindUniqueUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 43 | return request.useModelQuery('User', 'findUnique', args, options); 44 | } 45 | 46 | export function useFindFirstUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) { 47 | return request.useModelQuery('User', 'findFirst', args, options); 48 | } 49 | 50 | export function useUpdateUser(options?: MutationOptions | undefined, unknown, Prisma.UserUpdateArgs>) { 51 | const mutation = request.useModelMutation('User', 'PUT', 'update', metadata, options, true); 52 | return { 53 | ...mutation, 54 | trigger: (args: Prisma.SelectSubset) => { 55 | return mutation.trigger(args, options as any) as Promise | undefined>; 56 | } 57 | }; 58 | } 59 | 60 | export function useUpdateManyUser(options?: MutationOptions) { 61 | const mutation = request.useModelMutation('User', 'PUT', 'updateMany', metadata, options, false); 62 | return { 63 | ...mutation, 64 | trigger: (args: Prisma.SelectSubset) => { 65 | return mutation.trigger(args, options as any) as Promise; 66 | } 67 | }; 68 | } 69 | 70 | export function useUpsertUser(options?: MutationOptions | undefined, unknown, Prisma.UserUpsertArgs>) { 71 | const mutation = request.useModelMutation('User', 'POST', 'upsert', metadata, options, true); 72 | return { 73 | ...mutation, 74 | trigger: (args: Prisma.SelectSubset) => { 75 | return mutation.trigger(args, options as any) as Promise | undefined>; 76 | } 77 | }; 78 | } 79 | 80 | export function useDeleteUser(options?: MutationOptions | undefined, unknown, Prisma.UserDeleteArgs>) { 81 | const mutation = request.useModelMutation('User', 'DELETE', 'delete', metadata, options, true); 82 | return { 83 | ...mutation, 84 | trigger: (args: Prisma.SelectSubset) => { 85 | return mutation.trigger(args, options as any) as Promise | undefined>; 86 | } 87 | }; 88 | } 89 | 90 | export function useDeleteManyUser(options?: MutationOptions) { 91 | const mutation = request.useModelMutation('User', 'DELETE', 'deleteMany', metadata, options, false); 92 | return { 93 | ...mutation, 94 | trigger: (args: Prisma.SelectSubset) => { 95 | return mutation.trigger(args, options as any) as Promise; 96 | } 97 | }; 98 | } 99 | 100 | export function useAggregateUser(args?: Prisma.Subset, options?: QueryOptions>) { 101 | return request.useModelQuery('User', 'aggregate', args, options); 102 | } 103 | 104 | export function useGroupByUser>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 105 | ? `Error: "by" must not be empty.` 106 | : HavingValid extends Prisma.False 107 | ? { 108 | [P in HavingFields]: P extends ByFields 109 | ? never 110 | : P extends string 111 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 112 | : [ 113 | Error, 114 | 'Field ', 115 | P, 116 | ` in "having" needs to be provided in "by"`, 117 | ] 118 | }[HavingFields] 119 | : 'take' extends Prisma.Keys 120 | ? 'orderBy' extends Prisma.Keys 121 | ? ByValid extends Prisma.True 122 | ? {} 123 | : { 124 | [P in OrderFields]: P extends ByFields 125 | ? never 126 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 127 | }[OrderFields] 128 | : 'Error: If you provide "take", you also need to provide "orderBy"' 129 | : 'skip' extends Prisma.Keys 130 | ? 'orderBy' extends Prisma.Keys 131 | ? ByValid extends Prisma.True 132 | ? {} 133 | : { 134 | [P in OrderFields]: P extends ByFields 135 | ? never 136 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 137 | }[OrderFields] 138 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 139 | : ByValid extends Prisma.True 140 | ? {} 141 | : { 142 | [P in OrderFields]: P extends ByFields 143 | ? never 144 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 145 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ? 146 | Array & 147 | { 148 | [P in ((keyof T) & (keyof Prisma.UserGroupByOutputType))]: P extends '_count' 149 | ? T[P] extends boolean 150 | ? number 151 | : Prisma.GetScalarType 152 | : Prisma.GetScalarType 153 | } 154 | > : InputErrors>) { 155 | return request.useModelQuery('User', 'groupBy', args, options); 156 | } 157 | 158 | export function useCountUser(args?: Prisma.Subset, options?: QueryOptions : number>) { 159 | return request.useModelQuery('User', 'count', args, options); 160 | } 161 | 162 | export function useCheckUser(args: { operation: PolicyCrudKind; where?: { id?: string; email?: string; password?: string; name?: string; image?: string }; }, options?: QueryOptions) { 163 | return request.useModelQuery('User', 'check', args, options); 164 | } 165 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | images: { 6 | remotePatterns: [ 7 | { hostname: 'picsum.photos' }, 8 | { hostname: 'lh3.googleusercontent.com' }, 9 | { hostname: 'avatars.githubusercontent.com' }, 10 | ], 11 | }, 12 | }; 13 | 14 | module.exports = nextConfig; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zenstack-todo-sample-nextjs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "npm run generate && npm run lint && next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:push": "prisma db push", 11 | "db:migrate": "prisma migrate dev", 12 | "db:deploy": "prisma migrate deploy", 13 | "db:reset": "prisma migrate reset", 14 | "db:browse": "prisma studio", 15 | "generate": "zenstack generate", 16 | "vercel-build": "npm run build && npm run db:deploy", 17 | "package-clean": "npm rm zenstack @zenstackhq/runtime @zenstackhq/server @zenstackhq/swr", 18 | "up": "npm run package-clean && npm install -D --save-exact zenstack@latest @zenstackhq/swr@latest && npm install --save-exact @zenstackhq/runtime@latest @zenstackhq/server@latest", 19 | "up-preview": "npm run package-clean && npm install --registry https://preview.registry.zenstack.dev -D zenstack@latest @zenstackhq/swr@latest && npm install --registry https://preview.registry.zenstack.dev @zenstackhq/runtime@latest @zenstackhq/server@latest" 20 | }, 21 | "dependencies": { 22 | "@heroicons/react": "^2.0.12", 23 | "@next-auth/prisma-adapter": "^1.0.6", 24 | "@prisma/client": "^6.1.0", 25 | "@vercel/analytics": "^1.0.1", 26 | "@zenstackhq/runtime": "2.15.0", 27 | "@zenstackhq/server": "2.15.0", 28 | "babel-plugin-superjson-next": "^0.4.5", 29 | "bcryptjs": "^2.4.3", 30 | "daisyui": "^4.4.10", 31 | "moment": "^2.29.4", 32 | "nanoid": "^4.0.0", 33 | "next": "^14.0.3", 34 | "next-auth": "^4.24.5", 35 | "react": "^18.2.0", 36 | "react-dom": "18.2.0", 37 | "react-toastify": "^9.0.8", 38 | "superjson": "^1.12.0", 39 | "swr": "^2.2.5" 40 | }, 41 | "devDependencies": { 42 | "@tailwindcss/line-clamp": "^0.4.2", 43 | "@types/bcryptjs": "^2.4.2", 44 | "@types/node": "^14.17.3", 45 | "@types/react": "^18.2.22", 46 | "@types/react-dom": "18.0.6", 47 | "@typescript-eslint/eslint-plugin": "^6.13.1", 48 | "@typescript-eslint/parser": "^6.13.1", 49 | "@zenstackhq/swr": "2.15.0", 50 | "autoprefixer": "^10.4.12", 51 | "eslint": "^7.19.0", 52 | "eslint-config-next": "12.3.1", 53 | "postcss": "^8.4.16", 54 | "prisma": "^6.1.0", 55 | "tailwindcss": "^3.1.8", 56 | "typescript": "^5.1.6", 57 | "zenstack": "2.15.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceContext, useCurrentSpace, useCurrentUser, UserContext } from '@lib/context'; 2 | import AuthGuard from 'components/AuthGuard'; 3 | import { SessionProvider } from 'next-auth/react'; 4 | import type { AppProps } from 'next/app'; 5 | import { ToastContainer } from 'react-toastify'; 6 | import 'react-toastify/dist/ReactToastify.css'; 7 | import { Provider as ZenStackHooksProvider } from '../lib/hooks'; 8 | import { Analytics } from '@vercel/analytics/react'; 9 | import '../styles/globals.css'; 10 | 11 | function AppContent(props: { children: JSX.Element | JSX.Element[] }) { 12 | const user = useCurrentUser(); 13 | const space = useCurrentSpace(); 14 | 15 | return ( 16 | 17 | 18 | 19 |
{props.children}
20 |
21 |
22 |
23 | ); 24 | } 25 | 26 | function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { 27 | return ( 28 | <> 29 | 30 | 31 | 32 |
33 | 34 | 35 |
36 |
37 |
38 |
39 | 40 | 41 | ); 42 | } 43 | 44 | export default MyApp; 45 | -------------------------------------------------------------------------------- /pages/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs/89221497884b3597649d70ac61c43721b15e7c3b/pages/api/.DS_Store -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import { PrismaAdapter } from '@next-auth/prisma-adapter'; 2 | import { PrismaClient, SpaceUserRole } from '@prisma/client'; 3 | import { compare } from 'bcryptjs'; 4 | import { nanoid } from 'nanoid'; 5 | import NextAuth, { NextAuthOptions, User } from 'next-auth'; 6 | import CredentialsProvider from 'next-auth/providers/credentials'; 7 | import GitHubProvider from 'next-auth/providers/github'; 8 | import { prisma } from 'server/db'; 9 | 10 | export const authOptions: NextAuthOptions = { 11 | adapter: PrismaAdapter(prisma), 12 | 13 | session: { 14 | strategy: 'jwt', 15 | }, 16 | 17 | pages: { 18 | signIn: '/signin', 19 | }, 20 | 21 | providers: [ 22 | CredentialsProvider({ 23 | credentials: { 24 | email: { 25 | type: 'email', 26 | }, 27 | password: { 28 | type: 'password', 29 | }, 30 | }, 31 | authorize: authorize(prisma), 32 | }), 33 | 34 | GitHubProvider({ 35 | clientId: process.env.GITHUB_ID!, 36 | clientSecret: process.env.GITHUB_SECRET!, 37 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 38 | // @ts-ignore 39 | scope: 'read:user,user:email', 40 | }), 41 | ], 42 | 43 | callbacks: { 44 | session({ session, token }) { 45 | return { 46 | ...session, 47 | user: { 48 | ...session.user, 49 | id: token.sub!, 50 | }, 51 | }; 52 | }, 53 | }, 54 | 55 | events: { 56 | async signIn({ user }: { user: User }) { 57 | const spaceCount = await prisma.spaceUser.count({ 58 | where: { 59 | userId: user.id, 60 | }, 61 | }); 62 | if (spaceCount > 0) { 63 | return; 64 | } 65 | 66 | console.log(`User ${user.id} doesn't belong to any space. Creating one.`); 67 | const space = await prisma.space.create({ 68 | data: { 69 | name: `${user.name || user.email}'s space`, 70 | slug: nanoid(8), 71 | owner: { connect: { id: user.id } }, 72 | members: { 73 | create: [ 74 | { 75 | userId: user.id, 76 | role: SpaceUserRole.ADMIN, 77 | }, 78 | ], 79 | }, 80 | }, 81 | }); 82 | console.log(`Space created:`, space); 83 | }, 84 | }, 85 | }; 86 | 87 | function authorize(prisma: PrismaClient) { 88 | return async (credentials: Record<'email' | 'password', string> | undefined) => { 89 | if (!credentials) { 90 | throw new Error('Missing credentials'); 91 | } 92 | 93 | if (!credentials.email) { 94 | throw new Error('"email" is required in credentials'); 95 | } 96 | 97 | if (!credentials.password) { 98 | throw new Error('"password" is required in credentials'); 99 | } 100 | 101 | const maybeUser = await prisma.user.findFirst({ 102 | where: { 103 | email: credentials.email, 104 | }, 105 | select: { 106 | id: true, 107 | email: true, 108 | password: true, 109 | }, 110 | }); 111 | 112 | if (!maybeUser || !maybeUser.password) { 113 | return null; 114 | } 115 | 116 | const isValid = await compare(credentials.password, maybeUser.password); 117 | 118 | if (!isValid) { 119 | return null; 120 | } 121 | 122 | return { 123 | id: maybeUser.id, 124 | email: maybeUser.email, 125 | }; 126 | }; 127 | } 128 | 129 | export default NextAuth(authOptions); 130 | -------------------------------------------------------------------------------- /pages/api/model/[...path].ts: -------------------------------------------------------------------------------- 1 | import { NextRequestHandler } from '@zenstackhq/server/next'; 2 | import { getEnhancedPrisma } from 'server/enhanced-db'; 3 | 4 | export default NextRequestHandler({ 5 | getPrisma: (req, res) => getEnhancedPrisma({ req, res }), 6 | }); 7 | -------------------------------------------------------------------------------- /pages/create-space.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 3 | import { useCreateSpace } from '@lib/hooks'; 4 | import { SpaceUserRole } from '@prisma/client'; 5 | import WithNavBar from 'components/WithNavBar'; 6 | import { NextPage } from 'next'; 7 | import { useSession } from 'next-auth/react'; 8 | import { useRouter } from 'next/router'; 9 | import { FormEvent, useState } from 'react'; 10 | import { toast } from 'react-toastify'; 11 | 12 | const CreateSpace: NextPage = () => { 13 | const { data: session } = useSession(); 14 | const [name, setName] = useState(''); 15 | const [slug, setSlug] = useState(''); 16 | 17 | const { trigger: createSpace } = useCreateSpace(); 18 | const router = useRouter(); 19 | 20 | const onSubmit = async (event: FormEvent) => { 21 | event.preventDefault(); 22 | try { 23 | const space = await createSpace({ 24 | data: { 25 | name, 26 | slug, 27 | members: { 28 | create: [ 29 | { 30 | userId: session!.user.id, 31 | role: SpaceUserRole.ADMIN, 32 | }, 33 | ], 34 | }, 35 | }, 36 | }); 37 | console.log('Space created:', space); 38 | toast.success("Space created successfully! You'll be redirected."); 39 | 40 | setTimeout(() => { 41 | if (space) { 42 | void router.push(`/space/${space.slug}`); 43 | } 44 | }, 2000); 45 | } catch (err: any) { 46 | console.error(err); 47 | if (err.info?.prisma === true) { 48 | if (err.info.code === 'P2002') { 49 | toast.error('Space slug already in use'); 50 | } else { 51 | toast.error(`Unexpected Prisma error: ${err.info.code}`); 52 | } 53 | } else { 54 | toast.error(JSON.stringify(err)); 55 | } 56 | } 57 | }; 58 | 59 | return ( 60 | 61 |
62 |
void onSubmit(e)}> 63 |

Create a space

64 |
65 |
66 | 69 | ) => setName(e.currentTarget.value)} 77 | /> 78 |
79 |
80 | 83 | ) => setSlug(e.currentTarget.value)} 90 | /> 91 |
92 |
93 | 94 |
95 | 20 || !slug.match(/^[0-9a-zA-Z]{4,16}$/)} 98 | value="Create" 99 | className="btn btn-primary px-8" 100 | /> 101 | 110 |
111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default CreateSpace; 118 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCurrentUser } from '@lib/context'; 2 | import { Space } from '@prisma/client'; 3 | import Spaces from 'components/Spaces'; 4 | import WithNavBar from 'components/WithNavBar'; 5 | import type { GetServerSideProps, NextPage } from 'next'; 6 | import Link from 'next/link'; 7 | import { getEnhancedPrisma } from 'server/enhanced-db'; 8 | 9 | type Props = { 10 | spaces: Space[]; 11 | }; 12 | 13 | const Home: NextPage = ({ spaces }) => { 14 | const user = useCurrentUser(); 15 | 16 | return ( 17 | 18 | {user && ( 19 |
20 |

Welcome {user.name || user.email}!

21 | 22 |
23 |

24 | Choose a space to start, or{' '} 25 | 26 | create a new one. 27 | 28 |

29 | 30 |
31 |
32 | )} 33 |
34 | ); 35 | }; 36 | 37 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 38 | const db = await getEnhancedPrisma(ctx); 39 | const spaces = await db.space.findMany(); 40 | return { 41 | props: { spaces }, 42 | }; 43 | }; 44 | 45 | export default Home; 46 | -------------------------------------------------------------------------------- /pages/signin.tsx: -------------------------------------------------------------------------------- 1 | import { signIn } from 'next-auth/react'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/router'; 5 | import { FormEvent, useEffect, useState } from 'react'; 6 | import { toast } from 'react-toastify'; 7 | 8 | export default function Signup() { 9 | const [email, setEmail] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | const router = useRouter(); 12 | 13 | useEffect(() => { 14 | if (router.query.error) { 15 | if (router.query.error === 'OAuthCreateAccount') { 16 | toast.error('Unable to signin. The user email may be already in use.'); 17 | } else { 18 | toast.error(`Authentication error: ${router.query.error.toString()}`); 19 | } 20 | } 21 | }, [router]); 22 | 23 | async function onSignin(e: FormEvent) { 24 | e.preventDefault(); 25 | const signInResult = await signIn('credentials', { 26 | redirect: false, 27 | email, 28 | password, 29 | }); 30 | if (signInResult?.ok) { 31 | window.location.href = '/'; 32 | } else { 33 | toast.error(`Signin failed. Please check your email and password.`); 34 | } 35 | } 36 | 37 | return ( 38 |
39 | 40 |
41 | logo 42 |

Welcome to Todo

43 |
44 | 45 |
46 |
47 |

Sign in to your account

48 | 49 |
void onSignin(e)}> 50 |
51 | 54 | setEmail(e.target.value)} 59 | className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5" 60 | placeholder="Email address" 61 | required 62 | /> 63 |
64 |
65 | 68 | setPassword(e.target.value)} 73 | placeholder="••••••••" 74 | className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5" 75 | required 76 | /> 77 |
78 |
79 |
80 | 87 |
88 |
89 | 92 |
93 |
94 | 95 |
96 | 99 | 100 |
void signIn('github', { callbackUrl: '/' })} 103 | > 104 | Sign in with GitHub 105 |
106 |
107 | 108 |
109 | Not registered?{' '} 110 | 111 | Create account 112 | 113 |
114 |
115 |
116 |
117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /pages/signup.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | import { useCreateUser } from '@lib/hooks'; 3 | import { signIn } from 'next-auth/react'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | import { FormEvent, useState } from 'react'; 7 | import { toast } from 'react-toastify'; 8 | 9 | export default function Signup() { 10 | const [email, setEmail] = useState(''); 11 | const [password, setPassword] = useState(''); 12 | const { trigger: createUser } = useCreateUser(); 13 | 14 | async function onSignup(e: FormEvent) { 15 | e.preventDefault(); 16 | try { 17 | await createUser({ data: { email, password } }); 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | } catch (err: any) { 20 | console.error(err); 21 | if (err.info?.prisma === true) { 22 | if (err.info.code === 'P2002') { 23 | toast.error('User already exists'); 24 | } else { 25 | toast.error(`Unexpected Prisma error: ${err.info.code}`); 26 | } 27 | } else { 28 | toast.error(`Error occurred: ${JSON.stringify(err)}`); 29 | } 30 | return; 31 | } 32 | 33 | const signInResult = await signIn('credentials', { 34 | redirect: false, 35 | email, 36 | password, 37 | }); 38 | if (signInResult?.ok) { 39 | window.location.href = '/'; 40 | } else { 41 | console.error('Signin failed:', signInResult?.error); 42 | } 43 | } 44 | 45 | return ( 46 |
47 | 48 |
49 | logo 50 |

Welcome to Todo

51 |
52 | 53 |
54 |
55 |

Create a Free Account

56 |
void onSignup(e)}> 57 |
58 | 61 | setEmail(e.target.value)} 66 | className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5" 67 | placeholder="Email address" 68 | required 69 | /> 70 |
71 |
72 | 75 | setPassword(e.target.value)} 80 | placeholder="••••••••" 81 | className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5" 82 | required 83 | /> 84 |
85 |
86 |
87 | 95 |
96 |
97 | 103 |
104 |
105 | 108 |
109 | Already have an account?{' '} 110 | 111 | Login here 112 | 113 |
114 |
115 |
116 |
117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /pages/space/[slug]/[listId]/index.tsx: -------------------------------------------------------------------------------- 1 | import { PlusIcon } from '@heroicons/react/24/outline'; 2 | import { useCreateTodo, useFindManyTodo } from '@lib/hooks'; 3 | import { List, Space } from '@prisma/client'; 4 | import BreadCrumb from 'components/BreadCrumb'; 5 | import TodoComponent from 'components/Todo'; 6 | import WithNavBar from 'components/WithNavBar'; 7 | import { GetServerSideProps } from 'next'; 8 | import { ChangeEvent, KeyboardEvent, useState } from 'react'; 9 | import { getEnhancedPrisma } from 'server/enhanced-db'; 10 | 11 | type Props = { 12 | space: Space; 13 | list: List; 14 | }; 15 | 16 | export default function TodoList(props: Props) { 17 | const [title, setTitle] = useState(''); 18 | const { trigger: createTodo } = useCreateTodo({ optimisticUpdate: true }); 19 | 20 | const { data: todos } = useFindManyTodo( 21 | { 22 | where: { listId: props.list.id }, 23 | include: { 24 | owner: true, 25 | }, 26 | orderBy: { 27 | createdAt: 'desc', 28 | }, 29 | }, 30 | { keepPreviousData: true } 31 | ); 32 | 33 | const _createTodo = () => { 34 | void createTodo({ 35 | data: { 36 | title, 37 | list: { connect: { id: props.list.id } }, 38 | }, 39 | }); 40 | setTitle(''); 41 | }; 42 | 43 | if (!props.space || !props.list) { 44 | return <>; 45 | } 46 | 47 | return ( 48 | 49 |
50 | 51 |
52 |
53 |

{props.list?.title}

54 |
55 | ) => { 61 | if (e.key === 'Enter') { 62 | _createTodo(); 63 | } 64 | }} 65 | onChange={(e: ChangeEvent) => { 66 | setTitle(e.currentTarget.value); 67 | }} 68 | /> 69 | 72 |
73 | 74 |
    75 | {todos?.map((todo) => ( 76 | 77 | ))} 78 |
79 |
80 |
81 | ); 82 | } 83 | 84 | export const getServerSideProps: GetServerSideProps = async ({ req, res, params }) => { 85 | const db = await getEnhancedPrisma({ req, res }); 86 | const space = await db.space.findUnique({ 87 | where: { slug: params!.slug as string }, 88 | }); 89 | if (!space) { 90 | return { 91 | notFound: true, 92 | }; 93 | } 94 | 95 | const list = await db.list.findUnique({ 96 | where: { id: params!.listId as string }, 97 | }); 98 | if (!list) { 99 | return { 100 | notFound: true, 101 | }; 102 | } 103 | 104 | return { 105 | props: { space, list }, 106 | }; 107 | }; 108 | -------------------------------------------------------------------------------- /pages/space/[slug]/index.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceContext } from '@lib/context'; 2 | import { useCreateList, useFindManyList } from '@lib/hooks'; 3 | import { List, Space, User } from '@prisma/client'; 4 | import BreadCrumb from 'components/BreadCrumb'; 5 | import SpaceMembers from 'components/SpaceMembers'; 6 | import TodoList from 'components/TodoList'; 7 | import WithNavBar from 'components/WithNavBar'; 8 | import { GetServerSideProps } from 'next'; 9 | import { useRouter } from 'next/router'; 10 | import { ChangeEvent, FormEvent, useContext, useEffect, useRef, useState } from 'react'; 11 | import { toast } from 'react-toastify'; 12 | import { getEnhancedPrisma } from 'server/enhanced-db'; 13 | 14 | function CreateDialog() { 15 | const space = useContext(SpaceContext); 16 | 17 | const [modalOpen, setModalOpen] = useState(false); 18 | const [title, setTitle] = useState(''); 19 | const [_private, setPrivate] = useState(false); 20 | 21 | const { trigger: createList } = useCreateList({ 22 | onSuccess: () => { 23 | toast.success('List created successfully!'); 24 | 25 | // reset states 26 | setTitle(''); 27 | setPrivate(false); 28 | 29 | // close modal 30 | setModalOpen(false); 31 | }, 32 | }); 33 | 34 | const inputRef = useRef(null); 35 | 36 | useEffect(() => { 37 | if (modalOpen) { 38 | inputRef.current?.focus(); 39 | } 40 | }, [modalOpen]); 41 | 42 | const onSubmit = (event: FormEvent) => { 43 | event.preventDefault(); 44 | 45 | void createList({ 46 | data: { 47 | title, 48 | private: _private, 49 | space: { connect: { id: space!.id } }, 50 | }, 51 | }); 52 | }; 53 | 54 | return ( 55 | <> 56 | ) => { 62 | setModalOpen(e.currentTarget.checked); 63 | }} 64 | /> 65 |
66 |
67 |

Create a Todo list

68 |
69 |
70 |
71 | 74 | ) => setTitle(e.currentTarget.value)} 83 | /> 84 |
85 |
86 | 89 | ) => setPrivate(e.currentTarget.checked)} 94 | /> 95 |
96 |
97 |
98 | 99 | 102 |
103 |
104 |
105 |
106 | 107 | ); 108 | } 109 | 110 | type Props = { 111 | space: Space; 112 | lists: (List & { owner: User })[]; 113 | }; 114 | 115 | export default function SpaceHome(props: Props) { 116 | const router = useRouter(); 117 | 118 | const { data: lists } = useFindManyList( 119 | { 120 | where: { 121 | space: { 122 | slug: router.query.slug as string, 123 | }, 124 | }, 125 | include: { 126 | owner: true, 127 | }, 128 | orderBy: { 129 | updatedAt: 'desc', 130 | }, 131 | }, 132 | { 133 | disabled: !router.query.slug, 134 | fallbackData: props.lists, 135 | } 136 | ); 137 | 138 | return ( 139 | 140 |
141 | 142 |
143 |
144 |
145 | 148 | 149 |
150 | 151 |
    152 | {lists?.map((list) => ( 153 |
  • 154 | 155 |
  • 156 | ))} 157 |
158 | 159 | 160 |
161 |
162 | ); 163 | } 164 | 165 | export const getServerSideProps: GetServerSideProps = async ({ req, res, params }) => { 166 | const db = await getEnhancedPrisma({ req, res }); 167 | 168 | const space = await db.space.findUnique({ 169 | where: { slug: params!.slug as string }, 170 | }); 171 | if (!space) { 172 | return { 173 | notFound: true, 174 | }; 175 | } 176 | 177 | const lists = await db.list.findMany({ 178 | where: { 179 | space: { slug: params?.slug as string }, 180 | }, 181 | include: { 182 | owner: true, 183 | }, 184 | orderBy: { 185 | updatedAt: 'desc', 186 | }, 187 | }); 188 | 189 | return { 190 | props: { space, lists }, 191 | }; 192 | }; 193 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20221014084317_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "SpaceUserRole" AS ENUM ('USER', 'ADMIN'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Space" ( 6 | "id" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL, 9 | "name" TEXT NOT NULL, 10 | "slug" TEXT NOT NULL, 11 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true, 12 | 13 | CONSTRAINT "Space_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateTable 17 | CREATE TABLE "SpaceUser" ( 18 | "id" TEXT NOT NULL, 19 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | "updatedAt" TIMESTAMP(3) NOT NULL, 21 | "spaceId" TEXT NOT NULL, 22 | "userId" TEXT NOT NULL, 23 | "role" "SpaceUserRole" NOT NULL, 24 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true, 25 | 26 | CONSTRAINT "SpaceUser_pkey" PRIMARY KEY ("id") 27 | ); 28 | 29 | -- CreateTable 30 | CREATE TABLE "User" ( 31 | "id" TEXT NOT NULL, 32 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 33 | "updatedAt" TIMESTAMP(3) NOT NULL, 34 | "email" TEXT NOT NULL, 35 | "emailVerified" TIMESTAMP(3), 36 | "password" TEXT, 37 | "name" TEXT, 38 | "image" TEXT, 39 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true, 40 | 41 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 42 | ); 43 | 44 | -- CreateTable 45 | CREATE TABLE "List" ( 46 | "id" TEXT NOT NULL, 47 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 48 | "updatedAt" TIMESTAMP(3) NOT NULL, 49 | "spaceId" TEXT NOT NULL, 50 | "ownerId" TEXT NOT NULL, 51 | "title" TEXT NOT NULL, 52 | "private" BOOLEAN NOT NULL DEFAULT false, 53 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true, 54 | 55 | CONSTRAINT "List_pkey" PRIMARY KEY ("id") 56 | ); 57 | 58 | -- CreateTable 59 | CREATE TABLE "Todo" ( 60 | "id" TEXT NOT NULL, 61 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 62 | "updatedAt" TIMESTAMP(3) NOT NULL, 63 | "ownerId" TEXT NOT NULL, 64 | "listId" TEXT NOT NULL, 65 | "title" TEXT NOT NULL, 66 | "completedAt" TIMESTAMP(3), 67 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true, 68 | 69 | CONSTRAINT "Todo_pkey" PRIMARY KEY ("id") 70 | ); 71 | 72 | -- CreateTable 73 | CREATE TABLE "Account" ( 74 | "id" TEXT NOT NULL, 75 | "userId" TEXT NOT NULL, 76 | "type" TEXT NOT NULL, 77 | "provider" TEXT NOT NULL, 78 | "providerAccountId" TEXT NOT NULL, 79 | "refresh_token" TEXT, 80 | "access_token" TEXT, 81 | "expires_at" INTEGER, 82 | "token_type" TEXT, 83 | "scope" TEXT, 84 | "id_token" TEXT, 85 | "session_state" TEXT, 86 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true, 87 | 88 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 89 | ); 90 | 91 | -- CreateTable 92 | CREATE TABLE "Session" ( 93 | "id" TEXT NOT NULL, 94 | "sessionToken" TEXT NOT NULL, 95 | "userId" TEXT NOT NULL, 96 | "expires" TIMESTAMP(3) NOT NULL, 97 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true, 98 | 99 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 100 | ); 101 | 102 | -- CreateTable 103 | CREATE TABLE "VerificationToken" ( 104 | "identifier" TEXT NOT NULL, 105 | "token" TEXT NOT NULL, 106 | "expires" TIMESTAMP(3) NOT NULL, 107 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true 108 | ); 109 | 110 | -- CreateIndex 111 | CREATE UNIQUE INDEX "Space_slug_key" ON "Space"("slug"); 112 | 113 | -- CreateIndex 114 | CREATE UNIQUE INDEX "SpaceUser_userId_spaceId_key" ON "SpaceUser"("userId", "spaceId"); 115 | 116 | -- CreateIndex 117 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 118 | 119 | -- CreateIndex 120 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 121 | 122 | -- CreateIndex 123 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 124 | 125 | -- CreateIndex 126 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 127 | 128 | -- CreateIndex 129 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 130 | 131 | -- AddForeignKey 132 | ALTER TABLE "SpaceUser" ADD CONSTRAINT "SpaceUser_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space"("id") ON DELETE CASCADE ON UPDATE CASCADE; 133 | 134 | -- AddForeignKey 135 | ALTER TABLE "SpaceUser" ADD CONSTRAINT "SpaceUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 136 | 137 | -- AddForeignKey 138 | ALTER TABLE "List" ADD CONSTRAINT "List_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space"("id") ON DELETE CASCADE ON UPDATE CASCADE; 139 | 140 | -- AddForeignKey 141 | ALTER TABLE "List" ADD CONSTRAINT "List_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 142 | 143 | -- AddForeignKey 144 | ALTER TABLE "Todo" ADD CONSTRAINT "Todo_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 145 | 146 | -- AddForeignKey 147 | ALTER TABLE "Todo" ADD CONSTRAINT "Todo_listId_fkey" FOREIGN KEY ("listId") REFERENCES "List"("id") ON DELETE CASCADE ON UPDATE CASCADE; 148 | 149 | -- AddForeignKey 150 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 151 | 152 | -- AddForeignKey 153 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 154 | -------------------------------------------------------------------------------- /prisma/migrations/20221020094651_upate_cli/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Account" ADD COLUMN "zenstack_transaction" TEXT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "List" ADD COLUMN "zenstack_transaction" TEXT; 6 | 7 | -- AlterTable 8 | ALTER TABLE "Session" ADD COLUMN "zenstack_transaction" TEXT; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Space" ADD COLUMN "zenstack_transaction" TEXT; 12 | 13 | -- AlterTable 14 | ALTER TABLE "SpaceUser" ADD COLUMN "zenstack_transaction" TEXT; 15 | 16 | -- AlterTable 17 | ALTER TABLE "Todo" ADD COLUMN "zenstack_transaction" TEXT; 18 | 19 | -- AlterTable 20 | ALTER TABLE "User" ADD COLUMN "zenstack_transaction" TEXT; 21 | 22 | -- AlterTable 23 | ALTER TABLE "VerificationToken" ADD COLUMN "zenstack_transaction" TEXT; 24 | -------------------------------------------------------------------------------- /prisma/migrations/20221103144245_drop_account_session/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost. 6 | - You are about to drop the `VerificationToken` table. If the table is not empty, all the data it contains will be lost. 7 | - Made the column `password` on table `User` required. This step will fail if there are existing NULL values in that column. 8 | 9 | */ 10 | -- DropForeignKey 11 | ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey"; 12 | 13 | -- DropForeignKey 14 | ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey"; 15 | 16 | -- AlterTable 17 | ALTER TABLE "User" ALTER COLUMN "password" SET NOT NULL; 18 | 19 | -- DropTable 20 | DROP TABLE "Account"; 21 | 22 | -- DropTable 23 | DROP TABLE "Session"; 24 | 25 | -- DropTable 26 | DROP TABLE "VerificationToken"; 27 | 28 | -- CreateIndex 29 | CREATE INDEX "List_zenstack_transaction_idx" ON "List"("zenstack_transaction"); 30 | 31 | -- CreateIndex 32 | CREATE INDEX "Space_zenstack_transaction_idx" ON "Space"("zenstack_transaction"); 33 | 34 | -- CreateIndex 35 | CREATE INDEX "SpaceUser_zenstack_transaction_idx" ON "SpaceUser"("zenstack_transaction"); 36 | 37 | -- CreateIndex 38 | CREATE INDEX "Todo_zenstack_transaction_idx" ON "Todo"("zenstack_transaction"); 39 | 40 | -- CreateIndex 41 | CREATE INDEX "User_zenstack_transaction_idx" ON "User"("zenstack_transaction"); 42 | -------------------------------------------------------------------------------- /prisma/migrations/20221126150023_add_account/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Account" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "type" TEXT NOT NULL, 6 | "provider" TEXT NOT NULL, 7 | "providerAccountId" TEXT NOT NULL, 8 | "refresh_token" TEXT, 9 | "access_token" TEXT, 10 | "expires_at" INTEGER, 11 | "token_type" TEXT, 12 | "scope" TEXT, 13 | "id_token" TEXT, 14 | "session_state" TEXT, 15 | "zenstack_guard" BOOLEAN NOT NULL DEFAULT true, 16 | "zenstack_transaction" TEXT, 17 | 18 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE INDEX "Account_zenstack_transaction_idx" ON "Account"("zenstack_transaction"); 23 | 24 | -- CreateIndex 25 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 29 | -------------------------------------------------------------------------------- /prisma/migrations/20221126151212_email_password_optional/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL, 3 | ALTER COLUMN "password" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20221126151510_refresh_token_expires/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Account" ADD COLUMN "refresh_token_expires_in" INTEGER; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20221127033222_email_required/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `email` on table `User` required. This step will fail if there are existing NULL values in that column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" ALTER COLUMN "email" SET NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20230306121228_update/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `zenstack_transaction` on the `Account` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "Account_zenstack_transaction_idx"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Account" DROP COLUMN "zenstack_transaction"; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230905035233_drop_aux_fields/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `zenstack_guard` on the `Account` table. All the data in the column will be lost. 5 | - You are about to drop the column `zenstack_guard` on the `List` table. All the data in the column will be lost. 6 | - You are about to drop the column `zenstack_transaction` on the `List` table. All the data in the column will be lost. 7 | - You are about to drop the column `zenstack_guard` on the `Space` table. All the data in the column will be lost. 8 | - You are about to drop the column `zenstack_transaction` on the `Space` table. All the data in the column will be lost. 9 | - You are about to drop the column `zenstack_guard` on the `SpaceUser` table. All the data in the column will be lost. 10 | - You are about to drop the column `zenstack_transaction` on the `SpaceUser` table. All the data in the column will be lost. 11 | - You are about to drop the column `zenstack_guard` on the `Todo` table. All the data in the column will be lost. 12 | - You are about to drop the column `zenstack_transaction` on the `Todo` table. All the data in the column will be lost. 13 | - You are about to drop the column `zenstack_guard` on the `User` table. All the data in the column will be lost. 14 | - You are about to drop the column `zenstack_transaction` on the `User` table. All the data in the column will be lost. 15 | 16 | */ 17 | -- DropIndex 18 | DROP INDEX "List_zenstack_transaction_idx"; 19 | 20 | -- DropIndex 21 | DROP INDEX "Space_zenstack_transaction_idx"; 22 | 23 | -- DropIndex 24 | DROP INDEX "SpaceUser_zenstack_transaction_idx"; 25 | 26 | -- DropIndex 27 | DROP INDEX "Todo_zenstack_transaction_idx"; 28 | 29 | -- DropIndex 30 | DROP INDEX "User_zenstack_transaction_idx"; 31 | 32 | -- AlterTable 33 | ALTER TABLE "Account" DROP COLUMN "zenstack_guard"; 34 | 35 | -- AlterTable 36 | ALTER TABLE "List" DROP COLUMN "zenstack_guard", 37 | DROP COLUMN "zenstack_transaction"; 38 | 39 | -- AlterTable 40 | ALTER TABLE "Space" DROP COLUMN "zenstack_guard", 41 | DROP COLUMN "zenstack_transaction"; 42 | 43 | -- AlterTable 44 | ALTER TABLE "SpaceUser" DROP COLUMN "zenstack_guard", 45 | DROP COLUMN "zenstack_transaction"; 46 | 47 | -- AlterTable 48 | ALTER TABLE "Todo" DROP COLUMN "zenstack_guard", 49 | DROP COLUMN "zenstack_transaction"; 50 | 51 | -- AlterTable 52 | ALTER TABLE "User" DROP COLUMN "zenstack_guard", 53 | DROP COLUMN "zenstack_transaction"; 54 | -------------------------------------------------------------------------------- /prisma/migrations/20241222114017_add_space_owner/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `ownerId` to the `Space` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Space" ADD COLUMN "ownerId" TEXT NOT NULL; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Space" ADD CONSTRAINT "Space_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////// 2 | // DO NOT MODIFY THIS FILE // 3 | // This file is automatically generated by ZenStack CLI and should not be manually updated. // 4 | ////////////////////////////////////////////////////////////////////////////////////////////// 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | generator js { 12 | provider = "prisma-client-js" 13 | } 14 | 15 | enum SpaceUserRole { 16 | USER 17 | ADMIN 18 | } 19 | 20 | model Space { 21 | id String @id() @default(uuid()) 22 | createdAt DateTime @default(now()) 23 | updatedAt DateTime @updatedAt() 24 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 25 | ownerId String 26 | name String 27 | slug String @unique() 28 | members SpaceUser[] 29 | lists List[] 30 | } 31 | 32 | model SpaceUser { 33 | id String @id() @default(uuid()) 34 | createdAt DateTime @default(now()) 35 | updatedAt DateTime @updatedAt() 36 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 37 | spaceId String 38 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 39 | userId String 40 | role SpaceUserRole 41 | 42 | @@unique([userId, spaceId]) 43 | } 44 | 45 | model User { 46 | id String @id() @default(uuid()) 47 | createdAt DateTime @default(now()) 48 | updatedAt DateTime @updatedAt() 49 | email String @unique() 50 | emailVerified DateTime? 51 | password String? 52 | name String? 53 | ownedSpaces Space[] 54 | memberships SpaceUser[] 55 | image String? 56 | lists List[] 57 | todos Todo[] 58 | accounts Account[] 59 | } 60 | 61 | model List { 62 | id String @id() @default(uuid()) 63 | createdAt DateTime @default(now()) 64 | updatedAt DateTime @updatedAt() 65 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 66 | spaceId String 67 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 68 | ownerId String 69 | title String 70 | private Boolean @default(false) 71 | todos Todo[] 72 | } 73 | 74 | model Todo { 75 | id String @id() @default(uuid()) 76 | createdAt DateTime @default(now()) 77 | updatedAt DateTime @updatedAt() 78 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 79 | ownerId String 80 | list List @relation(fields: [listId], references: [id], onDelete: Cascade) 81 | listId String 82 | title String 83 | completedAt DateTime? 84 | } 85 | 86 | model Account { 87 | id String @id() @default(uuid()) 88 | userId String 89 | type String 90 | provider String 91 | providerAccountId String 92 | refresh_token String? 93 | refresh_token_expires_in Int? 94 | access_token String? 95 | expires_at Int? 96 | token_type String? 97 | scope String? 98 | id_token String? 99 | session_state String? 100 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 101 | 102 | @@unique([provider, providerAccountId]) 103 | } 104 | -------------------------------------------------------------------------------- /public/auth-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs/89221497884b3597649d70ac61c43721b15e7c3b/public/auth-bg.jpg -------------------------------------------------------------------------------- /public/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs/89221497884b3597649d70ac61c43721b15e7c3b/public/avatar.jpg -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs/89221497884b3597649d70ac61c43721b15e7c3b/public/logo.png -------------------------------------------------------------------------------- /schema.zmodel: -------------------------------------------------------------------------------- 1 | /* 2 | * Sample model for a collaborative Todo app 3 | */ 4 | 5 | datasource db { 6 | provider = 'postgresql' 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | generator js { 11 | provider = 'prisma-client-js' 12 | } 13 | 14 | plugin enhancer { 15 | provider = '@core/enhancer' 16 | generatePermissionChecker = true 17 | } 18 | 19 | plugin hooks { 20 | provider = '@zenstackhq/swr' 21 | output = 'lib/hooks' 22 | } 23 | 24 | /** 25 | * Enum for user's role in a space 26 | */ 27 | enum SpaceUserRole { 28 | USER 29 | ADMIN 30 | } 31 | 32 | /** 33 | * Model for a space in which users can collaborate on Lists and Todos 34 | */ 35 | model Space { 36 | id String @id @default(uuid()) 37 | createdAt DateTime @default(now()) 38 | updatedAt DateTime @updatedAt 39 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 40 | ownerId String @default(auth().id) 41 | name String @length(4, 50) 42 | slug String @unique @regex('^[0-9a-zA-Z]{4,16}$') 43 | members SpaceUser[] 44 | lists List[] 45 | 46 | // require login 47 | @@deny('all', auth() == null) 48 | 49 | // everyone can create a space 50 | @@allow('create', true) 51 | 52 | // any user in the space can read the space 53 | @@allow('read', members?[user == auth()]) 54 | 55 | // space admin can update and delete 56 | @@allow('update,delete', members?[user == auth() && role == ADMIN]) 57 | } 58 | 59 | /** 60 | * Model representing membership of a user in a space 61 | */ 62 | model SpaceUser { 63 | id String @id @default(uuid()) 64 | createdAt DateTime @default(now()) 65 | updatedAt DateTime @updatedAt 66 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 67 | spaceId String 68 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 69 | userId String 70 | role SpaceUserRole 71 | @@unique([userId, spaceId]) 72 | 73 | // require login 74 | @@deny('all', auth() == null) 75 | 76 | // space owner can add any one 77 | @@allow('create', space.owner == auth()) 78 | 79 | // space admin can add anyone but not himself 80 | @@allow('create', auth() != this.user && space.members?[user == auth() && role == ADMIN]) 81 | 82 | // space admin can update/delete 83 | @@allow('update,delete', space.members?[user == auth() && role == ADMIN]) 84 | 85 | // user can read entries for spaces which he's a member of 86 | @@allow('read', space.members?[user == auth()]) 87 | } 88 | 89 | /** 90 | * Model for a user 91 | */ 92 | model User { 93 | id String @id @default(uuid()) 94 | createdAt DateTime @default(now()) 95 | updatedAt DateTime @updatedAt 96 | email String @unique @email 97 | emailVerified DateTime? 98 | password String? @password @omit 99 | name String? 100 | ownedSpaces Space[] 101 | memberships SpaceUser[] 102 | image String? @url 103 | lists List[] 104 | todos Todo[] 105 | 106 | // next-auth 107 | accounts Account[] 108 | 109 | // can be created by anyone, even not logged in 110 | @@allow('create', true) 111 | 112 | // can be read by users sharing any space 113 | @@allow('read', memberships?[space.members?[user == auth()]]) 114 | 115 | // full access by oneself 116 | @@allow('all', auth() == this) 117 | } 118 | 119 | abstract model BaseEntity { 120 | id String @id @default(uuid()) 121 | createdAt DateTime @default(now()) 122 | updatedAt DateTime @updatedAt 123 | 124 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 125 | spaceId String 126 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 127 | ownerId String @default(auth().id) 128 | 129 | // can be read by owner or space members 130 | @@allow('read', owner == auth() || (space.members?[user == auth()])) 131 | 132 | // when create, owner must be set to current user, and user must be in the space 133 | @@allow('create', owner == auth() && space.members?[user == auth()]) 134 | 135 | // when create, owner must be set to current user, and user must be in the space 136 | // update is not allowed to change owner 137 | @@allow('update', owner == auth() && space.members?[user == auth()] && future().owner == owner) 138 | 139 | // can be deleted by owner 140 | @@allow('delete', owner == auth()) 141 | } 142 | 143 | /** 144 | * Model for a Todo list 145 | */ 146 | model List extends BaseEntity { 147 | title String @length(1, 100) 148 | private Boolean @default(false) 149 | todos Todo[] 150 | 151 | // can't be read by others if it's private 152 | @@deny('read', private == true && owner != auth()) 153 | } 154 | 155 | /** 156 | * Model for a single Todo 157 | */ 158 | model Todo { 159 | id String @id @default(uuid()) 160 | createdAt DateTime @default(now()) 161 | updatedAt DateTime @updatedAt 162 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 163 | ownerId String @default(auth().id) 164 | list List @relation(fields: [listId], references: [id], onDelete: Cascade) 165 | listId String 166 | title String @length(1, 100) 167 | completedAt DateTime? 168 | 169 | // full access if the parent list is readable 170 | @@allow('all', check(list, 'read')) 171 | } 172 | 173 | /** 174 | * Next-auth user account 175 | */ 176 | model Account { 177 | id String @id @default(uuid()) 178 | userId String 179 | type String 180 | provider String 181 | providerAccountId String 182 | refresh_token String? 183 | refresh_token_expires_in Int? 184 | access_token String? 185 | expires_at Int? 186 | token_type String? 187 | scope String? 188 | id_token String? 189 | session_state String? 190 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 191 | @@unique([provider, providerAccountId]) 192 | } -------------------------------------------------------------------------------- /server/auth.ts: -------------------------------------------------------------------------------- 1 | import { type GetServerSidePropsContext } from 'next'; 2 | import { getServerSession } from 'next-auth'; 3 | import { authOptions } from 'pages/api/auth/[...nextauth]'; 4 | 5 | export const getServerAuthSession = async (ctx: { 6 | req: GetServerSidePropsContext['req']; 7 | res: GetServerSidePropsContext['res']; 8 | }) => { 9 | return getServerSession(ctx.req, ctx.res, authOptions); 10 | }; 11 | -------------------------------------------------------------------------------- /server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-var 5 | var prisma: PrismaClient | undefined; 6 | } 7 | 8 | export const prisma = global.prisma || new PrismaClient(); 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | global.prisma = prisma; 12 | } 13 | -------------------------------------------------------------------------------- /server/enhanced-db.ts: -------------------------------------------------------------------------------- 1 | import { enhance } from '@zenstackhq/runtime'; 2 | import type { GetServerSidePropsContext } from 'next'; 3 | import { getServerAuthSession } from './auth'; 4 | import { prisma } from './db'; 5 | 6 | export async function getEnhancedPrisma(ctx: { 7 | req: GetServerSidePropsContext['req']; 8 | res: GetServerSidePropsContext['res']; 9 | }) { 10 | const session = await getServerAuthSession(ctx); 11 | return enhance(prisma, { user: session?.user }); 12 | } 13 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply text-gray-800; 7 | } 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require('daisyui')], 8 | daisyui: { 9 | themes: ['light'], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@lib/*": ["lib/*"], 20 | "@components/*": ["lib/components/*"], 21 | "@components": ["lib/components/index"] 22 | }, 23 | "plugins": [ 24 | { 25 | "name": "next" 26 | } 27 | ] 28 | }, 29 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 30 | "exclude": ["node_modules"] 31 | } 32 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { Session } from 'next-auth'; 2 | import { JWT } from 'next-auth/jwt'; 3 | 4 | /** Example on how to extend the built-in session types */ 5 | declare module 'next-auth' { 6 | interface Session { 7 | user: { id: string; name: string; email: string; image?: string }; 8 | } 9 | } 10 | 11 | /** Example on how to extend the built-in types for JWT */ 12 | declare module 'next-auth/jwt' { 13 | interface JWT {} 14 | } 15 | -------------------------------------------------------------------------------- /types/next.d.ts: -------------------------------------------------------------------------------- 1 | import type { NextComponentType, NextPageContext } from 'next'; 2 | import type { Session } from 'next-auth'; 3 | import type { Router } from 'next/router'; 4 | 5 | declare module 'next/app' { 6 | type AppProps

> = { 7 | Component: NextComponentType; 8 | router: Router; 9 | __N_SSG?: boolean; 10 | __N_SSP?: boolean; 11 | pageProps: P & { 12 | /** Initial session passed in from `getServerSideProps` or `getInitialProps` */ 13 | session?: Session; 14 | }; 15 | }; 16 | } 17 | --------------------------------------------------------------------------------