├── .env.sample ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── update.yml ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── app ├── api │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ └── model │ │ └── [...path] │ │ └── route.ts ├── create-space │ └── page.tsx ├── layout.tsx ├── page.tsx ├── providers.tsx ├── signin │ └── page.tsx ├── signup │ └── page.tsx └── space │ └── [slug] │ ├── [listId] │ └── page.tsx │ └── page.tsx ├── assets └── styles │ └── globals.css ├── components ├── 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 ├── middleware.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── 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 │ ├── 20230905040400_drop_aux_fields │ │ └── migration.sql │ ├── 20241222133651_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 ├── tailwind.config.js ├── tsconfig.json └── types ├── next-auth.d.ts └── next.d.ts /.env.sample: -------------------------------------------------------------------------------- 1 | POSTGRES_URL= 2 | POSTGRES_URL_NON_POOLING= 3 | AUTH_GITHUB_ID= 4 | AUTH_GITHUB_SECRET= 5 | AUTH_SECRET= 6 | -------------------------------------------------------------------------------- /.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 | name: CI 2 | 3 | env: 4 | DO_NOT_TRACK: '1' 5 | 6 | on: 7 | push: 8 | branches: ['main'] 9 | pull_request: 10 | branches: ['main'] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js 20.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20.x 22 | cache: 'npm' 23 | - run: npm ci 24 | - run: npm run build 25 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: Update ZenStack 2 | 3 | env: 4 | DO_NOT_TRACK: '1' 5 | 6 | on: 7 | workflow_dispatch: 8 | repository_dispatch: 9 | types: [zenstack-release] 10 | 11 | jobs: 12 | update: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Update to latest ZenStack 18 | run: | 19 | git config --global user.name ymc9 20 | git config --global user.email yiming@whimslab.io 21 | npm ci 22 | npm run up 23 | 24 | - name: Build 25 | run: | 26 | npm run build 27 | 28 | - name: Commit and push 29 | run: | 30 | git add . 31 | git commit -m "chore: update to latest ZenStack" || true 32 | git push || true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | node_modules/ 4 | .env.local 5 | .next 6 | .zenstack_repl* 7 | .env 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 | # A Collaborative Todo Sample - ZenStack + Next.js 2 | 3 | This project is a collaborative todo app built with [Next.js](https://nextjs.org) (app router), [Auth.js](https://authjs.dev/), and [ZenStack](https://zenstack.dev). 4 | 5 | > See the [pages-route](https://github.com/zenstackhq/sample-todo-nextjs-tanstack/tree/pages-route) branch for an implementation using Next.js's old pages router. 6 | 7 | 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. 8 | 9 | See a live deployment at: https://zenstack-todo.vercel.app/. 10 | 11 | ## Features: 12 | 13 | - User signup/signin 14 | - Creating workspaces and inviting members 15 | - Data segregation and permission control 16 | 17 | ## Running the sample: 18 | 19 | 1. Setup a new PostgreSQL database 20 | 21 | 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. 22 | 23 | 1. Install dependencies 24 | 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | 1. Configure environment variables 30 | 31 | Copy the `.env.example` file to `.env` and set the values for your environment. Github related variables can be left empty if you don't need GitHub OAuth login. 32 | 33 | 1. Generate server and client-side code from model 34 | 35 | ```bash 36 | npm run generate 37 | ``` 38 | 39 | 1. Synchronize database schema 40 | 41 | ```bash 42 | npm run db:push 43 | ``` 44 | 45 | 1. Start dev server 46 | 47 | ```bash 48 | npm run dev 49 | ``` 50 | 51 | For more information on using ZenStack, visit [https://zenstack.dev](https://zenstack.dev). 52 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from 'server/auth'; 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /app/api/model/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { enhance } from '@zenstackhq/runtime'; 2 | import { NextRequestHandler } from '@zenstackhq/server/next'; 3 | import { auth } from 'server/auth'; 4 | import { prisma } from 'server/db'; 5 | 6 | // create an enhanced Prisma client with user context 7 | async function getPrisma() { 8 | const authObj = await auth(); 9 | if (authObj?.user) { 10 | const user = await prisma.user.findUniqueOrThrow({ 11 | where: { id: authObj.user.id }, 12 | include: { memberships: true }, 13 | }); 14 | return enhance(prisma, { user }); 15 | } else { 16 | // anonymous user 17 | return enhance(prisma); 18 | } 19 | } 20 | 21 | const handler = NextRequestHandler({ getPrisma, useAppDir: true }); 22 | 23 | export { handler as DELETE, handler as GET, handler as PATCH, handler as POST, handler as PUT }; 24 | -------------------------------------------------------------------------------- /app/create-space/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | /* eslint-disable @typescript-eslint/no-explicit-any */ 5 | import { SpaceUserRole } from '@prisma/client'; 6 | import WithNavBar from 'components/WithNavBar'; 7 | import { useCreateSpace } from 'lib/hooks'; 8 | import { NextPage } from 'next'; 9 | import { useSession } from 'next-auth/react'; 10 | import { useRouter } from 'next/navigation'; 11 | import { FormEvent, useState } from 'react'; 12 | import { toast } from 'react-toastify'; 13 | 14 | const CreateSpace: NextPage = () => { 15 | const { data: session } = useSession(); 16 | const [name, setName] = useState(''); 17 | const [slug, setSlug] = useState(''); 18 | 19 | const { mutateAsync: createAsync } = useCreateSpace(); 20 | const router = useRouter(); 21 | 22 | const onSubmit = async (event: FormEvent) => { 23 | event.preventDefault(); 24 | try { 25 | const space = await createAsync({ 26 | data: { 27 | name, 28 | slug, 29 | members: { 30 | create: [ 31 | { 32 | userId: session!.user.id, 33 | role: SpaceUserRole.ADMIN, 34 | }, 35 | ], 36 | }, 37 | }, 38 | }); 39 | console.log('Space created:', space); 40 | toast.success("Space created successfully! You'll be redirected."); 41 | 42 | setTimeout(() => { 43 | if (space) { 44 | void router.push(`/space/${space.slug}`); 45 | } 46 | }, 2000); 47 | } catch (err: any) { 48 | console.error(err); 49 | if (err.info?.prisma === true) { 50 | if (err.info.code === 'P2002') { 51 | toast.error('Space slug already in use'); 52 | } else { 53 | toast.error(`Unexpected Prisma error: ${err.info.code}`); 54 | } 55 | } else { 56 | toast.error(JSON.stringify(err)); 57 | } 58 | } 59 | }; 60 | 61 | return ( 62 | 63 |
64 |
void onSubmit(e)}> 65 |

Create a space

66 |
67 |
68 | 71 | ) => setName(e.currentTarget.value)} 79 | /> 80 |
81 |
82 | 85 | ) => setSlug(e.currentTarget.value)} 92 | /> 93 |
94 |
95 | 96 |
97 | 20 || !slug.match(/^[0-9a-zA-Z]{4,16}$/)} 100 | value="Create" 101 | className="btn btn-primary px-8" 102 | /> 103 | 106 |
107 |
108 |
109 |
110 | ); 111 | }; 112 | 113 | export default CreateSpace; 114 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'assets/styles/globals.css'; 4 | import { ToastContainer } from 'react-toastify'; 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | import Providers from './providers'; 7 | 8 | export default function RootLayout({ children }: { children: React.ReactNode }) { 9 | return ( 10 | 11 | 12 | 13 |
{children}
14 |
15 | {' '} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Spaces from 'components/Spaces'; 4 | import WithNavBar from 'components/WithNavBar'; 5 | import { useCurrentUser } from 'lib/context'; 6 | import { useFindManySpace } from 'lib/hooks'; 7 | import Link from 'next/link'; 8 | 9 | const Home = () => { 10 | const user = useCurrentUser(); 11 | const { data: spaces } = useFindManySpace(); 12 | 13 | if (!spaces || !user) { 14 | return
Loading...
; 15 | } 16 | 17 | return ( 18 | 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 | export default Home; 37 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { SpaceContext, useCurrentSpace, useCurrentUser, UserContext } from 'lib/context'; 5 | import { SessionProvider } from 'next-auth/react'; 6 | import type { ReactNode } from 'react'; 7 | 8 | const queryClient = new QueryClient(); 9 | 10 | function UserSpaceProvider({ children }: { children: ReactNode }) { 11 | const user = useCurrentUser(); 12 | const space = useCurrentSpace(); 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | 20 | export default function Providers({ children }: { children: ReactNode }) { 21 | return ( 22 | 23 | 24 | {children} 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/signin/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { signIn } from 'next-auth/react'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | import { useSearchParams } from 'next/navigation'; 7 | import { FormEvent, Suspense, useEffect, useState } from 'react'; 8 | import { toast } from 'react-toastify'; 9 | 10 | function OAuthError() { 11 | const params = useSearchParams(); 12 | 13 | useEffect(() => { 14 | const error = params.get('error'); 15 | if (error) { 16 | if (error === 'OAuthAccountNotLinked') { 17 | toast.error('Unable to signin. The user email may be already in use.'); 18 | } else { 19 | toast.error(`Authentication error: ${error.toString()}`); 20 | } 21 | } 22 | }, [params]); 23 | return null; 24 | } 25 | 26 | export default function Signin() { 27 | const [email, setEmail] = useState(''); 28 | const [password, setPassword] = useState(''); 29 | 30 | async function onSignin(e: FormEvent) { 31 | e.preventDefault(); 32 | const signInResult = await signIn('credentials', { 33 | redirect: false, 34 | email, 35 | password, 36 | }); 37 | if (!signInResult?.error) { 38 | window.location.href = '/'; 39 | } else { 40 | toast.error(`Signin failed. Please check your email and password.`); 41 | } 42 | } 43 | 44 | return ( 45 |
46 | 47 |
48 | logo 49 |

Welcome to Todo

50 |
51 | 52 |
53 |
54 |

Sign in to your account

55 | 56 |
void onSignin(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 | 94 |
95 |
96 | 99 |
100 |
101 | 102 |
103 | 106 | 107 |
void signIn('github', { callbackUrl: '/' })} 110 | > 111 | Sign in with GitHub 112 |
113 |
114 | 115 |
116 | Not registered?{' '} 117 | 118 | Create account 119 | 120 |
121 |
122 |
123 |
124 | 125 | {/* Suspense is needed because 🤔: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout */} 126 | 127 | 128 |
129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 'use client'; 4 | 5 | import { useCreateUser } from 'lib/hooks'; 6 | import { signIn } from 'next-auth/react'; 7 | import Image from 'next/image'; 8 | import Link from 'next/link'; 9 | import { FormEvent, useState } from 'react'; 10 | import { toast } from 'react-toastify'; 11 | 12 | export default function Signup() { 13 | const [email, setEmail] = useState(''); 14 | const [password, setPassword] = useState(''); 15 | const signup = useCreateUser(); 16 | 17 | async function onSignup(e: FormEvent) { 18 | e.preventDefault(); 19 | try { 20 | await signup.mutateAsync({ data: { email, password } }); 21 | } catch (err: any) { 22 | console.error(err); 23 | if (err.info?.prisma === true) { 24 | if (err.info.code === 'P2002') { 25 | toast.error('User already exists'); 26 | } else { 27 | toast.error(`Unexpected Prisma error: ${err.info.code}`); 28 | } 29 | } else { 30 | toast.error(`Error occurred: ${JSON.stringify(err)}`); 31 | } 32 | return; 33 | } 34 | 35 | const signInResult = await signIn('credentials', { 36 | redirect: false, 37 | email, 38 | password, 39 | }); 40 | if (signInResult?.ok) { 41 | window.location.href = '/'; 42 | } else { 43 | console.error('Signin failed:', signInResult?.error); 44 | } 45 | } 46 | 47 | return ( 48 |
49 | 50 |
51 | logo 52 |

Welcome to Todo

53 |
54 | 55 |
56 |
57 |

Create a Free Account

58 |
void onSignup(e)}> 59 |
60 | 63 | setEmail(e.target.value)} 68 | 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" 69 | placeholder="Email address" 70 | required 71 | /> 72 |
73 |
74 | 77 | setPassword(e.target.value)} 82 | placeholder="••••••••" 83 | 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" 84 | required 85 | /> 86 |
87 |
88 |
89 | 97 |
98 |
99 | 105 |
106 |
107 | 110 |
111 | Already have an account?{' '} 112 | 113 | Login here 114 | 115 |
116 |
117 |
118 |
119 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /app/space/[slug]/[listId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PlusIcon } from '@heroicons/react/24/outline'; 4 | import BreadCrumb from 'components/BreadCrumb'; 5 | import TodoComponent from 'components/Todo'; 6 | import WithNavBar from 'components/WithNavBar'; 7 | import { useCurrentSpace } from 'lib/context'; 8 | import { useCreateTodo, useFindManyTodo, useFindUniqueList } from 'lib/hooks'; 9 | import { useParams } from 'next/navigation'; 10 | import { ChangeEvent, KeyboardEvent, useState } from 'react'; 11 | 12 | export default function TodoList() { 13 | const [title, setTitle] = useState(''); 14 | const { mutate: createTodo } = useCreateTodo({ optimisticUpdate: true }); 15 | const params = useParams<{ listId: string }>(); 16 | const space = useCurrentSpace(); 17 | 18 | const { data: list } = useFindUniqueList({ where: { id: params.listId } }); 19 | 20 | const { data } = useFindManyTodo({ 21 | where: { listId: params.listId }, 22 | include: { 23 | owner: true, 24 | }, 25 | orderBy: { 26 | createdAt: 'desc' as const, 27 | }, 28 | }); 29 | 30 | const onCreateTodo = () => { 31 | if (!title) { 32 | return; 33 | } 34 | setTitle(''); 35 | createTodo({ 36 | data: { 37 | title, 38 | list: { connect: { id: params.listId } }, 39 | }, 40 | }); 41 | }; 42 | 43 | if (!space || !list) { 44 | return <>; 45 | } 46 | 47 | return ( 48 | 49 |
50 | 51 |
52 |
53 |

{list.title}

54 |
55 | ) => { 61 | if (e.key === 'Enter') { 62 | onCreateTodo(); 63 | } 64 | }} 65 | onChange={(e: ChangeEvent) => { 66 | setTitle(e.currentTarget.value); 67 | }} 68 | /> 69 | 72 |
73 |
    74 | {data?.map((todo) => ( 75 | 76 | ))} 77 |
78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /app/space/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | /* eslint-disable @typescript-eslint/no-explicit-any */ 5 | import BreadCrumb from 'components/BreadCrumb'; 6 | import SpaceMembers from 'components/SpaceMembers'; 7 | import TodoList from 'components/TodoList'; 8 | import WithNavBar from 'components/WithNavBar'; 9 | import { useCurrentSpace } from 'lib/context'; 10 | import { useCreateList, useFindManyList } from 'lib/hooks'; 11 | import { useParams } from 'next/navigation'; 12 | import { ChangeEvent, FormEvent, useRef, useState } from 'react'; 13 | import { toast } from 'react-toastify'; 14 | 15 | function CreateDialog() { 16 | const space = useCurrentSpace(); 17 | 18 | const [modalOpen, setModalOpen] = useState(false); 19 | const [title, setTitle] = useState(''); 20 | const [_private, setPrivate] = useState(false); 21 | 22 | const create = useCreateList(); 23 | 24 | const inputRef = useRef(null); 25 | 26 | const onSubmit = async (event: FormEvent) => { 27 | event.preventDefault(); 28 | 29 | try { 30 | await create.mutateAsync({ 31 | data: { 32 | title, 33 | private: _private, 34 | space: { connect: { id: space!.id } }, 35 | }, 36 | }); 37 | } catch (err: any) { 38 | toast.error(`Failed to create list: ${err.info?.message || err.message}`); 39 | return; 40 | } 41 | 42 | toast.success('List created successfully!'); 43 | 44 | // reset states 45 | setTitle(''); 46 | setPrivate(false); 47 | 48 | // close modal 49 | setModalOpen(false); 50 | }; 51 | 52 | return ( 53 | <> 54 | ) => { 60 | setModalOpen(e.currentTarget.checked); 61 | }} 62 | /> 63 |
64 |
65 |

Create a Todo list

66 |
void onSubmit(e)}> 67 |
68 |
69 | 72 | ) => setTitle(e.currentTarget.value)} 81 | /> 82 |
83 |
84 | 87 | ) => setPrivate(e.currentTarget.checked)} 92 | /> 93 |
94 |
95 |
96 | 97 | 100 |
101 |
102 |
103 |
104 | 105 | ); 106 | } 107 | 108 | export default function SpaceHome() { 109 | const params = useParams<{ slug: string }>(); 110 | const space = useCurrentSpace(); 111 | 112 | const { data: lists } = useFindManyList({ 113 | where: { 114 | space: { slug: params.slug }, 115 | }, 116 | include: { 117 | owner: true, 118 | }, 119 | orderBy: { 120 | updatedAt: 'desc', 121 | }, 122 | }); 123 | 124 | if (!space) { 125 | return
Loading...
; 126 | } 127 | 128 | return ( 129 | 130 |
131 | 132 |
133 |
134 |
135 | 138 | 139 |
140 | 141 |
    142 | {lists?.map((list) => ( 143 |
  • 144 | 145 |
  • 146 | ))} 147 |
148 | 149 | 150 |
151 |
152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /assets/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply text-gray-800; 7 | } 8 | -------------------------------------------------------------------------------- /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 | 'use client'; 2 | 3 | import { List, Space } from '@prisma/client'; 4 | import Link from 'next/link'; 5 | import { usePathname } from 'next/navigation'; 6 | 7 | type Props = { 8 | space: Space; 9 | list?: List; 10 | }; 11 | 12 | export default function BreadCrumb({ space, list }: Props) { 13 | const path = usePathname(); 14 | const parts = path.split('/').filter((p) => p); 15 | const [base] = parts; 16 | if (base !== 'space') { 17 | return <>; 18 | } 19 | 20 | const items: Array<{ text: string; link: string }> = []; 21 | 22 | items.push({ text: 'Home', link: '/' }); 23 | items.push({ text: space.name || '', link: `/space/${space.slug}` }); 24 | 25 | if (list) { 26 | items.push({ 27 | text: list?.title || '', 28 | link: `/space/${space.slug}/${list.id}`, 29 | }); 30 | } 31 | 32 | return ( 33 |
34 |
    35 | {items.map((item, i) => ( 36 |
  • 37 | {item.text} 38 |
  • 39 | ))} 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /components/ManageMembers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | /* eslint-disable @typescript-eslint/no-explicit-any */ 5 | import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; 6 | import { Space, SpaceUserRole } from '@prisma/client'; 7 | import { useCurrentUser } from 'lib/context'; 8 | import { useCreateSpaceUser, useDeleteSpaceUser, useFindManySpaceUser } from 'lib/hooks'; 9 | import { ChangeEvent, KeyboardEvent, useState } from 'react'; 10 | import { toast } from 'react-toastify'; 11 | import Avatar from './Avatar'; 12 | 13 | type Props = { 14 | space: Space; 15 | }; 16 | 17 | export default function ManageMembers({ space }: Props) { 18 | const [email, setEmail] = useState(''); 19 | const [role, setRole] = useState(SpaceUserRole.USER); 20 | const user = useCurrentUser(); 21 | const { mutateAsync: createMember } = useCreateSpaceUser(); 22 | const { mutate: deleteMember } = useDeleteSpaceUser(); 23 | 24 | const { data: members } = useFindManySpaceUser({ 25 | where: { 26 | spaceId: space.id, 27 | }, 28 | include: { 29 | user: true, 30 | }, 31 | orderBy: { 32 | role: 'desc', 33 | }, 34 | }); 35 | 36 | const inviteUser = async () => { 37 | try { 38 | const r = await createMember({ 39 | data: { 40 | user: { 41 | connect: { 42 | email, 43 | }, 44 | }, 45 | space: { 46 | connect: { 47 | id: space.id, 48 | }, 49 | }, 50 | role, 51 | }, 52 | }); 53 | console.log('SpaceUser created:', r); 54 | } catch (err: any) { 55 | console.error(err); 56 | if (err.info?.prisma === true) { 57 | if (err.info.code === 'P2002') { 58 | toast.error('User is already a member of the space'); 59 | } else if (err.info.code === 'P2025') { 60 | toast.error('User is not found for this email'); 61 | } else { 62 | toast.error(`Unexpected Prisma error: ${err.info.code}`); 63 | } 64 | } else { 65 | toast.error(`Error occurred: ${JSON.stringify(err)}`); 66 | } 67 | } 68 | }; 69 | 70 | const removeMember = (id: string) => { 71 | if (confirm(`Are you sure to remove this member from space?`)) { 72 | void deleteMember({ where: { id } }); 73 | } 74 | }; 75 | 76 | return ( 77 |
78 |
79 | ) => { 85 | setEmail(e.currentTarget.value); 86 | }} 87 | onKeyUp={(e: KeyboardEvent) => { 88 | if (e.key === 'Enter') { 89 | void inviteUser(); 90 | } 91 | }} 92 | /> 93 | 94 | 104 | 105 | 108 |
109 | 110 |
    111 | {members?.map((member) => ( 112 |
  • 113 |
    114 |
    115 | 116 |
    117 |

    {member.user.name || member.user.email}

    118 |

    {member.role}

    119 |
    120 |
    121 | {user?.id !== member.user.id && ( 122 | { 125 | removeMember(member.id); 126 | }} 127 | /> 128 | )} 129 |
    130 |
  • 131 | ))} 132 |
133 |
134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /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 | 'use client'; 2 | 3 | import { PlusIcon } from '@heroicons/react/24/outline'; 4 | import { Space } from '@prisma/client'; 5 | import { useCurrentSpace } from 'lib/context'; 6 | import { useFindManySpaceUser } from 'lib/hooks'; 7 | import Avatar from './Avatar'; 8 | import ManageMembers from './ManageMembers'; 9 | 10 | function ManagementDialog(space?: Space) { 11 | if (!space) return undefined; 12 | return ( 13 | <> 14 | 17 | 18 | 19 |
20 |
21 |

Manage Members of {space.name}

22 | 23 |
24 | 25 |
26 | 27 |
28 | 31 |
32 |
33 |
34 | 35 | ); 36 | } 37 | 38 | export default function SpaceMembers() { 39 | const space = useCurrentSpace(); 40 | 41 | const { data: members } = useFindManySpaceUser( 42 | { 43 | where: { 44 | spaceId: space?.id, 45 | }, 46 | include: { 47 | user: true, 48 | }, 49 | orderBy: { 50 | role: 'desc', 51 | }, 52 | }, 53 | { enabled: !!space } 54 | ); 55 | 56 | return ( 57 |
58 | {ManagementDialog(space)} 59 | {members && ( 60 | 65 | )} 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /components/Spaces.tsx: -------------------------------------------------------------------------------- 1 | import { Space } from '@prisma/client'; 2 | import { useCountList } from 'lib/hooks'; 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 |
    28 | {spaces?.map((space) => ( 29 |
  • 33 | 34 |
  • 35 | ))} 36 |
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 | 'use client'; 2 | 3 | import { TrashIcon } from '@heroicons/react/24/outline'; 4 | import { Todo, User } from '@prisma/client'; 5 | import { useDeleteTodo, useUpdateTodo } from 'lib/hooks'; 6 | import { ChangeEvent } from 'react'; 7 | import Avatar from './Avatar'; 8 | import TimeInfo from './TimeInfo'; 9 | 10 | type Props = { 11 | value: Todo & { owner: User }; 12 | optimistic?: boolean; 13 | }; 14 | 15 | export default function TodoComponent({ value, optimistic }: Props) { 16 | const { mutate: updateTodo } = useUpdateTodo({ optimisticUpdate: true }); 17 | const { mutate: deleteTodo } = useDeleteTodo({ optimisticUpdate: true }); 18 | 19 | const onDelete = () => { 20 | deleteTodo({ where: { id: value.id } }); 21 | }; 22 | 23 | const onToggleCompleted = (completed: boolean) => { 24 | if (completed === !!value.completedAt) { 25 | return; 26 | } 27 | updateTodo({ 28 | where: { id: value.id }, 29 | data: { completedAt: completed ? new Date() : null }, 30 | }); 31 | }; 32 | 33 | return ( 34 |
35 |
36 |

41 | {value.title} 42 | {optimistic && } 43 |

44 |
45 | ) => onToggleCompleted(e.currentTarget.checked)} 50 | /> 51 | 52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LockClosedIcon, TrashIcon } from '@heroicons/react/24/outline'; 4 | import { List } from '@prisma/client'; 5 | import { useCheckList, useDeleteList } from 'lib/hooks'; 6 | import { customAlphabet } from 'nanoid'; 7 | import { User } from 'next-auth'; 8 | import Image from 'next/image'; 9 | import Link from 'next/link'; 10 | import { usePathname } from 'next/navigation'; 11 | import Avatar from './Avatar'; 12 | import TimeInfo from './TimeInfo'; 13 | 14 | type Props = { 15 | value: List & { owner: User }; 16 | }; 17 | 18 | export default function TodoList({ value }: Props) { 19 | const path = usePathname(); 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 { mutate: deleteList } = useDeleteList(); 24 | 25 | const onDelete = () => { 26 | if (confirm('Are you sure to delete this list?')) { 27 | 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 | {canDelete && } 60 |
61 |
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /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 | 'use client'; 2 | 3 | import { Space } from '@prisma/client'; 4 | import { User } from 'next-auth'; 5 | import { useSession } from 'next-auth/react'; 6 | import { useParams } from 'next/navigation'; 7 | import { createContext } from 'react'; 8 | import { useFindManySpace } from './hooks'; 9 | 10 | export const UserContext = createContext(undefined); 11 | 12 | export function useCurrentUser() { 13 | const { data: session } = useSession(); 14 | return session?.user; 15 | } 16 | 17 | export const SpaceContext = createContext(undefined); 18 | 19 | export function useCurrentSpace() { 20 | const params = useParams<{ slug: string }>(); 21 | const { data: spaces } = useFindManySpace( 22 | { 23 | where: { slug: params.slug }, 24 | }, 25 | { 26 | enabled: !!params.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, Account } from "@zenstackhq/runtime/models"; 9 | import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query'; 10 | import { getHooksContext } from '@zenstackhq/tanstack-query/runtime-v5/react'; 11 | import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '@zenstackhq/tanstack-query/runtime-v5/react'; 12 | import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '@zenstackhq/tanstack-query/runtime-v5'; 13 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 14 | import metadata from './__model_meta'; 15 | type DefaultError = QueryError; 16 | import { useSuspenseModelQuery, useSuspenseInfiniteModelQuery } from '@zenstackhq/tanstack-query/runtime-v5/react'; 17 | import type { UseSuspenseQueryOptions, UseSuspenseInfiniteQueryOptions } from '@tanstack/react-query'; 18 | 19 | export function useCreateAccount(options?: Omit<(UseMutationOptions<(Account | undefined), DefaultError, Prisma.AccountCreateArgs> & ExtraMutationOptions), 'mutationFn'>) { 20 | const { endpoint, fetch } = getHooksContext(); 21 | const _mutation = 22 | useModelMutation('Account', 'POST', `${endpoint}/account/create`, metadata, options, fetch, true) 23 | ; 24 | const mutation = { 25 | ..._mutation, 26 | mutateAsync: async ( 27 | args: Prisma.SelectSubset, 28 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 29 | ) => { 30 | return (await _mutation.mutateAsync( 31 | args, 32 | options as any 33 | )) as (CheckSelect> | undefined); 34 | }, 35 | }; 36 | return mutation; 37 | } 38 | 39 | export function useCreateManyAccount(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 40 | const { endpoint, fetch } = getHooksContext(); 41 | const _mutation = 42 | useModelMutation('Account', 'POST', `${endpoint}/account/createMany`, metadata, options, fetch, false) 43 | ; 44 | const mutation = { 45 | ..._mutation, 46 | mutateAsync: async ( 47 | args: Prisma.SelectSubset, 48 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 49 | ) => { 50 | return (await _mutation.mutateAsync( 51 | args, 52 | options as any 53 | )) as Prisma.BatchPayload; 54 | }, 55 | }; 56 | return mutation; 57 | } 58 | 59 | export function useFindManyAccount & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 60 | const { endpoint, fetch } = getHooksContext(); 61 | return useModelQuery('Account', `${endpoint}/account/findMany`, args, options, fetch); 62 | } 63 | 64 | export function useInfiniteFindManyAccount>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 65 | options = options ?? { getNextPageParam: () => null }; 66 | const { endpoint, fetch } = getHooksContext(); 67 | return useInfiniteModelQuery('Account', `${endpoint}/account/findMany`, args, options, fetch); 68 | } 69 | 70 | export function useSuspenseFindManyAccount & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 71 | const { endpoint, fetch } = getHooksContext(); 72 | return useSuspenseModelQuery('Account', `${endpoint}/account/findMany`, args, options, fetch); 73 | } 74 | 75 | export function useSuspenseInfiniteFindManyAccount>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 76 | options = options ?? { getNextPageParam: () => null }; 77 | const { endpoint, fetch } = getHooksContext(); 78 | return useSuspenseInfiniteModelQuery('Account', `${endpoint}/account/findMany`, args, options, fetch); 79 | } 80 | 81 | export function useFindUniqueAccount & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 82 | const { endpoint, fetch } = getHooksContext(); 83 | return useModelQuery('Account', `${endpoint}/account/findUnique`, args, options, fetch); 84 | } 85 | 86 | export function useSuspenseFindUniqueAccount & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 87 | const { endpoint, fetch } = getHooksContext(); 88 | return useSuspenseModelQuery('Account', `${endpoint}/account/findUnique`, args, options, fetch); 89 | } 90 | 91 | export function useFindFirstAccount & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 92 | const { endpoint, fetch } = getHooksContext(); 93 | return useModelQuery('Account', `${endpoint}/account/findFirst`, args, options, fetch); 94 | } 95 | 96 | export function useSuspenseFindFirstAccount & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 97 | const { endpoint, fetch } = getHooksContext(); 98 | return useSuspenseModelQuery('Account', `${endpoint}/account/findFirst`, args, options, fetch); 99 | } 100 | 101 | export function useUpdateAccount(options?: Omit<(UseMutationOptions<(Account | undefined), DefaultError, Prisma.AccountUpdateArgs> & ExtraMutationOptions), 'mutationFn'>) { 102 | const { endpoint, fetch } = getHooksContext(); 103 | const _mutation = 104 | useModelMutation('Account', 'PUT', `${endpoint}/account/update`, metadata, options, fetch, true) 105 | ; 106 | const mutation = { 107 | ..._mutation, 108 | mutateAsync: async ( 109 | args: Prisma.SelectSubset, 110 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 111 | ) => { 112 | return (await _mutation.mutateAsync( 113 | args, 114 | options as any 115 | )) as (CheckSelect> | undefined); 116 | }, 117 | }; 118 | return mutation; 119 | } 120 | 121 | export function useUpdateManyAccount(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 122 | const { endpoint, fetch } = getHooksContext(); 123 | const _mutation = 124 | useModelMutation('Account', 'PUT', `${endpoint}/account/updateMany`, metadata, options, fetch, false) 125 | ; 126 | const mutation = { 127 | ..._mutation, 128 | mutateAsync: async ( 129 | args: Prisma.SelectSubset, 130 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 131 | ) => { 132 | return (await _mutation.mutateAsync( 133 | args, 134 | options as any 135 | )) as Prisma.BatchPayload; 136 | }, 137 | }; 138 | return mutation; 139 | } 140 | 141 | export function useUpsertAccount(options?: Omit<(UseMutationOptions<(Account | undefined), DefaultError, Prisma.AccountUpsertArgs> & ExtraMutationOptions), 'mutationFn'>) { 142 | const { endpoint, fetch } = getHooksContext(); 143 | const _mutation = 144 | useModelMutation('Account', 'POST', `${endpoint}/account/upsert`, metadata, options, fetch, true) 145 | ; 146 | const mutation = { 147 | ..._mutation, 148 | mutateAsync: async ( 149 | args: Prisma.SelectSubset, 150 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 151 | ) => { 152 | return (await _mutation.mutateAsync( 153 | args, 154 | options as any 155 | )) as (CheckSelect> | undefined); 156 | }, 157 | }; 158 | return mutation; 159 | } 160 | 161 | export function useDeleteAccount(options?: Omit<(UseMutationOptions<(Account | undefined), DefaultError, Prisma.AccountDeleteArgs> & ExtraMutationOptions), 'mutationFn'>) { 162 | const { endpoint, fetch } = getHooksContext(); 163 | const _mutation = 164 | useModelMutation('Account', 'DELETE', `${endpoint}/account/delete`, metadata, options, fetch, true) 165 | ; 166 | const mutation = { 167 | ..._mutation, 168 | mutateAsync: async ( 169 | args: Prisma.SelectSubset, 170 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 171 | ) => { 172 | return (await _mutation.mutateAsync( 173 | args, 174 | options as any 175 | )) as (CheckSelect> | undefined); 176 | }, 177 | }; 178 | return mutation; 179 | } 180 | 181 | export function useDeleteManyAccount(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 182 | const { endpoint, fetch } = getHooksContext(); 183 | const _mutation = 184 | useModelMutation('Account', 'DELETE', `${endpoint}/account/deleteMany`, metadata, options, fetch, false) 185 | ; 186 | const mutation = { 187 | ..._mutation, 188 | mutateAsync: async ( 189 | args: Prisma.SelectSubset, 190 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 191 | ) => { 192 | return (await _mutation.mutateAsync( 193 | args, 194 | options as any 195 | )) as Prisma.BatchPayload; 196 | }, 197 | }; 198 | return mutation; 199 | } 200 | 201 | export function useAggregateAccount, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 202 | const { endpoint, fetch } = getHooksContext(); 203 | return useModelQuery('Account', `${endpoint}/account/aggregate`, args, options, fetch); 204 | } 205 | 206 | export function useSuspenseAggregateAccount, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 207 | const { endpoint, fetch } = getHooksContext(); 208 | return useSuspenseModelQuery('Account', `${endpoint}/account/aggregate`, args, options, fetch); 209 | } 210 | 211 | 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 212 | ? `Error: "by" must not be empty.` 213 | : HavingValid extends Prisma.False 214 | ? { 215 | [P in HavingFields]: P extends ByFields 216 | ? never 217 | : P extends string 218 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 219 | : [ 220 | Error, 221 | 'Field ', 222 | P, 223 | ` in "having" needs to be provided in "by"`, 224 | ] 225 | }[HavingFields] 226 | : 'take' extends Prisma.Keys 227 | ? 'orderBy' extends Prisma.Keys 228 | ? ByValid extends Prisma.True 229 | ? {} 230 | : { 231 | [P in OrderFields]: P extends ByFields 232 | ? never 233 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 234 | }[OrderFields] 235 | : 'Error: If you provide "take", you also need to provide "orderBy"' 236 | : 'skip' extends Prisma.Keys 237 | ? 'orderBy' extends Prisma.Keys 238 | ? ByValid extends Prisma.True 239 | ? {} 240 | : { 241 | [P in OrderFields]: P extends ByFields 242 | ? never 243 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 244 | }[OrderFields] 245 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 246 | : ByValid extends Prisma.True 247 | ? {} 248 | : { 249 | [P in OrderFields]: P extends ByFields 250 | ? never 251 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 252 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 253 | Array & 254 | { 255 | [P in ((keyof TArgs) & (keyof Prisma.AccountGroupByOutputType))]: P extends '_count' 256 | ? TArgs[P] extends boolean 257 | ? number 258 | : Prisma.GetScalarType 259 | : Prisma.GetScalarType 260 | } 261 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 262 | const { endpoint, fetch } = getHooksContext(); 263 | return useModelQuery('Account', `${endpoint}/account/groupBy`, args, options, fetch); 264 | } 265 | 266 | export function useSuspenseGroupByAccount>, 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 267 | ? `Error: "by" must not be empty.` 268 | : HavingValid extends Prisma.False 269 | ? { 270 | [P in HavingFields]: P extends ByFields 271 | ? never 272 | : P extends string 273 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 274 | : [ 275 | Error, 276 | 'Field ', 277 | P, 278 | ` in "having" needs to be provided in "by"`, 279 | ] 280 | }[HavingFields] 281 | : 'take' extends Prisma.Keys 282 | ? 'orderBy' extends Prisma.Keys 283 | ? ByValid extends Prisma.True 284 | ? {} 285 | : { 286 | [P in OrderFields]: P extends ByFields 287 | ? never 288 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 289 | }[OrderFields] 290 | : 'Error: If you provide "take", you also need to provide "orderBy"' 291 | : 'skip' extends Prisma.Keys 292 | ? 'orderBy' extends Prisma.Keys 293 | ? ByValid extends Prisma.True 294 | ? {} 295 | : { 296 | [P in OrderFields]: P extends ByFields 297 | ? never 298 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 299 | }[OrderFields] 300 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 301 | : ByValid extends Prisma.True 302 | ? {} 303 | : { 304 | [P in OrderFields]: P extends ByFields 305 | ? never 306 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 307 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 308 | Array & 309 | { 310 | [P in ((keyof TArgs) & (keyof Prisma.AccountGroupByOutputType))]: P extends '_count' 311 | ? TArgs[P] extends boolean 312 | ? number 313 | : Prisma.GetScalarType 314 | : Prisma.GetScalarType 315 | } 316 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 317 | const { endpoint, fetch } = getHooksContext(); 318 | return useSuspenseModelQuery('Account', `${endpoint}/account/groupBy`, args, options, fetch); 319 | } 320 | 321 | export function useCountAccount : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 322 | const { endpoint, fetch } = getHooksContext(); 323 | return useModelQuery('Account', `${endpoint}/account/count`, args, options, fetch); 324 | } 325 | 326 | export function useSuspenseCountAccount : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 327 | const { endpoint, fetch } = getHooksContext(); 328 | return useSuspenseModelQuery('Account', `${endpoint}/account/count`, args, options, fetch); 329 | } 330 | 331 | 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?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 332 | const { endpoint, fetch } = getHooksContext(); 333 | return useModelQuery('Account', `${endpoint}/account/check`, args, options, fetch); 334 | } 335 | -------------------------------------------------------------------------------- /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 { getQueryKey } from '@zenstackhq/tanstack-query/runtime-v5'; 15 | export { Provider } from '@zenstackhq/tanstack-query/runtime-v5/react'; 16 | export { default as metadata } from './__model_meta'; 17 | -------------------------------------------------------------------------------- /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, List } from "@zenstackhq/runtime/models"; 9 | import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query'; 10 | import { getHooksContext } from '@zenstackhq/tanstack-query/runtime-v5/react'; 11 | import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '@zenstackhq/tanstack-query/runtime-v5/react'; 12 | import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '@zenstackhq/tanstack-query/runtime-v5'; 13 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 14 | import metadata from './__model_meta'; 15 | type DefaultError = QueryError; 16 | import { useSuspenseModelQuery, useSuspenseInfiniteModelQuery } from '@zenstackhq/tanstack-query/runtime-v5/react'; 17 | import type { UseSuspenseQueryOptions, UseSuspenseInfiniteQueryOptions } from '@tanstack/react-query'; 18 | 19 | export function useCreateList(options?: Omit<(UseMutationOptions<(List | undefined), DefaultError, Prisma.ListCreateArgs> & ExtraMutationOptions), 'mutationFn'>) { 20 | const { endpoint, fetch } = getHooksContext(); 21 | const _mutation = 22 | useModelMutation('List', 'POST', `${endpoint}/list/create`, metadata, options, fetch, true) 23 | ; 24 | const mutation = { 25 | ..._mutation, 26 | mutateAsync: async ( 27 | args: Prisma.SelectSubset, 28 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 29 | ) => { 30 | return (await _mutation.mutateAsync( 31 | args, 32 | options as any 33 | )) as (CheckSelect> | undefined); 34 | }, 35 | }; 36 | return mutation; 37 | } 38 | 39 | export function useCreateManyList(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 40 | const { endpoint, fetch } = getHooksContext(); 41 | const _mutation = 42 | useModelMutation('List', 'POST', `${endpoint}/list/createMany`, metadata, options, fetch, false) 43 | ; 44 | const mutation = { 45 | ..._mutation, 46 | mutateAsync: async ( 47 | args: Prisma.SelectSubset, 48 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 49 | ) => { 50 | return (await _mutation.mutateAsync( 51 | args, 52 | options as any 53 | )) as Prisma.BatchPayload; 54 | }, 55 | }; 56 | return mutation; 57 | } 58 | 59 | export function useFindManyList & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 60 | const { endpoint, fetch } = getHooksContext(); 61 | return useModelQuery('List', `${endpoint}/list/findMany`, args, options, fetch); 62 | } 63 | 64 | export function useInfiniteFindManyList>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 65 | options = options ?? { getNextPageParam: () => null }; 66 | const { endpoint, fetch } = getHooksContext(); 67 | return useInfiniteModelQuery('List', `${endpoint}/list/findMany`, args, options, fetch); 68 | } 69 | 70 | export function useSuspenseFindManyList & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 71 | const { endpoint, fetch } = getHooksContext(); 72 | return useSuspenseModelQuery('List', `${endpoint}/list/findMany`, args, options, fetch); 73 | } 74 | 75 | export function useSuspenseInfiniteFindManyList>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 76 | options = options ?? { getNextPageParam: () => null }; 77 | const { endpoint, fetch } = getHooksContext(); 78 | return useSuspenseInfiniteModelQuery('List', `${endpoint}/list/findMany`, args, options, fetch); 79 | } 80 | 81 | export function useFindUniqueList & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 82 | const { endpoint, fetch } = getHooksContext(); 83 | return useModelQuery('List', `${endpoint}/list/findUnique`, args, options, fetch); 84 | } 85 | 86 | export function useSuspenseFindUniqueList & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 87 | const { endpoint, fetch } = getHooksContext(); 88 | return useSuspenseModelQuery('List', `${endpoint}/list/findUnique`, args, options, fetch); 89 | } 90 | 91 | export function useFindFirstList & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 92 | const { endpoint, fetch } = getHooksContext(); 93 | return useModelQuery('List', `${endpoint}/list/findFirst`, args, options, fetch); 94 | } 95 | 96 | export function useSuspenseFindFirstList & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 97 | const { endpoint, fetch } = getHooksContext(); 98 | return useSuspenseModelQuery('List', `${endpoint}/list/findFirst`, args, options, fetch); 99 | } 100 | 101 | export function useUpdateList(options?: Omit<(UseMutationOptions<(List | undefined), DefaultError, Prisma.ListUpdateArgs> & ExtraMutationOptions), 'mutationFn'>) { 102 | const { endpoint, fetch } = getHooksContext(); 103 | const _mutation = 104 | useModelMutation('List', 'PUT', `${endpoint}/list/update`, metadata, options, fetch, true) 105 | ; 106 | const mutation = { 107 | ..._mutation, 108 | mutateAsync: async ( 109 | args: Prisma.SelectSubset, 110 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 111 | ) => { 112 | return (await _mutation.mutateAsync( 113 | args, 114 | options as any 115 | )) as (CheckSelect> | undefined); 116 | }, 117 | }; 118 | return mutation; 119 | } 120 | 121 | export function useUpdateManyList(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 122 | const { endpoint, fetch } = getHooksContext(); 123 | const _mutation = 124 | useModelMutation('List', 'PUT', `${endpoint}/list/updateMany`, metadata, options, fetch, false) 125 | ; 126 | const mutation = { 127 | ..._mutation, 128 | mutateAsync: async ( 129 | args: Prisma.SelectSubset, 130 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 131 | ) => { 132 | return (await _mutation.mutateAsync( 133 | args, 134 | options as any 135 | )) as Prisma.BatchPayload; 136 | }, 137 | }; 138 | return mutation; 139 | } 140 | 141 | export function useUpsertList(options?: Omit<(UseMutationOptions<(List | undefined), DefaultError, Prisma.ListUpsertArgs> & ExtraMutationOptions), 'mutationFn'>) { 142 | const { endpoint, fetch } = getHooksContext(); 143 | const _mutation = 144 | useModelMutation('List', 'POST', `${endpoint}/list/upsert`, metadata, options, fetch, true) 145 | ; 146 | const mutation = { 147 | ..._mutation, 148 | mutateAsync: async ( 149 | args: Prisma.SelectSubset, 150 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 151 | ) => { 152 | return (await _mutation.mutateAsync( 153 | args, 154 | options as any 155 | )) as (CheckSelect> | undefined); 156 | }, 157 | }; 158 | return mutation; 159 | } 160 | 161 | export function useDeleteList(options?: Omit<(UseMutationOptions<(List | undefined), DefaultError, Prisma.ListDeleteArgs> & ExtraMutationOptions), 'mutationFn'>) { 162 | const { endpoint, fetch } = getHooksContext(); 163 | const _mutation = 164 | useModelMutation('List', 'DELETE', `${endpoint}/list/delete`, metadata, options, fetch, true) 165 | ; 166 | const mutation = { 167 | ..._mutation, 168 | mutateAsync: async ( 169 | args: Prisma.SelectSubset, 170 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 171 | ) => { 172 | return (await _mutation.mutateAsync( 173 | args, 174 | options as any 175 | )) as (CheckSelect> | undefined); 176 | }, 177 | }; 178 | return mutation; 179 | } 180 | 181 | export function useDeleteManyList(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 182 | const { endpoint, fetch } = getHooksContext(); 183 | const _mutation = 184 | useModelMutation('List', 'DELETE', `${endpoint}/list/deleteMany`, metadata, options, fetch, false) 185 | ; 186 | const mutation = { 187 | ..._mutation, 188 | mutateAsync: async ( 189 | args: Prisma.SelectSubset, 190 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 191 | ) => { 192 | return (await _mutation.mutateAsync( 193 | args, 194 | options as any 195 | )) as Prisma.BatchPayload; 196 | }, 197 | }; 198 | return mutation; 199 | } 200 | 201 | export function useAggregateList, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 202 | const { endpoint, fetch } = getHooksContext(); 203 | return useModelQuery('List', `${endpoint}/list/aggregate`, args, options, fetch); 204 | } 205 | 206 | export function useSuspenseAggregateList, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 207 | const { endpoint, fetch } = getHooksContext(); 208 | return useSuspenseModelQuery('List', `${endpoint}/list/aggregate`, args, options, fetch); 209 | } 210 | 211 | 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 212 | ? `Error: "by" must not be empty.` 213 | : HavingValid extends Prisma.False 214 | ? { 215 | [P in HavingFields]: P extends ByFields 216 | ? never 217 | : P extends string 218 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 219 | : [ 220 | Error, 221 | 'Field ', 222 | P, 223 | ` in "having" needs to be provided in "by"`, 224 | ] 225 | }[HavingFields] 226 | : 'take' extends Prisma.Keys 227 | ? 'orderBy' extends Prisma.Keys 228 | ? ByValid extends Prisma.True 229 | ? {} 230 | : { 231 | [P in OrderFields]: P extends ByFields 232 | ? never 233 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 234 | }[OrderFields] 235 | : 'Error: If you provide "take", you also need to provide "orderBy"' 236 | : 'skip' extends Prisma.Keys 237 | ? 'orderBy' extends Prisma.Keys 238 | ? ByValid extends Prisma.True 239 | ? {} 240 | : { 241 | [P in OrderFields]: P extends ByFields 242 | ? never 243 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 244 | }[OrderFields] 245 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 246 | : ByValid extends Prisma.True 247 | ? {} 248 | : { 249 | [P in OrderFields]: P extends ByFields 250 | ? never 251 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 252 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 253 | Array & 254 | { 255 | [P in ((keyof TArgs) & (keyof Prisma.ListGroupByOutputType))]: P extends '_count' 256 | ? TArgs[P] extends boolean 257 | ? number 258 | : Prisma.GetScalarType 259 | : Prisma.GetScalarType 260 | } 261 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 262 | const { endpoint, fetch } = getHooksContext(); 263 | return useModelQuery('List', `${endpoint}/list/groupBy`, args, options, fetch); 264 | } 265 | 266 | export function useSuspenseGroupByList>, 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 267 | ? `Error: "by" must not be empty.` 268 | : HavingValid extends Prisma.False 269 | ? { 270 | [P in HavingFields]: P extends ByFields 271 | ? never 272 | : P extends string 273 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 274 | : [ 275 | Error, 276 | 'Field ', 277 | P, 278 | ` in "having" needs to be provided in "by"`, 279 | ] 280 | }[HavingFields] 281 | : 'take' extends Prisma.Keys 282 | ? 'orderBy' extends Prisma.Keys 283 | ? ByValid extends Prisma.True 284 | ? {} 285 | : { 286 | [P in OrderFields]: P extends ByFields 287 | ? never 288 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 289 | }[OrderFields] 290 | : 'Error: If you provide "take", you also need to provide "orderBy"' 291 | : 'skip' extends Prisma.Keys 292 | ? 'orderBy' extends Prisma.Keys 293 | ? ByValid extends Prisma.True 294 | ? {} 295 | : { 296 | [P in OrderFields]: P extends ByFields 297 | ? never 298 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 299 | }[OrderFields] 300 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 301 | : ByValid extends Prisma.True 302 | ? {} 303 | : { 304 | [P in OrderFields]: P extends ByFields 305 | ? never 306 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 307 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 308 | Array & 309 | { 310 | [P in ((keyof TArgs) & (keyof Prisma.ListGroupByOutputType))]: P extends '_count' 311 | ? TArgs[P] extends boolean 312 | ? number 313 | : Prisma.GetScalarType 314 | : Prisma.GetScalarType 315 | } 316 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 317 | const { endpoint, fetch } = getHooksContext(); 318 | return useSuspenseModelQuery('List', `${endpoint}/list/groupBy`, args, options, fetch); 319 | } 320 | 321 | export function useCountList : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 322 | const { endpoint, fetch } = getHooksContext(); 323 | return useModelQuery('List', `${endpoint}/list/count`, args, options, fetch); 324 | } 325 | 326 | export function useSuspenseCountList : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 327 | const { endpoint, fetch } = getHooksContext(); 328 | return useSuspenseModelQuery('List', `${endpoint}/list/count`, args, options, fetch); 329 | } 330 | 331 | export function useCheckList(args: { operation: PolicyCrudKind; where?: { id?: string; spaceId?: string; ownerId?: string; title?: string; private?: boolean }; }, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 332 | const { endpoint, fetch } = getHooksContext(); 333 | return useModelQuery('List', `${endpoint}/list/check`, args, options, fetch); 334 | } 335 | -------------------------------------------------------------------------------- /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, Space } from "@zenstackhq/runtime/models"; 9 | import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query'; 10 | import { getHooksContext } from '@zenstackhq/tanstack-query/runtime-v5/react'; 11 | import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '@zenstackhq/tanstack-query/runtime-v5/react'; 12 | import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '@zenstackhq/tanstack-query/runtime-v5'; 13 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 14 | import metadata from './__model_meta'; 15 | type DefaultError = QueryError; 16 | import { useSuspenseModelQuery, useSuspenseInfiniteModelQuery } from '@zenstackhq/tanstack-query/runtime-v5/react'; 17 | import type { UseSuspenseQueryOptions, UseSuspenseInfiniteQueryOptions } from '@tanstack/react-query'; 18 | 19 | export function useCreateSpace(options?: Omit<(UseMutationOptions<(Space | undefined), DefaultError, Prisma.SpaceCreateArgs> & ExtraMutationOptions), 'mutationFn'>) { 20 | const { endpoint, fetch } = getHooksContext(); 21 | const _mutation = 22 | useModelMutation('Space', 'POST', `${endpoint}/space/create`, metadata, options, fetch, true) 23 | ; 24 | const mutation = { 25 | ..._mutation, 26 | mutateAsync: async ( 27 | args: Prisma.SelectSubset, 28 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 29 | ) => { 30 | return (await _mutation.mutateAsync( 31 | args, 32 | options as any 33 | )) as (CheckSelect> | undefined); 34 | }, 35 | }; 36 | return mutation; 37 | } 38 | 39 | export function useCreateManySpace(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 40 | const { endpoint, fetch } = getHooksContext(); 41 | const _mutation = 42 | useModelMutation('Space', 'POST', `${endpoint}/space/createMany`, metadata, options, fetch, false) 43 | ; 44 | const mutation = { 45 | ..._mutation, 46 | mutateAsync: async ( 47 | args: Prisma.SelectSubset, 48 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 49 | ) => { 50 | return (await _mutation.mutateAsync( 51 | args, 52 | options as any 53 | )) as Prisma.BatchPayload; 54 | }, 55 | }; 56 | return mutation; 57 | } 58 | 59 | export function useFindManySpace & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 60 | const { endpoint, fetch } = getHooksContext(); 61 | return useModelQuery('Space', `${endpoint}/space/findMany`, args, options, fetch); 62 | } 63 | 64 | export function useInfiniteFindManySpace>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 65 | options = options ?? { getNextPageParam: () => null }; 66 | const { endpoint, fetch } = getHooksContext(); 67 | return useInfiniteModelQuery('Space', `${endpoint}/space/findMany`, args, options, fetch); 68 | } 69 | 70 | export function useSuspenseFindManySpace & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 71 | const { endpoint, fetch } = getHooksContext(); 72 | return useSuspenseModelQuery('Space', `${endpoint}/space/findMany`, args, options, fetch); 73 | } 74 | 75 | export function useSuspenseInfiniteFindManySpace>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 76 | options = options ?? { getNextPageParam: () => null }; 77 | const { endpoint, fetch } = getHooksContext(); 78 | return useSuspenseInfiniteModelQuery('Space', `${endpoint}/space/findMany`, args, options, fetch); 79 | } 80 | 81 | export function useFindUniqueSpace & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 82 | const { endpoint, fetch } = getHooksContext(); 83 | return useModelQuery('Space', `${endpoint}/space/findUnique`, args, options, fetch); 84 | } 85 | 86 | export function useSuspenseFindUniqueSpace & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 87 | const { endpoint, fetch } = getHooksContext(); 88 | return useSuspenseModelQuery('Space', `${endpoint}/space/findUnique`, args, options, fetch); 89 | } 90 | 91 | export function useFindFirstSpace & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 92 | const { endpoint, fetch } = getHooksContext(); 93 | return useModelQuery('Space', `${endpoint}/space/findFirst`, args, options, fetch); 94 | } 95 | 96 | export function useSuspenseFindFirstSpace & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 97 | const { endpoint, fetch } = getHooksContext(); 98 | return useSuspenseModelQuery('Space', `${endpoint}/space/findFirst`, args, options, fetch); 99 | } 100 | 101 | export function useUpdateSpace(options?: Omit<(UseMutationOptions<(Space | undefined), DefaultError, Prisma.SpaceUpdateArgs> & ExtraMutationOptions), 'mutationFn'>) { 102 | const { endpoint, fetch } = getHooksContext(); 103 | const _mutation = 104 | useModelMutation('Space', 'PUT', `${endpoint}/space/update`, metadata, options, fetch, true) 105 | ; 106 | const mutation = { 107 | ..._mutation, 108 | mutateAsync: async ( 109 | args: Prisma.SelectSubset, 110 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 111 | ) => { 112 | return (await _mutation.mutateAsync( 113 | args, 114 | options as any 115 | )) as (CheckSelect> | undefined); 116 | }, 117 | }; 118 | return mutation; 119 | } 120 | 121 | export function useUpdateManySpace(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 122 | const { endpoint, fetch } = getHooksContext(); 123 | const _mutation = 124 | useModelMutation('Space', 'PUT', `${endpoint}/space/updateMany`, metadata, options, fetch, false) 125 | ; 126 | const mutation = { 127 | ..._mutation, 128 | mutateAsync: async ( 129 | args: Prisma.SelectSubset, 130 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 131 | ) => { 132 | return (await _mutation.mutateAsync( 133 | args, 134 | options as any 135 | )) as Prisma.BatchPayload; 136 | }, 137 | }; 138 | return mutation; 139 | } 140 | 141 | export function useUpsertSpace(options?: Omit<(UseMutationOptions<(Space | undefined), DefaultError, Prisma.SpaceUpsertArgs> & ExtraMutationOptions), 'mutationFn'>) { 142 | const { endpoint, fetch } = getHooksContext(); 143 | const _mutation = 144 | useModelMutation('Space', 'POST', `${endpoint}/space/upsert`, metadata, options, fetch, true) 145 | ; 146 | const mutation = { 147 | ..._mutation, 148 | mutateAsync: async ( 149 | args: Prisma.SelectSubset, 150 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 151 | ) => { 152 | return (await _mutation.mutateAsync( 153 | args, 154 | options as any 155 | )) as (CheckSelect> | undefined); 156 | }, 157 | }; 158 | return mutation; 159 | } 160 | 161 | export function useDeleteSpace(options?: Omit<(UseMutationOptions<(Space | undefined), DefaultError, Prisma.SpaceDeleteArgs> & ExtraMutationOptions), 'mutationFn'>) { 162 | const { endpoint, fetch } = getHooksContext(); 163 | const _mutation = 164 | useModelMutation('Space', 'DELETE', `${endpoint}/space/delete`, metadata, options, fetch, true) 165 | ; 166 | const mutation = { 167 | ..._mutation, 168 | mutateAsync: async ( 169 | args: Prisma.SelectSubset, 170 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 171 | ) => { 172 | return (await _mutation.mutateAsync( 173 | args, 174 | options as any 175 | )) as (CheckSelect> | undefined); 176 | }, 177 | }; 178 | return mutation; 179 | } 180 | 181 | export function useDeleteManySpace(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 182 | const { endpoint, fetch } = getHooksContext(); 183 | const _mutation = 184 | useModelMutation('Space', 'DELETE', `${endpoint}/space/deleteMany`, metadata, options, fetch, false) 185 | ; 186 | const mutation = { 187 | ..._mutation, 188 | mutateAsync: async ( 189 | args: Prisma.SelectSubset, 190 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 191 | ) => { 192 | return (await _mutation.mutateAsync( 193 | args, 194 | options as any 195 | )) as Prisma.BatchPayload; 196 | }, 197 | }; 198 | return mutation; 199 | } 200 | 201 | export function useAggregateSpace, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 202 | const { endpoint, fetch } = getHooksContext(); 203 | return useModelQuery('Space', `${endpoint}/space/aggregate`, args, options, fetch); 204 | } 205 | 206 | export function useSuspenseAggregateSpace, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 207 | const { endpoint, fetch } = getHooksContext(); 208 | return useSuspenseModelQuery('Space', `${endpoint}/space/aggregate`, args, options, fetch); 209 | } 210 | 211 | 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 212 | ? `Error: "by" must not be empty.` 213 | : HavingValid extends Prisma.False 214 | ? { 215 | [P in HavingFields]: P extends ByFields 216 | ? never 217 | : P extends string 218 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 219 | : [ 220 | Error, 221 | 'Field ', 222 | P, 223 | ` in "having" needs to be provided in "by"`, 224 | ] 225 | }[HavingFields] 226 | : 'take' extends Prisma.Keys 227 | ? 'orderBy' extends Prisma.Keys 228 | ? ByValid extends Prisma.True 229 | ? {} 230 | : { 231 | [P in OrderFields]: P extends ByFields 232 | ? never 233 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 234 | }[OrderFields] 235 | : 'Error: If you provide "take", you also need to provide "orderBy"' 236 | : 'skip' extends Prisma.Keys 237 | ? 'orderBy' extends Prisma.Keys 238 | ? ByValid extends Prisma.True 239 | ? {} 240 | : { 241 | [P in OrderFields]: P extends ByFields 242 | ? never 243 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 244 | }[OrderFields] 245 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 246 | : ByValid extends Prisma.True 247 | ? {} 248 | : { 249 | [P in OrderFields]: P extends ByFields 250 | ? never 251 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 252 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 253 | Array & 254 | { 255 | [P in ((keyof TArgs) & (keyof Prisma.SpaceGroupByOutputType))]: P extends '_count' 256 | ? TArgs[P] extends boolean 257 | ? number 258 | : Prisma.GetScalarType 259 | : Prisma.GetScalarType 260 | } 261 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 262 | const { endpoint, fetch } = getHooksContext(); 263 | return useModelQuery('Space', `${endpoint}/space/groupBy`, args, options, fetch); 264 | } 265 | 266 | export function useSuspenseGroupBySpace>, 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 267 | ? `Error: "by" must not be empty.` 268 | : HavingValid extends Prisma.False 269 | ? { 270 | [P in HavingFields]: P extends ByFields 271 | ? never 272 | : P extends string 273 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 274 | : [ 275 | Error, 276 | 'Field ', 277 | P, 278 | ` in "having" needs to be provided in "by"`, 279 | ] 280 | }[HavingFields] 281 | : 'take' extends Prisma.Keys 282 | ? 'orderBy' extends Prisma.Keys 283 | ? ByValid extends Prisma.True 284 | ? {} 285 | : { 286 | [P in OrderFields]: P extends ByFields 287 | ? never 288 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 289 | }[OrderFields] 290 | : 'Error: If you provide "take", you also need to provide "orderBy"' 291 | : 'skip' extends Prisma.Keys 292 | ? 'orderBy' extends Prisma.Keys 293 | ? ByValid extends Prisma.True 294 | ? {} 295 | : { 296 | [P in OrderFields]: P extends ByFields 297 | ? never 298 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 299 | }[OrderFields] 300 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 301 | : ByValid extends Prisma.True 302 | ? {} 303 | : { 304 | [P in OrderFields]: P extends ByFields 305 | ? never 306 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 307 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 308 | Array & 309 | { 310 | [P in ((keyof TArgs) & (keyof Prisma.SpaceGroupByOutputType))]: P extends '_count' 311 | ? TArgs[P] extends boolean 312 | ? number 313 | : Prisma.GetScalarType 314 | : Prisma.GetScalarType 315 | } 316 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 317 | const { endpoint, fetch } = getHooksContext(); 318 | return useSuspenseModelQuery('Space', `${endpoint}/space/groupBy`, args, options, fetch); 319 | } 320 | 321 | export function useCountSpace : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 322 | const { endpoint, fetch } = getHooksContext(); 323 | return useModelQuery('Space', `${endpoint}/space/count`, args, options, fetch); 324 | } 325 | 326 | export function useSuspenseCountSpace : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 327 | const { endpoint, fetch } = getHooksContext(); 328 | return useSuspenseModelQuery('Space', `${endpoint}/space/count`, args, options, fetch); 329 | } 330 | 331 | export function useCheckSpace(args: { operation: PolicyCrudKind; where?: { id?: string; ownerId?: string; name?: string; slug?: string }; }, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 332 | const { endpoint, fetch } = getHooksContext(); 333 | return useModelQuery('Space', `${endpoint}/space/check`, args, options, fetch); 334 | } 335 | -------------------------------------------------------------------------------- /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, Todo } from "@zenstackhq/runtime/models"; 9 | import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query'; 10 | import { getHooksContext } from '@zenstackhq/tanstack-query/runtime-v5/react'; 11 | import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '@zenstackhq/tanstack-query/runtime-v5/react'; 12 | import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '@zenstackhq/tanstack-query/runtime-v5'; 13 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 14 | import metadata from './__model_meta'; 15 | type DefaultError = QueryError; 16 | import { useSuspenseModelQuery, useSuspenseInfiniteModelQuery } from '@zenstackhq/tanstack-query/runtime-v5/react'; 17 | import type { UseSuspenseQueryOptions, UseSuspenseInfiniteQueryOptions } from '@tanstack/react-query'; 18 | 19 | export function useCreateTodo(options?: Omit<(UseMutationOptions<(Todo | undefined), DefaultError, Prisma.TodoCreateArgs> & ExtraMutationOptions), 'mutationFn'>) { 20 | const { endpoint, fetch } = getHooksContext(); 21 | const _mutation = 22 | useModelMutation('Todo', 'POST', `${endpoint}/todo/create`, metadata, options, fetch, true) 23 | ; 24 | const mutation = { 25 | ..._mutation, 26 | mutateAsync: async ( 27 | args: Prisma.SelectSubset, 28 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 29 | ) => { 30 | return (await _mutation.mutateAsync( 31 | args, 32 | options as any 33 | )) as (CheckSelect> | undefined); 34 | }, 35 | }; 36 | return mutation; 37 | } 38 | 39 | export function useCreateManyTodo(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 40 | const { endpoint, fetch } = getHooksContext(); 41 | const _mutation = 42 | useModelMutation('Todo', 'POST', `${endpoint}/todo/createMany`, metadata, options, fetch, false) 43 | ; 44 | const mutation = { 45 | ..._mutation, 46 | mutateAsync: async ( 47 | args: Prisma.SelectSubset, 48 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 49 | ) => { 50 | return (await _mutation.mutateAsync( 51 | args, 52 | options as any 53 | )) as Prisma.BatchPayload; 54 | }, 55 | }; 56 | return mutation; 57 | } 58 | 59 | export function useFindManyTodo & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 60 | const { endpoint, fetch } = getHooksContext(); 61 | return useModelQuery('Todo', `${endpoint}/todo/findMany`, args, options, fetch); 62 | } 63 | 64 | export function useInfiniteFindManyTodo>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 65 | options = options ?? { getNextPageParam: () => null }; 66 | const { endpoint, fetch } = getHooksContext(); 67 | return useInfiniteModelQuery('Todo', `${endpoint}/todo/findMany`, args, options, fetch); 68 | } 69 | 70 | export function useSuspenseFindManyTodo & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 71 | const { endpoint, fetch } = getHooksContext(); 72 | return useSuspenseModelQuery('Todo', `${endpoint}/todo/findMany`, args, options, fetch); 73 | } 74 | 75 | export function useSuspenseInfiniteFindManyTodo>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 76 | options = options ?? { getNextPageParam: () => null }; 77 | const { endpoint, fetch } = getHooksContext(); 78 | return useSuspenseInfiniteModelQuery('Todo', `${endpoint}/todo/findMany`, args, options, fetch); 79 | } 80 | 81 | export function useFindUniqueTodo & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 82 | const { endpoint, fetch } = getHooksContext(); 83 | return useModelQuery('Todo', `${endpoint}/todo/findUnique`, args, options, fetch); 84 | } 85 | 86 | export function useSuspenseFindUniqueTodo & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 87 | const { endpoint, fetch } = getHooksContext(); 88 | return useSuspenseModelQuery('Todo', `${endpoint}/todo/findUnique`, args, options, fetch); 89 | } 90 | 91 | export function useFindFirstTodo & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 92 | const { endpoint, fetch } = getHooksContext(); 93 | return useModelQuery('Todo', `${endpoint}/todo/findFirst`, args, options, fetch); 94 | } 95 | 96 | export function useSuspenseFindFirstTodo & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 97 | const { endpoint, fetch } = getHooksContext(); 98 | return useSuspenseModelQuery('Todo', `${endpoint}/todo/findFirst`, args, options, fetch); 99 | } 100 | 101 | export function useUpdateTodo(options?: Omit<(UseMutationOptions<(Todo | undefined), DefaultError, Prisma.TodoUpdateArgs> & ExtraMutationOptions), 'mutationFn'>) { 102 | const { endpoint, fetch } = getHooksContext(); 103 | const _mutation = 104 | useModelMutation('Todo', 'PUT', `${endpoint}/todo/update`, metadata, options, fetch, true) 105 | ; 106 | const mutation = { 107 | ..._mutation, 108 | mutateAsync: async ( 109 | args: Prisma.SelectSubset, 110 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 111 | ) => { 112 | return (await _mutation.mutateAsync( 113 | args, 114 | options as any 115 | )) as (CheckSelect> | undefined); 116 | }, 117 | }; 118 | return mutation; 119 | } 120 | 121 | export function useUpdateManyTodo(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 122 | const { endpoint, fetch } = getHooksContext(); 123 | const _mutation = 124 | useModelMutation('Todo', 'PUT', `${endpoint}/todo/updateMany`, metadata, options, fetch, false) 125 | ; 126 | const mutation = { 127 | ..._mutation, 128 | mutateAsync: async ( 129 | args: Prisma.SelectSubset, 130 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 131 | ) => { 132 | return (await _mutation.mutateAsync( 133 | args, 134 | options as any 135 | )) as Prisma.BatchPayload; 136 | }, 137 | }; 138 | return mutation; 139 | } 140 | 141 | export function useUpsertTodo(options?: Omit<(UseMutationOptions<(Todo | undefined), DefaultError, Prisma.TodoUpsertArgs> & ExtraMutationOptions), 'mutationFn'>) { 142 | const { endpoint, fetch } = getHooksContext(); 143 | const _mutation = 144 | useModelMutation('Todo', 'POST', `${endpoint}/todo/upsert`, metadata, options, fetch, true) 145 | ; 146 | const mutation = { 147 | ..._mutation, 148 | mutateAsync: async ( 149 | args: Prisma.SelectSubset, 150 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 151 | ) => { 152 | return (await _mutation.mutateAsync( 153 | args, 154 | options as any 155 | )) as (CheckSelect> | undefined); 156 | }, 157 | }; 158 | return mutation; 159 | } 160 | 161 | export function useDeleteTodo(options?: Omit<(UseMutationOptions<(Todo | undefined), DefaultError, Prisma.TodoDeleteArgs> & ExtraMutationOptions), 'mutationFn'>) { 162 | const { endpoint, fetch } = getHooksContext(); 163 | const _mutation = 164 | useModelMutation('Todo', 'DELETE', `${endpoint}/todo/delete`, metadata, options, fetch, true) 165 | ; 166 | const mutation = { 167 | ..._mutation, 168 | mutateAsync: async ( 169 | args: Prisma.SelectSubset, 170 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 171 | ) => { 172 | return (await _mutation.mutateAsync( 173 | args, 174 | options as any 175 | )) as (CheckSelect> | undefined); 176 | }, 177 | }; 178 | return mutation; 179 | } 180 | 181 | export function useDeleteManyTodo(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 182 | const { endpoint, fetch } = getHooksContext(); 183 | const _mutation = 184 | useModelMutation('Todo', 'DELETE', `${endpoint}/todo/deleteMany`, metadata, options, fetch, false) 185 | ; 186 | const mutation = { 187 | ..._mutation, 188 | mutateAsync: async ( 189 | args: Prisma.SelectSubset, 190 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 191 | ) => { 192 | return (await _mutation.mutateAsync( 193 | args, 194 | options as any 195 | )) as Prisma.BatchPayload; 196 | }, 197 | }; 198 | return mutation; 199 | } 200 | 201 | export function useAggregateTodo, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 202 | const { endpoint, fetch } = getHooksContext(); 203 | return useModelQuery('Todo', `${endpoint}/todo/aggregate`, args, options, fetch); 204 | } 205 | 206 | export function useSuspenseAggregateTodo, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 207 | const { endpoint, fetch } = getHooksContext(); 208 | return useSuspenseModelQuery('Todo', `${endpoint}/todo/aggregate`, args, options, fetch); 209 | } 210 | 211 | 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 212 | ? `Error: "by" must not be empty.` 213 | : HavingValid extends Prisma.False 214 | ? { 215 | [P in HavingFields]: P extends ByFields 216 | ? never 217 | : P extends string 218 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 219 | : [ 220 | Error, 221 | 'Field ', 222 | P, 223 | ` in "having" needs to be provided in "by"`, 224 | ] 225 | }[HavingFields] 226 | : 'take' extends Prisma.Keys 227 | ? 'orderBy' extends Prisma.Keys 228 | ? ByValid extends Prisma.True 229 | ? {} 230 | : { 231 | [P in OrderFields]: P extends ByFields 232 | ? never 233 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 234 | }[OrderFields] 235 | : 'Error: If you provide "take", you also need to provide "orderBy"' 236 | : 'skip' extends Prisma.Keys 237 | ? 'orderBy' extends Prisma.Keys 238 | ? ByValid extends Prisma.True 239 | ? {} 240 | : { 241 | [P in OrderFields]: P extends ByFields 242 | ? never 243 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 244 | }[OrderFields] 245 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 246 | : ByValid extends Prisma.True 247 | ? {} 248 | : { 249 | [P in OrderFields]: P extends ByFields 250 | ? never 251 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 252 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 253 | Array & 254 | { 255 | [P in ((keyof TArgs) & (keyof Prisma.TodoGroupByOutputType))]: P extends '_count' 256 | ? TArgs[P] extends boolean 257 | ? number 258 | : Prisma.GetScalarType 259 | : Prisma.GetScalarType 260 | } 261 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 262 | const { endpoint, fetch } = getHooksContext(); 263 | return useModelQuery('Todo', `${endpoint}/todo/groupBy`, args, options, fetch); 264 | } 265 | 266 | export function useSuspenseGroupByTodo>, 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 267 | ? `Error: "by" must not be empty.` 268 | : HavingValid extends Prisma.False 269 | ? { 270 | [P in HavingFields]: P extends ByFields 271 | ? never 272 | : P extends string 273 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 274 | : [ 275 | Error, 276 | 'Field ', 277 | P, 278 | ` in "having" needs to be provided in "by"`, 279 | ] 280 | }[HavingFields] 281 | : 'take' extends Prisma.Keys 282 | ? 'orderBy' extends Prisma.Keys 283 | ? ByValid extends Prisma.True 284 | ? {} 285 | : { 286 | [P in OrderFields]: P extends ByFields 287 | ? never 288 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 289 | }[OrderFields] 290 | : 'Error: If you provide "take", you also need to provide "orderBy"' 291 | : 'skip' extends Prisma.Keys 292 | ? 'orderBy' extends Prisma.Keys 293 | ? ByValid extends Prisma.True 294 | ? {} 295 | : { 296 | [P in OrderFields]: P extends ByFields 297 | ? never 298 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 299 | }[OrderFields] 300 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 301 | : ByValid extends Prisma.True 302 | ? {} 303 | : { 304 | [P in OrderFields]: P extends ByFields 305 | ? never 306 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 307 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 308 | Array & 309 | { 310 | [P in ((keyof TArgs) & (keyof Prisma.TodoGroupByOutputType))]: P extends '_count' 311 | ? TArgs[P] extends boolean 312 | ? number 313 | : Prisma.GetScalarType 314 | : Prisma.GetScalarType 315 | } 316 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 317 | const { endpoint, fetch } = getHooksContext(); 318 | return useSuspenseModelQuery('Todo', `${endpoint}/todo/groupBy`, args, options, fetch); 319 | } 320 | 321 | export function useCountTodo : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 322 | const { endpoint, fetch } = getHooksContext(); 323 | return useModelQuery('Todo', `${endpoint}/todo/count`, args, options, fetch); 324 | } 325 | 326 | export function useSuspenseCountTodo : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 327 | const { endpoint, fetch } = getHooksContext(); 328 | return useSuspenseModelQuery('Todo', `${endpoint}/todo/count`, args, options, fetch); 329 | } 330 | 331 | export function useCheckTodo(args: { operation: PolicyCrudKind; where?: { id?: string; ownerId?: string; listId?: string; title?: string }; }, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 332 | const { endpoint, fetch } = getHooksContext(); 333 | return useModelQuery('Todo', `${endpoint}/todo/check`, args, options, fetch); 334 | } 335 | -------------------------------------------------------------------------------- /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, User } from "@zenstackhq/runtime/models"; 9 | import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query'; 10 | import { getHooksContext } from '@zenstackhq/tanstack-query/runtime-v5/react'; 11 | import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '@zenstackhq/tanstack-query/runtime-v5/react'; 12 | import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '@zenstackhq/tanstack-query/runtime-v5'; 13 | import type { PolicyCrudKind } from '@zenstackhq/runtime' 14 | import metadata from './__model_meta'; 15 | type DefaultError = QueryError; 16 | import { useSuspenseModelQuery, useSuspenseInfiniteModelQuery } from '@zenstackhq/tanstack-query/runtime-v5/react'; 17 | import type { UseSuspenseQueryOptions, UseSuspenseInfiniteQueryOptions } from '@tanstack/react-query'; 18 | 19 | export function useCreateUser(options?: Omit<(UseMutationOptions<(User | undefined), DefaultError, Prisma.UserCreateArgs> & ExtraMutationOptions), 'mutationFn'>) { 20 | const { endpoint, fetch } = getHooksContext(); 21 | const _mutation = 22 | useModelMutation('User', 'POST', `${endpoint}/user/create`, metadata, options, fetch, true) 23 | ; 24 | const mutation = { 25 | ..._mutation, 26 | mutateAsync: async ( 27 | args: Prisma.SelectSubset, 28 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 29 | ) => { 30 | return (await _mutation.mutateAsync( 31 | args, 32 | options as any 33 | )) as (CheckSelect> | undefined); 34 | }, 35 | }; 36 | return mutation; 37 | } 38 | 39 | export function useCreateManyUser(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 40 | const { endpoint, fetch } = getHooksContext(); 41 | const _mutation = 42 | useModelMutation('User', 'POST', `${endpoint}/user/createMany`, metadata, options, fetch, false) 43 | ; 44 | const mutation = { 45 | ..._mutation, 46 | mutateAsync: async ( 47 | args: Prisma.SelectSubset, 48 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 49 | ) => { 50 | return (await _mutation.mutateAsync( 51 | args, 52 | options as any 53 | )) as Prisma.BatchPayload; 54 | }, 55 | }; 56 | return mutation; 57 | } 58 | 59 | export function useFindManyUser & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 60 | const { endpoint, fetch } = getHooksContext(); 61 | return useModelQuery('User', `${endpoint}/user/findMany`, args, options, fetch); 62 | } 63 | 64 | export function useInfiniteFindManyUser>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 65 | options = options ?? { getNextPageParam: () => null }; 66 | const { endpoint, fetch } = getHooksContext(); 67 | return useInfiniteModelQuery('User', `${endpoint}/user/findMany`, args, options, fetch); 68 | } 69 | 70 | export function useSuspenseFindManyUser & { $optimistic?: boolean }>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 71 | const { endpoint, fetch } = getHooksContext(); 72 | return useSuspenseModelQuery('User', `${endpoint}/user/findMany`, args, options, fetch); 73 | } 74 | 75 | export function useSuspenseInfiniteFindManyUser>, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: Omit>, 'queryKey' | 'initialPageParam'>) { 76 | options = options ?? { getNextPageParam: () => null }; 77 | const { endpoint, fetch } = getHooksContext(); 78 | return useSuspenseInfiniteModelQuery('User', `${endpoint}/user/findMany`, args, options, fetch); 79 | } 80 | 81 | export function useFindUniqueUser & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 82 | const { endpoint, fetch } = getHooksContext(); 83 | return useModelQuery('User', `${endpoint}/user/findUnique`, args, options, fetch); 84 | } 85 | 86 | export function useSuspenseFindUniqueUser & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 87 | const { endpoint, fetch } = getHooksContext(); 88 | return useSuspenseModelQuery('User', `${endpoint}/user/findUnique`, args, options, fetch); 89 | } 90 | 91 | export function useFindFirstUser & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 92 | const { endpoint, fetch } = getHooksContext(); 93 | return useModelQuery('User', `${endpoint}/user/findFirst`, args, options, fetch); 94 | } 95 | 96 | export function useSuspenseFindFirstUser & { $optimistic?: boolean }, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 97 | const { endpoint, fetch } = getHooksContext(); 98 | return useSuspenseModelQuery('User', `${endpoint}/user/findFirst`, args, options, fetch); 99 | } 100 | 101 | export function useUpdateUser(options?: Omit<(UseMutationOptions<(User | undefined), DefaultError, Prisma.UserUpdateArgs> & ExtraMutationOptions), 'mutationFn'>) { 102 | const { endpoint, fetch } = getHooksContext(); 103 | const _mutation = 104 | useModelMutation('User', 'PUT', `${endpoint}/user/update`, metadata, options, fetch, true) 105 | ; 106 | const mutation = { 107 | ..._mutation, 108 | mutateAsync: async ( 109 | args: Prisma.SelectSubset, 110 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 111 | ) => { 112 | return (await _mutation.mutateAsync( 113 | args, 114 | options as any 115 | )) as (CheckSelect> | undefined); 116 | }, 117 | }; 118 | return mutation; 119 | } 120 | 121 | export function useUpdateManyUser(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 122 | const { endpoint, fetch } = getHooksContext(); 123 | const _mutation = 124 | useModelMutation('User', 'PUT', `${endpoint}/user/updateMany`, metadata, options, fetch, false) 125 | ; 126 | const mutation = { 127 | ..._mutation, 128 | mutateAsync: async ( 129 | args: Prisma.SelectSubset, 130 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 131 | ) => { 132 | return (await _mutation.mutateAsync( 133 | args, 134 | options as any 135 | )) as Prisma.BatchPayload; 136 | }, 137 | }; 138 | return mutation; 139 | } 140 | 141 | export function useUpsertUser(options?: Omit<(UseMutationOptions<(User | undefined), DefaultError, Prisma.UserUpsertArgs> & ExtraMutationOptions), 'mutationFn'>) { 142 | const { endpoint, fetch } = getHooksContext(); 143 | const _mutation = 144 | useModelMutation('User', 'POST', `${endpoint}/user/upsert`, metadata, options, fetch, true) 145 | ; 146 | const mutation = { 147 | ..._mutation, 148 | mutateAsync: async ( 149 | args: Prisma.SelectSubset, 150 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 151 | ) => { 152 | return (await _mutation.mutateAsync( 153 | args, 154 | options as any 155 | )) as (CheckSelect> | undefined); 156 | }, 157 | }; 158 | return mutation; 159 | } 160 | 161 | export function useDeleteUser(options?: Omit<(UseMutationOptions<(User | undefined), DefaultError, Prisma.UserDeleteArgs> & ExtraMutationOptions), 'mutationFn'>) { 162 | const { endpoint, fetch } = getHooksContext(); 163 | const _mutation = 164 | useModelMutation('User', 'DELETE', `${endpoint}/user/delete`, metadata, options, fetch, true) 165 | ; 166 | const mutation = { 167 | ..._mutation, 168 | mutateAsync: async ( 169 | args: Prisma.SelectSubset, 170 | options?: Omit<(UseMutationOptions<(CheckSelect> | undefined), DefaultError, Prisma.SelectSubset> & ExtraMutationOptions), 'mutationFn'> 171 | ) => { 172 | return (await _mutation.mutateAsync( 173 | args, 174 | options as any 175 | )) as (CheckSelect> | undefined); 176 | }, 177 | }; 178 | return mutation; 179 | } 180 | 181 | export function useDeleteManyUser(options?: Omit<(UseMutationOptions & ExtraMutationOptions), 'mutationFn'>) { 182 | const { endpoint, fetch } = getHooksContext(); 183 | const _mutation = 184 | useModelMutation('User', 'DELETE', `${endpoint}/user/deleteMany`, metadata, options, fetch, false) 185 | ; 186 | const mutation = { 187 | ..._mutation, 188 | mutateAsync: async ( 189 | args: Prisma.SelectSubset, 190 | options?: Omit<(UseMutationOptions> & ExtraMutationOptions), 'mutationFn'> 191 | ) => { 192 | return (await _mutation.mutateAsync( 193 | args, 194 | options as any 195 | )) as Prisma.BatchPayload; 196 | }, 197 | }; 198 | return mutation; 199 | } 200 | 201 | export function useAggregateUser, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 202 | const { endpoint, fetch } = getHooksContext(); 203 | return useModelQuery('User', `${endpoint}/user/aggregate`, args, options, fetch); 204 | } 205 | 206 | export function useSuspenseAggregateUser, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 207 | const { endpoint, fetch } = getHooksContext(); 208 | return useSuspenseModelQuery('User', `${endpoint}/user/aggregate`, args, options, fetch); 209 | } 210 | 211 | 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 212 | ? `Error: "by" must not be empty.` 213 | : HavingValid extends Prisma.False 214 | ? { 215 | [P in HavingFields]: P extends ByFields 216 | ? never 217 | : P extends string 218 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 219 | : [ 220 | Error, 221 | 'Field ', 222 | P, 223 | ` in "having" needs to be provided in "by"`, 224 | ] 225 | }[HavingFields] 226 | : 'take' extends Prisma.Keys 227 | ? 'orderBy' extends Prisma.Keys 228 | ? ByValid extends Prisma.True 229 | ? {} 230 | : { 231 | [P in OrderFields]: P extends ByFields 232 | ? never 233 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 234 | }[OrderFields] 235 | : 'Error: If you provide "take", you also need to provide "orderBy"' 236 | : 'skip' extends Prisma.Keys 237 | ? 'orderBy' extends Prisma.Keys 238 | ? ByValid extends Prisma.True 239 | ? {} 240 | : { 241 | [P in OrderFields]: P extends ByFields 242 | ? never 243 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 244 | }[OrderFields] 245 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 246 | : ByValid extends Prisma.True 247 | ? {} 248 | : { 249 | [P in OrderFields]: P extends ByFields 250 | ? never 251 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 252 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 253 | Array & 254 | { 255 | [P in ((keyof TArgs) & (keyof Prisma.UserGroupByOutputType))]: P extends '_count' 256 | ? TArgs[P] extends boolean 257 | ? number 258 | : Prisma.GetScalarType 259 | : Prisma.GetScalarType 260 | } 261 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 262 | const { endpoint, fetch } = getHooksContext(); 263 | return useModelQuery('User', `${endpoint}/user/groupBy`, args, options, fetch); 264 | } 265 | 266 | export function useSuspenseGroupByUser>, 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 TArgs['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True 267 | ? `Error: "by" must not be empty.` 268 | : HavingValid extends Prisma.False 269 | ? { 270 | [P in HavingFields]: P extends ByFields 271 | ? never 272 | : P extends string 273 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".` 274 | : [ 275 | Error, 276 | 'Field ', 277 | P, 278 | ` in "having" needs to be provided in "by"`, 279 | ] 280 | }[HavingFields] 281 | : 'take' extends Prisma.Keys 282 | ? 'orderBy' extends Prisma.Keys 283 | ? ByValid extends Prisma.True 284 | ? {} 285 | : { 286 | [P in OrderFields]: P extends ByFields 287 | ? never 288 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 289 | }[OrderFields] 290 | : 'Error: If you provide "take", you also need to provide "orderBy"' 291 | : 'skip' extends Prisma.Keys 292 | ? 'orderBy' extends Prisma.Keys 293 | ? ByValid extends Prisma.True 294 | ? {} 295 | : { 296 | [P in OrderFields]: P extends ByFields 297 | ? never 298 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 299 | }[OrderFields] 300 | : 'Error: If you provide "skip", you also need to provide "orderBy"' 301 | : ByValid extends Prisma.True 302 | ? {} 303 | : { 304 | [P in OrderFields]: P extends ByFields 305 | ? never 306 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` 307 | }[OrderFields], TQueryFnData = {} extends InputErrors ? 308 | Array & 309 | { 310 | [P in ((keyof TArgs) & (keyof Prisma.UserGroupByOutputType))]: P extends '_count' 311 | ? TArgs[P] extends boolean 312 | ? number 313 | : Prisma.GetScalarType 314 | : Prisma.GetScalarType 315 | } 316 | > : InputErrors, TData = TQueryFnData, TError = DefaultError>(args: Prisma.SelectSubset & InputErrors>, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 317 | const { endpoint, fetch } = getHooksContext(); 318 | return useSuspenseModelQuery('User', `${endpoint}/user/groupBy`, args, options, fetch); 319 | } 320 | 321 | export function useCountUser : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 322 | const { endpoint, fetch } = getHooksContext(); 323 | return useModelQuery('User', `${endpoint}/user/count`, args, options, fetch); 324 | } 325 | 326 | export function useSuspenseCountUser : number, TData = TQueryFnData, TError = DefaultError>(args?: Prisma.SelectSubset, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 327 | const { endpoint, fetch } = getHooksContext(); 328 | return useSuspenseModelQuery('User', `${endpoint}/user/count`, args, options, fetch); 329 | } 330 | 331 | export function useCheckUser(args: { operation: PolicyCrudKind; where?: { id?: string; email?: string; password?: string; name?: string; image?: string }; }, options?: (Omit, 'queryKey'> & ExtraQueryOptions)) { 332 | const { endpoint, fetch } = getHooksContext(); 333 | return useModelQuery('User', `${endpoint}/user/check`, args, options, fetch); 334 | } 335 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { auth } from 'server/auth'; 2 | 3 | export default auth((req) => { 4 | if ( 5 | !req.auth && 6 | // allow signup 7 | req.nextUrl.pathname !== '/signup' && 8 | // allow signin 9 | req.nextUrl.pathname !== '/signin' && 10 | // allow images 11 | !['.jpg', '.png', '.svg'].some((ext) => req.nextUrl.pathname.endsWith(ext)) 12 | ) { 13 | const newUrl = new URL('/signin', req.nextUrl.origin); 14 | return Response.redirect(newUrl); 15 | } 16 | }); 17 | 18 | export const config = { 19 | matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], 20 | }; 21 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | remotePatterns: [ 6 | { hostname: 'picsum.photos' }, 7 | { hostname: 'lh3.googleusercontent.com' }, 8 | { hostname: 'avatars.githubusercontent.com' }, 9 | ], 10 | }, 11 | }; 12 | 13 | module.exports = nextConfig; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zenstack-todo-sample-nextjs-tanstack", 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/tanstack-query", 18 | "up": "npm run package-clean && npm install -D zenstack@latest && npm install @zenstackhq/runtime@latest @zenstackhq/server@latest @zenstackhq/tanstack-query@latest", 19 | "up-preview": "npm run package-clean && npm install --registry https://preview.registry.zenstack.dev -D zenstack@latest && npm install --registry https://preview.registry.zenstack.dev @zenstackhq/runtime@latest @zenstackhq/server@latest @zenstackhq/tanstack-query@latest" 20 | }, 21 | "dependencies": { 22 | "@auth/prisma-adapter": "^2.7.4", 23 | "@heroicons/react": "^2.0.12", 24 | "@prisma/client": "^6.2.0", 25 | "@tanstack/react-query": "^5.8.4", 26 | "@tanstack/react-query-devtools": "^5.8.3", 27 | "@zenstackhq/runtime": "^2.15.0", 28 | "@zenstackhq/server": "^2.15.0", 29 | "@zenstackhq/tanstack-query": "^2.15.0", 30 | "babel-plugin-superjson-next": "^0.4.5", 31 | "bcryptjs": "^2.4.3", 32 | "daisyui": "^4.0.7", 33 | "decimal.js": "^10.4.3", 34 | "moment": "^2.29.4", 35 | "nanoid": "^4.0.0", 36 | "next": "^15.0.3", 37 | "next-auth": "^5.0.0-beta.25", 38 | "react": "^18.2.0", 39 | "react-dom": "^18.2.0", 40 | "react-toastify": "^10.0.6", 41 | "superjson": "^1.12.0" 42 | }, 43 | "devDependencies": { 44 | "@tailwindcss/line-clamp": "^0.4.2", 45 | "@types/bcryptjs": "^2.4.2", 46 | "@types/node": "^14.17.3", 47 | "@types/react": "^18.2.0", 48 | "@types/react-dom": "^18.2.0", 49 | "@typescript-eslint/eslint-plugin": "^6.13.1", 50 | "@typescript-eslint/parser": "^6.13.1", 51 | "autoprefixer": "^10.4.20", 52 | "eslint": "^7.32.0", 53 | "eslint-config-next": "12.3.1", 54 | "lower-case-first": "^2.0.2", 55 | "postcss": "^8.4.49", 56 | "prisma": "^6.2.0", 57 | "tailwindcss": "^3.4.16", 58 | "typescript": "^5.3.2", 59 | "zenstack": "^2.15.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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/20230905040400_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/20241222133651_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 (i.e. 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("POSTGRES_URL") 9 | directUrl = env("POSTGRES_URL_NON_POOLING") 10 | } 11 | 12 | generator js { 13 | provider = "prisma-client-js" 14 | } 15 | 16 | enum SpaceUserRole { 17 | USER 18 | ADMIN 19 | } 20 | 21 | model Space { 22 | id String @id() @default(uuid()) 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt() 25 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 26 | ownerId String 27 | name String 28 | slug String @unique() 29 | members SpaceUser[] 30 | lists List[] 31 | } 32 | 33 | model SpaceUser { 34 | id String @id() @default(uuid()) 35 | createdAt DateTime @default(now()) 36 | updatedAt DateTime @updatedAt() 37 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 38 | spaceId String 39 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 40 | userId String 41 | role SpaceUserRole 42 | 43 | @@unique([userId, spaceId]) 44 | } 45 | 46 | model User { 47 | id String @id() @default(uuid()) 48 | createdAt DateTime @default(now()) 49 | updatedAt DateTime @updatedAt() 50 | email String @unique() 51 | emailVerified DateTime? 52 | password String? 53 | name String? 54 | ownedSpaces Space[] 55 | memberships SpaceUser[] 56 | image String? 57 | lists List[] 58 | todos Todo[] 59 | accounts Account[] 60 | } 61 | 62 | model List { 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 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 69 | ownerId String 70 | title String 71 | private Boolean @default(false) 72 | todos Todo[] 73 | } 74 | 75 | model Todo { 76 | id String @id() @default(uuid()) 77 | createdAt DateTime @default(now()) 78 | updatedAt DateTime @updatedAt() 79 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 80 | ownerId String 81 | list List @relation(fields: [listId], references: [id], onDelete: Cascade) 82 | listId String 83 | title String 84 | completedAt DateTime? 85 | } 86 | 87 | model Account { 88 | id String @id() @default(uuid()) 89 | userId String 90 | type String 91 | provider String 92 | providerAccountId String 93 | refresh_token String? 94 | refresh_token_expires_in Int? 95 | access_token String? 96 | expires_at Int? 97 | token_type String? 98 | scope String? 99 | id_token String? 100 | session_state String? 101 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 102 | 103 | @@unique([provider, providerAccountId]) 104 | } 105 | -------------------------------------------------------------------------------- /public/auth-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs-tanstack/f46849a95d3837d4af61a3b62d81d031ac4f1a78/public/auth-bg.jpg -------------------------------------------------------------------------------- /public/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs-tanstack/f46849a95d3837d4af61a3b62d81d031ac4f1a78/public/avatar.jpg -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs-tanstack/f46849a95d3837d4af61a3b62d81d031ac4f1a78/public/logo.png -------------------------------------------------------------------------------- /schema.zmodel: -------------------------------------------------------------------------------- 1 | /* 2 | * Sample model for a collaborative Todo app 3 | */ 4 | 5 | /* 6 | * Data source definition 7 | */ 8 | datasource db { 9 | provider = 'postgresql' 10 | url = env('POSTGRES_URL') 11 | directUrl = env('POSTGRES_URL_NON_POOLING') 12 | } 13 | 14 | generator js { 15 | provider = 'prisma-client-js' 16 | } 17 | 18 | plugin enhancer { 19 | provider = '@core/enhancer' 20 | generatePermissionChecker = true 21 | } 22 | 23 | plugin hooks { 24 | provider = '@zenstackhq/tanstack-query' 25 | output = 'lib/hooks' 26 | target = 'react' 27 | version = 'v5' 28 | } 29 | 30 | /* 31 | * Enum for user's role in a space 32 | */ 33 | enum SpaceUserRole { 34 | USER 35 | ADMIN 36 | } 37 | 38 | /* 39 | * Model for a space in which users can collaborate on Lists and Todos 40 | */ 41 | model Space { 42 | id String @id @default(uuid()) 43 | createdAt DateTime @default(now()) 44 | updatedAt DateTime @updatedAt 45 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 46 | ownerId String @default(auth().id) 47 | name String @length(4, 50) 48 | slug String @unique @regex('^[0-9a-zA-Z]{4,16}$') 49 | members SpaceUser[] 50 | lists List[] 51 | 52 | // require login 53 | @@deny('all', auth() == null) 54 | 55 | // everyone can create a space 56 | @@allow('create', true) 57 | 58 | // any user in the space can read the space 59 | @@allow('read', members?[user == auth()]) 60 | 61 | // space admin can update and delete 62 | @@allow('update,delete', members?[user == auth() && role == ADMIN]) 63 | } 64 | 65 | /* 66 | * Model representing membership of a user in a space 67 | */ 68 | model SpaceUser { 69 | id String @id @default(uuid()) 70 | createdAt DateTime @default(now()) 71 | updatedAt DateTime @updatedAt 72 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 73 | spaceId String 74 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 75 | userId String 76 | role SpaceUserRole 77 | @@unique([userId, spaceId]) 78 | 79 | // require login 80 | @@deny('all', auth() == null) 81 | 82 | // space owner can add any one 83 | @@allow('create', space.owner == auth()) 84 | 85 | // space admin can add anyone but not himself 86 | @@allow('create', auth() != user && space.members?[user == auth() && role == ADMIN]) 87 | 88 | // space admin can update and delete 89 | @@allow('update,delete', space.members?[user == auth() && role == ADMIN]) 90 | 91 | // user can read entries for spaces which he's a member of 92 | @@allow('read', space.members?[user == auth()]) 93 | } 94 | 95 | /* 96 | * Model for a user 97 | */ 98 | model User { 99 | id String @id @default(uuid()) 100 | createdAt DateTime @default(now()) 101 | updatedAt DateTime @updatedAt 102 | email String @unique @email 103 | emailVerified DateTime? 104 | password String? @password @omit 105 | name String? 106 | ownedSpaces Space[] 107 | memberships SpaceUser[] 108 | image String? @url 109 | lists List[] 110 | todos Todo[] 111 | 112 | // next-auth 113 | accounts Account[] 114 | 115 | // can be created by anyone, even not logged in 116 | @@allow('create', true) 117 | 118 | // can be read by users sharing any space 119 | @@allow('read', memberships?[space.members?[user == auth()]]) 120 | 121 | // full access by oneself 122 | @@allow('all', auth() == this) 123 | } 124 | 125 | /* 126 | * Model for a Todo list 127 | */ 128 | model List { 129 | id String @id @default(uuid()) 130 | createdAt DateTime @default(now()) 131 | updatedAt DateTime @updatedAt 132 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 133 | spaceId String 134 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 135 | ownerId String @default(auth().id) 136 | title String @length(1, 100) 137 | private Boolean @default(false) 138 | todos Todo[] 139 | 140 | // require login 141 | @@deny('all', auth() == null) 142 | 143 | // can be read by owner or space members (only if not private) 144 | @@allow('read', owner == auth() || (space.members?[user == auth()] && !private)) 145 | 146 | // when create, owner must be set to current user, and user must be in the space 147 | @@allow('create', owner == auth() && space.members?[user == auth()]) 148 | 149 | // when create, owner must be set to current user, and user must be in the space 150 | // update is not allowed to change owner 151 | @@allow('update', owner == auth() && space.members?[user == auth()] && future().owner == owner) 152 | 153 | // can be deleted by owner 154 | @@allow('delete', owner == auth()) 155 | } 156 | 157 | /* 158 | * Model for a single Todo 159 | */ 160 | model Todo { 161 | id String @id @default(uuid()) 162 | createdAt DateTime @default(now()) 163 | updatedAt DateTime @updatedAt 164 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 165 | ownerId String @default(auth().id) 166 | list List @relation(fields: [listId], references: [id], onDelete: Cascade) 167 | listId String 168 | title String @length(1, 100) 169 | completedAt DateTime? 170 | 171 | // require login 172 | @@deny('all', auth() == null) 173 | 174 | // owner has full access, also space members have full access (if the parent List is not private) 175 | @@allow('all', list.owner == auth()) 176 | @@allow('all', list.space.members?[user == auth()] && !list.private) 177 | 178 | // update cannot change owner 179 | @@deny('update', future().owner != owner) 180 | } 181 | 182 | // next-auth 183 | model Account { 184 | id String @id @default(uuid()) 185 | userId String 186 | type String 187 | provider String 188 | providerAccountId String 189 | refresh_token String? 190 | refresh_token_expires_in Int? 191 | access_token String? 192 | expires_at Int? 193 | token_type String? 194 | scope String? 195 | id_token String? 196 | session_state String? 197 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 198 | @@unique([provider, providerAccountId]) 199 | } -------------------------------------------------------------------------------- /server/auth.ts: -------------------------------------------------------------------------------- 1 | import { PrismaAdapter } from '@auth/prisma-adapter'; 2 | import { PrismaClient, SpaceUserRole } from '@prisma/client'; 3 | import { compare } from 'bcryptjs'; 4 | import { nanoid } from 'nanoid'; 5 | import NextAuth, { User, type NextAuthConfig } 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 | const authOptions: NextAuthConfig = { 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: { type: 'email' }, 25 | password: { type: 'password' }, 26 | }, 27 | authorize: authorize(prisma), 28 | }), 29 | 30 | GitHubProvider, 31 | ], 32 | 33 | callbacks: { 34 | session({ session, token }) { 35 | return { 36 | ...session, 37 | user: { 38 | ...session.user, 39 | id: token.sub!, 40 | }, 41 | }; 42 | }, 43 | }, 44 | 45 | events: { 46 | async signIn({ user }: { user: User }) { 47 | if (!user.id) { 48 | return; 49 | } 50 | 51 | const spaceCount = await prisma.spaceUser.count({ 52 | where: { 53 | userId: user.id, 54 | }, 55 | }); 56 | if (spaceCount > 0) { 57 | return; 58 | } 59 | 60 | console.log(`User ${user.id} doesn't belong to any space. Creating one.`); 61 | const space = await prisma.space.create({ 62 | data: { 63 | name: `${user.name || user.email}'s space`, 64 | slug: nanoid(8), 65 | owner: { connect: { id: user.id } }, 66 | members: { 67 | create: [ 68 | { 69 | userId: user.id, 70 | role: SpaceUserRole.ADMIN, 71 | }, 72 | ], 73 | }, 74 | }, 75 | }); 76 | console.log(`Space created:`, space); 77 | }, 78 | }, 79 | }; 80 | 81 | function authorize(prisma: PrismaClient) { 82 | return async (credentials: Partial>) => { 83 | if (!credentials) { 84 | throw new Error('Missing credentials'); 85 | } 86 | 87 | if (typeof credentials.email !== 'string') { 88 | throw new Error('"email" is required in credentials'); 89 | } 90 | 91 | if (typeof credentials.password !== 'string') { 92 | throw new Error('"password" is required in credentials'); 93 | } 94 | 95 | const maybeUser = await prisma.user.findFirst({ 96 | where: { 97 | email: credentials.email, 98 | }, 99 | select: { 100 | id: true, 101 | email: true, 102 | password: true, 103 | }, 104 | }); 105 | 106 | if (!maybeUser || !maybeUser.password) { 107 | return null; 108 | } 109 | 110 | const isValid = await compare(credentials.password, maybeUser.password); 111 | 112 | if (!isValid) { 113 | return null; 114 | } 115 | 116 | return { 117 | id: maybeUser.id, 118 | email: maybeUser.email, 119 | }; 120 | }; 121 | } 122 | 123 | export const { handlers, signIn, signOut, auth } = NextAuth(authOptions); 124 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx,mdx}', 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [require('daisyui')], 12 | daisyui: { 13 | themes: ['light'], 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /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 | "@/*": ["./*"] 20 | }, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------