├── public
├── logo.png
├── auth-bg.jpg
└── avatar.jpg
├── pages
├── api
│ ├── .DS_Store
│ ├── model
│ │ └── [...path].ts
│ └── auth
│ │ └── [...nextauth].ts
├── index.tsx
├── _app.tsx
├── space
│ └── [slug]
│ │ ├── [listId]
│ │ └── index.tsx
│ │ └── index.tsx
├── create-space.tsx
├── signin.tsx
└── signup.tsx
├── .gitignore
├── .prettierrc
├── postcss.config.js
├── styles
└── globals.css
├── .env
├── prisma
├── migrations
│ ├── 20221126151510_refresh_token_expires
│ │ └── migration.sql
│ ├── migration_lock.toml
│ ├── 20221126151212_email_password_optional
│ │ └── migration.sql
│ ├── 20221127033222_email_required
│ │ └── migration.sql
│ ├── 20230306121228_update
│ │ └── migration.sql
│ ├── 20241222114017_add_space_owner
│ │ └── migration.sql
│ ├── 20221020094651_upate_cli
│ │ └── migration.sql
│ ├── 20221126150023_add_account
│ │ └── migration.sql
│ ├── 20221103144245_drop_account_session
│ │ └── migration.sql
│ ├── 20230905035233_drop_aux_fields
│ │ └── migration.sql
│ └── 20221014084317_init
│ │ └── migration.sql
└── schema.prisma
├── next-env.d.ts
├── .babelrc
├── tailwind.config.js
├── server
├── db.ts
├── auth.ts
└── enhanced-db.ts
├── .eslintrc.json
├── next.config.js
├── types
├── next-auth.d.ts
└── next.d.ts
├── .vscode
└── launch.json
├── components
├── WithNavBar.tsx
├── TimeInfo.tsx
├── Avatar.tsx
├── AuthGuard.tsx
├── BreadCrumb.tsx
├── Spaces.tsx
├── NavBar.tsx
├── SpaceMembers.tsx
├── Todo.tsx
├── TodoList.tsx
└── ManageMembers.tsx
├── lib
├── hooks
│ ├── index.ts
│ ├── todo.ts
│ ├── user.ts
│ ├── list.ts
│ ├── space.ts
│ ├── account.ts
│ ├── space-user.ts
│ └── __model_meta.ts
└── context.ts
├── .github
└── workflows
│ ├── build.yml
│ └── update.yml
├── tsconfig.json
├── LICENSE
├── README.md
├── package.json
└── schema.zmodel
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs/HEAD/public/logo.png
--------------------------------------------------------------------------------
/pages/api/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs/HEAD/pages/api/.DS_Store
--------------------------------------------------------------------------------
/public/auth-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs/HEAD/public/auth-bg.jpg
--------------------------------------------------------------------------------
/public/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenstackhq/sample-todo-nextjs/HEAD/public/avatar.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | *.log
3 | node_modules/
4 | .env.local
5 | .next
6 | .zenstack_repl_history
7 | .idea
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "useTabs": false,
4 | "printWidth": 120,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | @apply text-gray-800;
7 | }
8 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NEXTAUTH_SECRET=abc123
2 | DATABASE_URL="postgresql://postgres:abc123@localhost:5432/todo?schema=public"
3 | GITHUB_ID=
4 | GITHUB_SECRET=
5 |
--------------------------------------------------------------------------------
/prisma/migrations/20221126151510_refresh_token_expires/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Account" ADD COLUMN "refresh_token_expires_in" INTEGER;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (e.g., Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/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 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | // "superjson-next" plugin uses superjson for serialization between getServerSideProps and client,
4 | // so that types like Date and BigInt are properly handled
5 | "plugins": ["superjson-next"]
6 | }
7 |
--------------------------------------------------------------------------------
/pages/api/model/[...path].ts:
--------------------------------------------------------------------------------
1 | import { NextRequestHandler } from '@zenstackhq/server/next';
2 | import { getEnhancedPrisma } from 'server/enhanced-db';
3 |
4 | export default NextRequestHandler({
5 | getPrisma: (req, res) => getEnhancedPrisma({ req, res }),
6 | });
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [require('daisyui')],
8 | daisyui: {
9 | themes: ['light'],
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/server/auth.ts:
--------------------------------------------------------------------------------
1 | import { type GetServerSidePropsContext } from 'next';
2 | import { getServerSession } from 'next-auth';
3 | import { authOptions } from 'pages/api/auth/[...nextauth]';
4 |
5 | export const getServerAuthSession = async (ctx: {
6 | req: GetServerSidePropsContext['req'];
7 | res: GetServerSidePropsContext['res'];
8 | }) => {
9 | return getServerSession(ctx.req, ctx.res, authOptions);
10 | };
11 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | images: {
6 | remotePatterns: [
7 | { hostname: 'picsum.photos' },
8 | { hostname: 'lh3.googleusercontent.com' },
9 | { hostname: 'avatars.githubusercontent.com' },
10 | ],
11 | },
12 | };
13 |
14 | module.exports = nextConfig;
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/prisma/migrations/20241222114017_add_space_owner/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `ownerId` to the `Space` table without a default value. This is not possible if the table is not empty.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "Space" ADD COLUMN "ownerId" TEXT NOT NULL;
9 |
10 | -- AddForeignKey
11 | ALTER TABLE "Space" ADD CONSTRAINT "Space_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
12 |
--------------------------------------------------------------------------------
/server/enhanced-db.ts:
--------------------------------------------------------------------------------
1 | import { enhance } from '@zenstackhq/runtime';
2 | import type { GetServerSidePropsContext } from 'next';
3 | import { getServerAuthSession } from './auth';
4 | import { prisma } from './db';
5 |
6 | export async function getEnhancedPrisma(ctx: {
7 | req: GetServerSidePropsContext['req'];
8 | res: GetServerSidePropsContext['res'];
9 | }) {
10 | const session = await getServerAuthSession(ctx);
11 | return enhance(prisma, { user: session?.user });
12 | }
13 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/hooks/index.ts:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | * This file was generated by ZenStack CLI.
3 | ******************************************************************************/
4 |
5 | /* eslint-disable */
6 |
7 | export * from './space';
8 | export * from './space-user';
9 | export * from './user';
10 | export * from './list';
11 | export * from './todo';
12 | export * from './account';
13 | export { Provider } from '@zenstackhq/swr/runtime';
14 | export { default as metadata } from './__model_meta';
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 |
--------------------------------------------------------------------------------
/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/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 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/AuthGuard.tsx:
--------------------------------------------------------------------------------
1 | import { useSession } from 'next-auth/react';
2 | import { useRouter } from 'next/router';
3 |
4 | type Props = {
5 | children: JSX.Element | JSX.Element[];
6 | };
7 |
8 | export default function AuthGuard({ children }: Props) {
9 | const { status } = useSession();
10 | const router = useRouter();
11 |
12 | if (router.pathname === '/signup' || router.pathname === '/signin') {
13 | return <>{children}>;
14 | }
15 |
16 | if (status === 'loading') {
17 | return Loading...
;
18 | } else if (status === 'unauthenticated') {
19 | void router.push('/signin');
20 | return <>>;
21 | } else {
22 | return <>{children}>;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: CI
5 |
6 | env:
7 | DO_NOT_TRACK: '1'
8 |
9 | on:
10 | push:
11 | branches: ['main']
12 | pull_request:
13 | branches: ['main']
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Use Node.js 20.x
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: 20.x
25 | cache: 'npm'
26 | - run: npm ci
27 | - run: npm run build
28 |
--------------------------------------------------------------------------------
/lib/context.ts:
--------------------------------------------------------------------------------
1 | import { Space } from '@prisma/client';
2 | import { User } from 'next-auth';
3 | import { useSession } from 'next-auth/react';
4 | import { useRouter } from 'next/router';
5 | import { createContext } from 'react';
6 | import { useFindManySpace } from './hooks';
7 |
8 | export const UserContext = createContext(undefined);
9 |
10 | export function useCurrentUser() {
11 | const { data: session } = useSession();
12 | return session?.user;
13 | }
14 |
15 | export const SpaceContext = createContext(undefined);
16 |
17 | export function useCurrentSpace() {
18 | const router = useRouter();
19 | const { data: spaces } = useFindManySpace(
20 | {
21 | where: {
22 | slug: router.query.slug as string,
23 | },
24 | },
25 | {
26 | disabled: !router.query.slug,
27 | }
28 | );
29 |
30 | return spaces?.[0];
31 | }
32 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@lib/*": ["lib/*"],
20 | "@components/*": ["lib/components/*"],
21 | "@components": ["lib/components/index"]
22 | },
23 | "plugins": [
24 | {
25 | "name": "next"
26 | }
27 | ]
28 | },
29 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
30 | "exclude": ["node_modules"]
31 | }
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/update.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Update ZenStack
5 |
6 | env:
7 | DO_NOT_TRACK: '1'
8 |
9 | on:
10 | workflow_dispatch:
11 | repository_dispatch:
12 | types: [zenstack-release]
13 |
14 | jobs:
15 | update:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | - name: Update to latest ZenStack
21 | run: |
22 | git config --global user.name ymc9
23 | git config --global user.email yiming@whimslab.io
24 | npm ci
25 | npm run up
26 |
27 | - name: Build
28 | run: |
29 | npm run build
30 |
31 | - name: Commit and push
32 | run: |
33 | git add .
34 | git commit -m "chore: update to latest ZenStack" || true
35 | git push || true
36 |
--------------------------------------------------------------------------------
/components/BreadCrumb.tsx:
--------------------------------------------------------------------------------
1 | import { List, Space } from '@prisma/client';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 |
5 | type Props = {
6 | space: Space;
7 | list?: List;
8 | };
9 |
10 | export default function BreadCrumb({ space, list }: Props) {
11 | const router = useRouter();
12 |
13 | const parts = router.asPath.split('/').filter((p) => p);
14 | const [base] = parts;
15 | if (base !== 'space') {
16 | return <>>;
17 | }
18 |
19 | const items: Array<{ text: string; link: string }> = [];
20 |
21 | items.push({ text: 'Home', link: '/' });
22 | items.push({ text: space.name || '', link: `/space/${space.slug}` });
23 |
24 | if (list) {
25 | items.push({
26 | text: list?.title || '',
27 | link: `/space/${space.slug}/${list.id}`,
28 | });
29 | }
30 |
31 | return (
32 |
33 |
34 | {items.map((item, i) => (
35 |
36 | {item.text}
37 |
38 | ))}
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/components/Spaces.tsx:
--------------------------------------------------------------------------------
1 | import { useCountList } from '@lib/hooks';
2 | import { Space } from '@prisma/client';
3 | import Link from 'next/link';
4 |
5 | type Props = {
6 | spaces: Space[];
7 | };
8 |
9 | function SpaceItem({ space }: { space: Space }) {
10 | const { data: listCount } = useCountList({
11 | where: { spaceId: space.id },
12 | });
13 | return (
14 |
15 |
{listCount}
16 |
17 |
18 |
{space.name}
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default function Spaces({ spaces }: Props) {
26 | return (
27 |
28 | {spaces?.map((space) => (
29 |
33 |
34 |
35 | ))}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { useCurrentUser } from '@lib/context';
2 | import { Space } from '@prisma/client';
3 | import Spaces from 'components/Spaces';
4 | import WithNavBar from 'components/WithNavBar';
5 | import type { GetServerSideProps, NextPage } from 'next';
6 | import Link from 'next/link';
7 | import { getEnhancedPrisma } from 'server/enhanced-db';
8 |
9 | type Props = {
10 | spaces: Space[];
11 | };
12 |
13 | const Home: NextPage = ({ spaces }) => {
14 | const user = useCurrentUser();
15 |
16 | return (
17 |
18 | {user && (
19 |
20 |
Welcome {user.name || user.email}!
21 |
22 |
23 |
24 | Choose a space to start, or{' '}
25 |
26 | create a new one.
27 |
28 |
29 |
30 |
31 |
32 | )}
33 |
34 | );
35 | };
36 |
37 | export const getServerSideProps: GetServerSideProps = async (ctx) => {
38 | const db = await getEnhancedPrisma(ctx);
39 | const spaces = await db.space.findMany();
40 | return {
41 | props: { spaces },
42 | };
43 | };
44 |
45 | export default Home;
46 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { SpaceContext, useCurrentSpace, useCurrentUser, UserContext } from '@lib/context';
2 | import AuthGuard from 'components/AuthGuard';
3 | import { SessionProvider } from 'next-auth/react';
4 | import type { AppProps } from 'next/app';
5 | import { ToastContainer } from 'react-toastify';
6 | import 'react-toastify/dist/ReactToastify.css';
7 | import { Provider as ZenStackHooksProvider } from '../lib/hooks';
8 | import { Analytics } from '@vercel/analytics/react';
9 | import '../styles/globals.css';
10 |
11 | function AppContent(props: { children: JSX.Element | JSX.Element[] }) {
12 | const user = useCurrentUser();
13 | const space = useCurrentSpace();
14 |
15 | return (
16 |
17 |
18 |
19 | {props.children}
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
27 | return (
28 | <>
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | >
41 | );
42 | }
43 |
44 | export default MyApp;
45 |
--------------------------------------------------------------------------------
/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 |
23 |
24 | {space?.name || 'Welcome Todo App'}
25 |
26 |
Powered by ZenStack
27 |
28 |
29 |
30 |
31 |
32 | {user && }
33 |
34 |
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
11 |
12 | # A Collaborative Todo Sample - ZenStack + Next.js
13 |
14 | This project is a collaborative Todo app built with [Next.js](https://nextjs.org), [Next-Auth](nextauth.org), and [ZenStack](https://zenstack.dev).
15 |
16 | In this fictitious app, users can be invited to workspaces where they can collaborate on todos. Public todo lists are visible to all members in the workspace.
17 |
18 | See a live deployment at: https://zenstack-todo.vercel.app/.
19 |
20 | ## Features
21 |
22 | - User signup/signin
23 | - Creating workspaces and inviting members
24 | - Data segregation and permission control
25 |
26 | ## Implementation
27 |
28 | - Data model is located at `/schema.zmodel`.
29 | - An automatic CRUD API is mounted at `/api/model` by `pages/api/model/[...path].ts`.
30 | - [SWR](https://swr.vercel.app/) CRUD hooks are generated under `lib/hooks` folder.
31 |
32 | ## Running the sample
33 |
34 | 1. Setup a new PostgreSQL database
35 |
36 | You can launch a PostgreSQL instance locally, or create one from a hoster like [Supabase](https://supabase.com). Create a new database for this app, and set the connection string in .env file.
37 |
38 | 1. Install dependencies
39 |
40 | ```bash
41 | npm install
42 | ```
43 |
44 | 1. Generate server and client-side code from model
45 |
46 | ```bash
47 | npm run generate
48 | ```
49 |
50 | 1. Synchronize database schema
51 |
52 | ```bash
53 | npm run db:push
54 | ```
55 |
56 | 1. Start dev server
57 |
58 | ```bash
59 | npm run dev
60 | ```
61 |
62 | For more information on using ZenStack, visit [https://zenstack.dev](https://zenstack.dev).
63 |
--------------------------------------------------------------------------------
/components/SpaceMembers.tsx:
--------------------------------------------------------------------------------
1 | import { PlusIcon } from '@heroicons/react/24/outline';
2 | import { useCurrentSpace } from '@lib/context';
3 | import { useFindManySpaceUser } from '@lib/hooks';
4 | import { Space } from '@prisma/client';
5 | import Avatar from './Avatar';
6 | import ManageMembers from './ManageMembers';
7 |
8 | function ManagementDialog(space?: Space) {
9 | if (!space) return undefined;
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Manage Members of {space.name}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Close
28 |
29 |
30 |
31 |
32 | >
33 | );
34 | }
35 |
36 | export default function SpaceMembers() {
37 | const space = useCurrentSpace();
38 |
39 | const { data: members } = useFindManySpaceUser(
40 | {
41 | where: {
42 | spaceId: space?.id,
43 | },
44 | include: {
45 | user: true,
46 | },
47 | orderBy: {
48 | role: 'desc',
49 | },
50 | },
51 | { disabled: !space }
52 | );
53 |
54 | return (
55 |
56 | {ManagementDialog(space)}
57 | {members && (
58 |
59 | {members?.map((member) => (
60 |
61 | ))}
62 |
63 | )}
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/prisma/migrations/20230905035233_drop_aux_fields/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `zenstack_guard` on the `Account` table. All the data in the column will be lost.
5 | - You are about to drop the column `zenstack_guard` on the `List` table. All the data in the column will be lost.
6 | - You are about to drop the column `zenstack_transaction` on the `List` table. All the data in the column will be lost.
7 | - You are about to drop the column `zenstack_guard` on the `Space` table. All the data in the column will be lost.
8 | - You are about to drop the column `zenstack_transaction` on the `Space` table. All the data in the column will be lost.
9 | - You are about to drop the column `zenstack_guard` on the `SpaceUser` table. All the data in the column will be lost.
10 | - You are about to drop the column `zenstack_transaction` on the `SpaceUser` table. All the data in the column will be lost.
11 | - You are about to drop the column `zenstack_guard` on the `Todo` table. All the data in the column will be lost.
12 | - You are about to drop the column `zenstack_transaction` on the `Todo` table. All the data in the column will be lost.
13 | - You are about to drop the column `zenstack_guard` on the `User` table. All the data in the column will be lost.
14 | - You are about to drop the column `zenstack_transaction` on the `User` table. All the data in the column will be lost.
15 |
16 | */
17 | -- DropIndex
18 | DROP INDEX "List_zenstack_transaction_idx";
19 |
20 | -- DropIndex
21 | DROP INDEX "Space_zenstack_transaction_idx";
22 |
23 | -- DropIndex
24 | DROP INDEX "SpaceUser_zenstack_transaction_idx";
25 |
26 | -- DropIndex
27 | DROP INDEX "Todo_zenstack_transaction_idx";
28 |
29 | -- DropIndex
30 | DROP INDEX "User_zenstack_transaction_idx";
31 |
32 | -- AlterTable
33 | ALTER TABLE "Account" DROP COLUMN "zenstack_guard";
34 |
35 | -- AlterTable
36 | ALTER TABLE "List" DROP COLUMN "zenstack_guard",
37 | DROP COLUMN "zenstack_transaction";
38 |
39 | -- AlterTable
40 | ALTER TABLE "Space" DROP COLUMN "zenstack_guard",
41 | DROP COLUMN "zenstack_transaction";
42 |
43 | -- AlterTable
44 | ALTER TABLE "SpaceUser" DROP COLUMN "zenstack_guard",
45 | DROP COLUMN "zenstack_transaction";
46 |
47 | -- AlterTable
48 | ALTER TABLE "Todo" DROP COLUMN "zenstack_guard",
49 | DROP COLUMN "zenstack_transaction";
50 |
51 | -- AlterTable
52 | ALTER TABLE "User" DROP COLUMN "zenstack_guard",
53 | DROP COLUMN "zenstack_transaction";
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zenstack-todo-sample-nextjs",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "npm run generate && npm run lint && next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "db:push": "prisma db push",
11 | "db:migrate": "prisma migrate dev",
12 | "db:deploy": "prisma migrate deploy",
13 | "db:reset": "prisma migrate reset",
14 | "db:browse": "prisma studio",
15 | "generate": "zenstack generate",
16 | "vercel-build": "npm run build && npm run db:deploy",
17 | "package-clean": "npm rm zenstack @zenstackhq/runtime @zenstackhq/server @zenstackhq/swr",
18 | "up": "npm run package-clean && npm install -D --save-exact zenstack@latest @zenstackhq/swr@latest && npm install --save-exact @zenstackhq/runtime@latest @zenstackhq/server@latest",
19 | "up-preview": "npm run package-clean && npm install --registry https://preview.registry.zenstack.dev -D zenstack@latest @zenstackhq/swr@latest && npm install --registry https://preview.registry.zenstack.dev @zenstackhq/runtime@latest @zenstackhq/server@latest"
20 | },
21 | "dependencies": {
22 | "@heroicons/react": "^2.0.12",
23 | "@next-auth/prisma-adapter": "^1.0.6",
24 | "@prisma/client": "^6.1.0",
25 | "@vercel/analytics": "^1.0.1",
26 | "@zenstackhq/runtime": "2.22.1",
27 | "@zenstackhq/server": "2.22.1",
28 | "babel-plugin-superjson-next": "^0.4.5",
29 | "bcryptjs": "^2.4.3",
30 | "daisyui": "^4.4.10",
31 | "moment": "^2.29.4",
32 | "nanoid": "^4.0.0",
33 | "next": "^14.0.3",
34 | "next-auth": "^4.24.5",
35 | "react": "^18.2.0",
36 | "react-dom": "18.2.0",
37 | "react-toastify": "^9.0.8",
38 | "superjson": "^1.12.0",
39 | "swr": "^2.2.5"
40 | },
41 | "devDependencies": {
42 | "@tailwindcss/line-clamp": "^0.4.2",
43 | "@types/bcryptjs": "^2.4.2",
44 | "@types/node": "^18.0.0",
45 | "@types/react": "^18.2.22",
46 | "@types/react-dom": "18.0.6",
47 | "@typescript-eslint/eslint-plugin": "^6.13.1",
48 | "@typescript-eslint/parser": "^6.13.1",
49 | "@zenstackhq/swr": "2.22.1",
50 | "autoprefixer": "^10.4.12",
51 | "eslint": "^7.19.0",
52 | "eslint-config-next": "12.3.1",
53 | "postcss": "^8.4.16",
54 | "prisma": "^6.1.0",
55 | "tailwindcss": "^3.1.8",
56 | "typescript": "^5.1.6",
57 | "zenstack": "2.22.1"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/components/Todo.tsx:
--------------------------------------------------------------------------------
1 | import { TrashIcon } from '@heroicons/react/24/outline';
2 | import { useDeleteTodo, useUpdateTodo } from '@lib/hooks';
3 | import { Todo, User } from '@prisma/client';
4 | import { ChangeEvent } from 'react';
5 | import Avatar from './Avatar';
6 | import TimeInfo from './TimeInfo';
7 |
8 | type Props = {
9 | value: Todo & { owner: User };
10 | optimistic?: boolean;
11 | };
12 |
13 | export default function TodoComponent({ value, optimistic }: Props) {
14 | const { trigger: updateTodo } = useUpdateTodo({ optimisticUpdate: true });
15 | const { trigger: deleteTodo } = useDeleteTodo({ optimisticUpdate: true });
16 |
17 | const onDeleteTodo = () => {
18 | void deleteTodo({ where: { id: value.id } });
19 | };
20 |
21 | const toggleCompleted = (completed: boolean) => {
22 | if (completed === !!value.completedAt) {
23 | return;
24 | }
25 | void updateTodo({
26 | where: { id: value.id },
27 | data: { completedAt: completed ? new Date() : null },
28 | });
29 | };
30 |
31 | return (
32 |
33 |
34 |
39 | {value.title}
40 | {optimistic && }
41 |
42 |
43 | ) => toggleCompleted(e.currentTarget.checked)}
49 | />
50 | {
55 | !optimistic && onDeleteTodo();
56 | }}
57 | />
58 |
59 |
60 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/components/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import { LockClosedIcon, TrashIcon } from '@heroicons/react/24/outline';
2 | import { useCheckList, useDeleteList } from '@lib/hooks';
3 | import { List } from '@prisma/client';
4 | import { customAlphabet } from 'nanoid';
5 | import { User } from 'next-auth';
6 | import Image from 'next/image';
7 | import Link from 'next/link';
8 | import { useRouter } from 'next/router';
9 | import Avatar from './Avatar';
10 | import TimeInfo from './TimeInfo';
11 |
12 | type Props = {
13 | value: List & { owner: User };
14 | deleted?: (value: List) => void;
15 | };
16 |
17 | export default function TodoList({ value }: Props) {
18 | const router = useRouter();
19 |
20 | // check if the current user can delete the list (based on its owner)
21 | const { data: canDelete } = useCheckList({ operation: 'delete', where: { ownerId: value.ownerId } });
22 |
23 | const { trigger: deleteList } = useDeleteList();
24 |
25 | const onDeleteList = () => {
26 | if (confirm('Are you sure to delete this list?')) {
27 | void deleteList({ where: { id: value.id } });
28 | }
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
42 |
43 |
44 |
45 |
46 |
{value.title || 'Missing Title'}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | {value.private && (
55 |
56 |
57 |
58 | )}
59 |
60 | {canDelete && (
61 |
{
64 | onDeleteList();
65 | }}
66 | />
67 | )}
68 |
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | //////////////////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE //
3 | // This file is automatically generated by ZenStack CLI and should not be manually updated. //
4 | //////////////////////////////////////////////////////////////////////////////////////////////
5 |
6 | datasource db {
7 | provider = "postgresql"
8 | url = env("DATABASE_URL")
9 | }
10 |
11 | generator js {
12 | provider = "prisma-client-js"
13 | }
14 |
15 | enum SpaceUserRole {
16 | USER
17 | ADMIN
18 | }
19 |
20 | model Space {
21 | id String @id() @default(uuid())
22 | createdAt DateTime @default(now())
23 | updatedAt DateTime @updatedAt()
24 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
25 | ownerId String
26 | name String
27 | slug String @unique()
28 | members SpaceUser[]
29 | lists List[]
30 | }
31 |
32 | model SpaceUser {
33 | id String @id() @default(uuid())
34 | createdAt DateTime @default(now())
35 | updatedAt DateTime @updatedAt()
36 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
37 | spaceId String
38 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
39 | userId String
40 | role SpaceUserRole
41 |
42 | @@unique([userId, spaceId])
43 | }
44 |
45 | model User {
46 | id String @id() @default(uuid())
47 | createdAt DateTime @default(now())
48 | updatedAt DateTime @updatedAt()
49 | email String @unique()
50 | emailVerified DateTime?
51 | password String?
52 | name String?
53 | ownedSpaces Space[]
54 | memberships SpaceUser[]
55 | image String?
56 | lists List[]
57 | todos Todo[]
58 | accounts Account[]
59 | }
60 |
61 | model List {
62 | id String @id() @default(uuid())
63 | createdAt DateTime @default(now())
64 | updatedAt DateTime @updatedAt()
65 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
66 | spaceId String
67 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
68 | ownerId String
69 | title String
70 | private Boolean @default(false)
71 | todos Todo[]
72 | }
73 |
74 | model Todo {
75 | id String @id() @default(uuid())
76 | createdAt DateTime @default(now())
77 | updatedAt DateTime @updatedAt()
78 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
79 | ownerId String
80 | list List @relation(fields: [listId], references: [id], onDelete: Cascade)
81 | listId String
82 | title String
83 | completedAt DateTime?
84 | }
85 |
86 | model Account {
87 | id String @id() @default(uuid())
88 | userId String
89 | type String
90 | provider String
91 | providerAccountId String
92 | refresh_token String?
93 | refresh_token_expires_in Int?
94 | access_token String?
95 | expires_at Int?
96 | token_type String?
97 | scope String?
98 | id_token String?
99 | session_state String?
100 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
101 |
102 | @@unique([provider, providerAccountId])
103 | }
104 |
--------------------------------------------------------------------------------
/pages/space/[slug]/[listId]/index.tsx:
--------------------------------------------------------------------------------
1 | import { PlusIcon } from '@heroicons/react/24/outline';
2 | import { useCreateTodo, useFindManyTodo } from '@lib/hooks';
3 | import { List, Space } from '@prisma/client';
4 | import BreadCrumb from 'components/BreadCrumb';
5 | import TodoComponent from 'components/Todo';
6 | import WithNavBar from 'components/WithNavBar';
7 | import { GetServerSideProps } from 'next';
8 | import { ChangeEvent, KeyboardEvent, useState } from 'react';
9 | import { getEnhancedPrisma } from 'server/enhanced-db';
10 |
11 | type Props = {
12 | space: Space;
13 | list: List;
14 | };
15 |
16 | export default function TodoList(props: Props) {
17 | const [title, setTitle] = useState('');
18 | const { trigger: createTodo } = useCreateTodo({ optimisticUpdate: true });
19 |
20 | const { data: todos } = useFindManyTodo(
21 | {
22 | where: { listId: props.list.id },
23 | include: {
24 | owner: true,
25 | },
26 | orderBy: {
27 | createdAt: 'desc',
28 | },
29 | },
30 | { keepPreviousData: true }
31 | );
32 |
33 | const _createTodo = () => {
34 | void createTodo({
35 | data: {
36 | title,
37 | list: { connect: { id: props.list.id } },
38 | },
39 | });
40 | setTitle('');
41 | };
42 |
43 | if (!props.space || !props.list) {
44 | return <>>;
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
{props.list?.title}
54 |
73 |
74 |
75 | {todos?.map((todo) => (
76 |
77 | ))}
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | export const getServerSideProps: GetServerSideProps = async ({ req, res, params }) => {
85 | const db = await getEnhancedPrisma({ req, res });
86 | const space = await db.space.findUnique({
87 | where: { slug: params!.slug as string },
88 | });
89 | if (!space) {
90 | return {
91 | notFound: true,
92 | };
93 | }
94 |
95 | const list = await db.list.findUnique({
96 | where: { id: params!.listId as string },
97 | });
98 | if (!list) {
99 | return {
100 | notFound: true,
101 | };
102 | }
103 |
104 | return {
105 | props: { space, list },
106 | };
107 | };
108 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import { PrismaAdapter } from '@next-auth/prisma-adapter';
2 | import { PrismaClient, SpaceUserRole } from '@prisma/client';
3 | import { compare } from 'bcryptjs';
4 | import { nanoid } from 'nanoid';
5 | import NextAuth, { NextAuthOptions, User } from 'next-auth';
6 | import CredentialsProvider from 'next-auth/providers/credentials';
7 | import GitHubProvider from 'next-auth/providers/github';
8 | import { prisma } from 'server/db';
9 |
10 | export const authOptions: NextAuthOptions = {
11 | adapter: PrismaAdapter(prisma),
12 |
13 | session: {
14 | strategy: 'jwt',
15 | },
16 |
17 | pages: {
18 | signIn: '/signin',
19 | },
20 |
21 | providers: [
22 | CredentialsProvider({
23 | credentials: {
24 | email: {
25 | type: 'email',
26 | },
27 | password: {
28 | type: 'password',
29 | },
30 | },
31 | authorize: authorize(prisma),
32 | }),
33 |
34 | GitHubProvider({
35 | clientId: process.env.GITHUB_ID!,
36 | clientSecret: process.env.GITHUB_SECRET!,
37 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
38 | // @ts-ignore
39 | scope: 'read:user,user:email',
40 | }),
41 | ],
42 |
43 | callbacks: {
44 | session({ session, token }) {
45 | return {
46 | ...session,
47 | user: {
48 | ...session.user,
49 | id: token.sub!,
50 | },
51 | };
52 | },
53 | },
54 |
55 | events: {
56 | async signIn({ user }: { user: User }) {
57 | const spaceCount = await prisma.spaceUser.count({
58 | where: {
59 | userId: user.id,
60 | },
61 | });
62 | if (spaceCount > 0) {
63 | return;
64 | }
65 |
66 | console.log(`User ${user.id} doesn't belong to any space. Creating one.`);
67 | const space = await prisma.space.create({
68 | data: {
69 | name: `${user.name || user.email}'s space`,
70 | slug: nanoid(8),
71 | owner: { connect: { id: user.id } },
72 | members: {
73 | create: [
74 | {
75 | userId: user.id,
76 | role: SpaceUserRole.ADMIN,
77 | },
78 | ],
79 | },
80 | },
81 | });
82 | console.log(`Space created:`, space);
83 | },
84 | },
85 | };
86 |
87 | function authorize(prisma: PrismaClient) {
88 | return async (credentials: Record<'email' | 'password', string> | undefined) => {
89 | if (!credentials) {
90 | throw new Error('Missing credentials');
91 | }
92 |
93 | if (!credentials.email) {
94 | throw new Error('"email" is required in credentials');
95 | }
96 |
97 | if (!credentials.password) {
98 | throw new Error('"password" is required in credentials');
99 | }
100 |
101 | const maybeUser = await prisma.user.findFirst({
102 | where: {
103 | email: credentials.email,
104 | },
105 | select: {
106 | id: true,
107 | email: true,
108 | password: true,
109 | },
110 | });
111 |
112 | if (!maybeUser || !maybeUser.password) {
113 | return null;
114 | }
115 |
116 | const isValid = await compare(credentials.password, maybeUser.password);
117 |
118 | if (!isValid) {
119 | return null;
120 | }
121 |
122 | return {
123 | id: maybeUser.id,
124 | email: maybeUser.email,
125 | };
126 | };
127 | }
128 |
129 | export default NextAuth(authOptions);
130 |
--------------------------------------------------------------------------------
/pages/create-space.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3 | import { useCreateSpace } from '@lib/hooks';
4 | import { SpaceUserRole } from '@prisma/client';
5 | import WithNavBar from 'components/WithNavBar';
6 | import { NextPage } from 'next';
7 | import { useSession } from 'next-auth/react';
8 | import { useRouter } from 'next/router';
9 | import { FormEvent, useState } from 'react';
10 | import { toast } from 'react-toastify';
11 |
12 | const CreateSpace: NextPage = () => {
13 | const { data: session } = useSession();
14 | const [name, setName] = useState('');
15 | const [slug, setSlug] = useState('');
16 |
17 | const { trigger: createSpace } = useCreateSpace();
18 | const router = useRouter();
19 |
20 | const onSubmit = async (event: FormEvent) => {
21 | event.preventDefault();
22 | try {
23 | const space = await createSpace({
24 | data: {
25 | name,
26 | slug,
27 | members: {
28 | create: [
29 | {
30 | userId: session!.user.id,
31 | role: SpaceUserRole.ADMIN,
32 | },
33 | ],
34 | },
35 | },
36 | });
37 | console.log('Space created:', space);
38 | toast.success("Space created successfully! You'll be redirected.");
39 |
40 | setTimeout(() => {
41 | if (space) {
42 | void router.push(`/space/${space.slug}`);
43 | }
44 | }, 2000);
45 | } catch (err: any) {
46 | console.error(err);
47 | if (err.info?.prisma === true) {
48 | if (err.info.code === 'P2002') {
49 | toast.error('Space slug already in use');
50 | } else {
51 | toast.error(`Unexpected Prisma error: ${err.info.code}`);
52 | }
53 | } else {
54 | toast.error(JSON.stringify(err));
55 | }
56 | }
57 | };
58 |
59 | return (
60 |
61 |
113 |
114 | );
115 | };
116 |
117 | export default CreateSpace;
118 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ManageMembers.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2 | import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
3 | import { useCurrentUser } from '@lib/context';
4 | import { useCreateSpaceUser, useDeleteSpaceUser, useFindManySpaceUser } from '@lib/hooks';
5 | import { Space, SpaceUserRole } from '@prisma/client';
6 | import { ChangeEvent, KeyboardEvent, useState } from 'react';
7 | import { toast } from 'react-toastify';
8 | import Avatar from './Avatar';
9 |
10 | type Props = {
11 | space: Space;
12 | };
13 |
14 | export default function ManageMembers({ space }: Props) {
15 | const [email, setEmail] = useState('');
16 | const [role, setRole] = useState(SpaceUserRole.USER);
17 | const user = useCurrentUser();
18 | const { trigger: createSpaceUser } = useCreateSpaceUser();
19 | const { trigger: deleteSpaceUser } = useDeleteSpaceUser();
20 |
21 | const { data: members } = useFindManySpaceUser({
22 | where: {
23 | spaceId: space.id,
24 | },
25 | include: {
26 | user: true,
27 | },
28 | orderBy: {
29 | role: 'desc',
30 | },
31 | });
32 |
33 | const inviteUser = async () => {
34 | try {
35 | const r = await createSpaceUser({
36 | data: {
37 | user: {
38 | connect: {
39 | email,
40 | },
41 | },
42 | space: {
43 | connect: {
44 | id: space.id,
45 | },
46 | },
47 | role,
48 | },
49 | });
50 | console.log('SpaceUser created:', r);
51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
52 | } catch (err: any) {
53 | console.error(err);
54 | if (err.info?.prisma === true) {
55 | if (err.info.code === 'P2002') {
56 | toast.error('User is already a member of the space');
57 | } else if (err.info.code === 'P2025') {
58 | toast.error('User is not found for this email');
59 | } else {
60 | toast.error(`Unexpected Prisma error: ${err.info.code}`);
61 | }
62 | } else {
63 | toast.error(`Error occurred: ${JSON.stringify(err)}`);
64 | }
65 | }
66 | };
67 |
68 | const removeMember = (id: string) => {
69 | if (confirm(`Are you sure to remove this member from space?`)) {
70 | void deleteSpaceUser({ where: { id } });
71 | }
72 | };
73 |
74 | return (
75 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/pages/signin.tsx:
--------------------------------------------------------------------------------
1 | import { signIn } from 'next-auth/react';
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import { useRouter } from 'next/router';
5 | import { FormEvent, useEffect, useState } from 'react';
6 | import { toast } from 'react-toastify';
7 |
8 | export default function Signup() {
9 | const [email, setEmail] = useState('');
10 | const [password, setPassword] = useState('');
11 | const router = useRouter();
12 |
13 | useEffect(() => {
14 | if (router.query.error) {
15 | if (router.query.error === 'OAuthCreateAccount') {
16 | toast.error('Unable to signin. The user email may be already in use.');
17 | } else {
18 | toast.error(`Authentication error: ${router.query.error.toString()}`);
19 | }
20 | }
21 | }, [router]);
22 |
23 | async function onSignin(e: FormEvent) {
24 | e.preventDefault();
25 | const signInResult = await signIn('credentials', {
26 | redirect: false,
27 | email,
28 | password,
29 | });
30 | if (signInResult?.ok) {
31 | window.location.href = '/';
32 | } else {
33 | toast.error(`Signin failed. Please check your email and password.`);
34 | }
35 | }
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
Welcome to Todo
43 |
44 |
45 |
46 |
47 |
Sign in to your account
48 |
49 |
115 |
116 |
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/pages/signup.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2 | import { useCreateUser } from '@lib/hooks';
3 | import { signIn } from 'next-auth/react';
4 | import Image from 'next/image';
5 | import Link from 'next/link';
6 | import { FormEvent, useState } from 'react';
7 | import { toast } from 'react-toastify';
8 |
9 | export default function Signup() {
10 | const [email, setEmail] = useState('');
11 | const [password, setPassword] = useState('');
12 | const { trigger: createUser } = useCreateUser();
13 |
14 | async function onSignup(e: FormEvent) {
15 | e.preventDefault();
16 | try {
17 | await createUser({ data: { email, password } });
18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
19 | } catch (err: any) {
20 | console.error(err);
21 | if (err.info?.prisma === true) {
22 | if (err.info.code === 'P2002') {
23 | toast.error('User already exists');
24 | } else {
25 | toast.error(`Unexpected Prisma error: ${err.info.code}`);
26 | }
27 | } else {
28 | toast.error(`Error occurred: ${JSON.stringify(err)}`);
29 | }
30 | return;
31 | }
32 |
33 | const signInResult = await signIn('credentials', {
34 | redirect: false,
35 | email,
36 | password,
37 | });
38 | if (signInResult?.ok) {
39 | window.location.href = '/';
40 | } else {
41 | console.error('Signin failed:', signInResult?.error);
42 | }
43 | }
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
Welcome to Todo
51 |
52 |
53 |
54 |
55 |
Create a Free Account
56 |
115 |
116 |
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/schema.zmodel:
--------------------------------------------------------------------------------
1 | /*
2 | * Sample model for a collaborative Todo app
3 | */
4 |
5 | datasource db {
6 | provider = 'postgresql'
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | generator js {
11 | provider = 'prisma-client-js'
12 | }
13 |
14 | plugin enhancer {
15 | provider = '@core/enhancer'
16 | generatePermissionChecker = true
17 | }
18 |
19 | plugin hooks {
20 | provider = '@zenstackhq/swr'
21 | output = 'lib/hooks'
22 | }
23 |
24 | /**
25 | * Enum for user's role in a space
26 | */
27 | enum SpaceUserRole {
28 | USER
29 | ADMIN
30 | }
31 |
32 | /**
33 | * Model for a space in which users can collaborate on Lists and Todos
34 | */
35 | model Space {
36 | id String @id @default(uuid())
37 | createdAt DateTime @default(now())
38 | updatedAt DateTime @updatedAt
39 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
40 | ownerId String @default(auth().id)
41 | name String @length(4, 50)
42 | slug String @unique @regex('^[0-9a-zA-Z]{4,16}$')
43 | members SpaceUser[]
44 | lists List[]
45 |
46 | // require login
47 | @@deny('all', auth() == null)
48 |
49 | // everyone can create a space
50 | @@allow('create', true)
51 |
52 | // any user in the space can read the space
53 | @@allow('read', members?[user == auth()])
54 |
55 | // space admin can update and delete
56 | @@allow('update,delete', members?[user == auth() && role == ADMIN])
57 | }
58 |
59 | /**
60 | * Model representing membership of a user in a space
61 | */
62 | model SpaceUser {
63 | id String @id @default(uuid())
64 | createdAt DateTime @default(now())
65 | updatedAt DateTime @updatedAt
66 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
67 | spaceId String
68 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
69 | userId String
70 | role SpaceUserRole
71 | @@unique([userId, spaceId])
72 |
73 | // require login
74 | @@deny('all', auth() == null)
75 |
76 | // space owner can add any one
77 | @@allow('create', space.owner == auth())
78 |
79 | // space admin can add anyone but not himself
80 | @@allow('create', auth() != this.user && space.members?[user == auth() && role == ADMIN])
81 |
82 | // space admin can update/delete
83 | @@allow('update,delete', space.members?[user == auth() && role == ADMIN])
84 |
85 | // user can read entries for spaces which he's a member of
86 | @@allow('read', space.members?[user == auth()])
87 | }
88 |
89 | /**
90 | * Model for a user
91 | */
92 | model User {
93 | id String @id @default(uuid())
94 | createdAt DateTime @default(now())
95 | updatedAt DateTime @updatedAt
96 | email String @unique @email
97 | emailVerified DateTime?
98 | password String? @password @omit
99 | name String?
100 | ownedSpaces Space[]
101 | memberships SpaceUser[]
102 | image String? @url
103 | lists List[]
104 | todos Todo[]
105 |
106 | // next-auth
107 | accounts Account[]
108 |
109 | // can be created by anyone, even not logged in
110 | @@allow('create', true)
111 |
112 | // can be read by users sharing any space
113 | @@allow('read', memberships?[space.members?[user == auth()]])
114 |
115 | // full access by oneself
116 | @@allow('all', auth() == this)
117 | }
118 |
119 | abstract model BaseEntity {
120 | id String @id @default(uuid())
121 | createdAt DateTime @default(now())
122 | updatedAt DateTime @updatedAt
123 |
124 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
125 | spaceId String
126 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
127 | ownerId String @default(auth().id)
128 |
129 | // can be read by owner or space members
130 | @@allow('read', owner == auth() || (space.members?[user == auth()]))
131 |
132 | // when create, owner must be set to current user, and user must be in the space
133 | @@allow('create', owner == auth() && space.members?[user == auth()])
134 |
135 | // when create, owner must be set to current user, and user must be in the space
136 | // update is not allowed to change owner
137 | @@allow('update', owner == auth() && space.members?[user == auth()] && future().owner == owner)
138 |
139 | // can be deleted by owner
140 | @@allow('delete', owner == auth())
141 | }
142 |
143 | /**
144 | * Model for a Todo list
145 | */
146 | model List extends BaseEntity {
147 | title String @length(1, 100)
148 | private Boolean @default(false)
149 | todos Todo[]
150 |
151 | // can't be read by others if it's private
152 | @@deny('read', private == true && owner != auth())
153 | }
154 |
155 | /**
156 | * Model for a single Todo
157 | */
158 | model Todo {
159 | id String @id @default(uuid())
160 | createdAt DateTime @default(now())
161 | updatedAt DateTime @updatedAt
162 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
163 | ownerId String @default(auth().id)
164 | list List @relation(fields: [listId], references: [id], onDelete: Cascade)
165 | listId String
166 | title String @length(1, 100)
167 | completedAt DateTime?
168 |
169 | // full access if the parent list is readable
170 | @@allow('all', check(list, 'read'))
171 | }
172 |
173 | /**
174 | * Next-auth user account
175 | */
176 | model Account {
177 | id String @id @default(uuid())
178 | userId String
179 | type String
180 | provider String
181 | providerAccountId String
182 | refresh_token String?
183 | refresh_token_expires_in Int?
184 | access_token String?
185 | expires_at Int?
186 | token_type String?
187 | scope String?
188 | id_token String?
189 | session_state String?
190 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
191 | @@unique([provider, providerAccountId])
192 | }
--------------------------------------------------------------------------------
/pages/space/[slug]/index.tsx:
--------------------------------------------------------------------------------
1 | import { SpaceContext } from '@lib/context';
2 | import { useCreateList, useFindManyList } from '@lib/hooks';
3 | import { List, Space, User } from '@prisma/client';
4 | import BreadCrumb from 'components/BreadCrumb';
5 | import SpaceMembers from 'components/SpaceMembers';
6 | import TodoList from 'components/TodoList';
7 | import WithNavBar from 'components/WithNavBar';
8 | import { GetServerSideProps } from 'next';
9 | import { useRouter } from 'next/router';
10 | import { ChangeEvent, FormEvent, useContext, useEffect, useRef, useState } from 'react';
11 | import { toast } from 'react-toastify';
12 | import { getEnhancedPrisma } from 'server/enhanced-db';
13 |
14 | function CreateDialog() {
15 | const space = useContext(SpaceContext);
16 |
17 | const [modalOpen, setModalOpen] = useState(false);
18 | const [title, setTitle] = useState('');
19 | const [_private, setPrivate] = useState(false);
20 |
21 | const { trigger: createList } = useCreateList({
22 | onSuccess: () => {
23 | toast.success('List created successfully!');
24 |
25 | // reset states
26 | setTitle('');
27 | setPrivate(false);
28 |
29 | // close modal
30 | setModalOpen(false);
31 | },
32 | });
33 |
34 | const inputRef = useRef(null);
35 |
36 | useEffect(() => {
37 | if (modalOpen) {
38 | inputRef.current?.focus();
39 | }
40 | }, [modalOpen]);
41 |
42 | const onSubmit = (event: FormEvent) => {
43 | event.preventDefault();
44 |
45 | void createList({
46 | data: {
47 | title,
48 | private: _private,
49 | space: { connect: { id: space!.id } },
50 | },
51 | });
52 | };
53 |
54 | return (
55 | <>
56 | ) => {
62 | setModalOpen(e.currentTarget.checked);
63 | }}
64 | />
65 |
66 |
67 |
Create a Todo list
68 |
104 |
105 |
106 | >
107 | );
108 | }
109 |
110 | type Props = {
111 | space: Space;
112 | lists: (List & { owner: User })[];
113 | };
114 |
115 | export default function SpaceHome(props: Props) {
116 | const router = useRouter();
117 |
118 | const { data: lists } = useFindManyList(
119 | {
120 | where: {
121 | space: {
122 | slug: router.query.slug as string,
123 | },
124 | },
125 | include: {
126 | owner: true,
127 | },
128 | orderBy: {
129 | updatedAt: 'desc',
130 | },
131 | },
132 | {
133 | disabled: !router.query.slug,
134 | fallbackData: props.lists,
135 | }
136 | );
137 |
138 | return (
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | Create a list
147 |
148 |
149 |
150 |
151 |
152 | {lists?.map((list) => (
153 |
154 |
155 |
156 | ))}
157 |
158 |
159 |
160 |
161 |
162 | );
163 | }
164 |
165 | export const getServerSideProps: GetServerSideProps = async ({ req, res, params }) => {
166 | const db = await getEnhancedPrisma({ req, res });
167 |
168 | const space = await db.space.findUnique({
169 | where: { slug: params!.slug as string },
170 | });
171 | if (!space) {
172 | return {
173 | notFound: true,
174 | };
175 | }
176 |
177 | const lists = await db.list.findMany({
178 | where: {
179 | space: { slug: params?.slug as string },
180 | },
181 | include: {
182 | owner: true,
183 | },
184 | orderBy: {
185 | updatedAt: 'desc',
186 | },
187 | });
188 |
189 | return {
190 | props: { space, lists },
191 | };
192 | };
193 |
--------------------------------------------------------------------------------
/lib/hooks/todo.ts:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | * This file was generated by ZenStack CLI.
3 | ******************************************************************************/
4 |
5 | /* eslint-disable */
6 |
7 | import type { Prisma } from "@zenstackhq/runtime/models";
8 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime';
9 | import type { PolicyCrudKind } from '@zenstackhq/runtime'
10 | import metadata from './__model_meta';
11 | import * as request from '@zenstackhq/swr/runtime';
12 |
13 | export function useCreateTodo(options?: MutationOptions | undefined, unknown, Prisma.TodoCreateArgs>) {
14 | const mutation = request.useModelMutation('Todo', 'POST', 'create', metadata, options, true);
15 | return {
16 | ...mutation,
17 | trigger: (args: Prisma.SelectSubset) => {
18 | return mutation.trigger(args, options as any) as Promise | undefined>;
19 | }
20 | };
21 | }
22 |
23 | export function useCreateManyTodo(options?: MutationOptions) {
24 | const mutation = request.useModelMutation('Todo', 'POST', 'createMany', metadata, options, false);
25 | return {
26 | ...mutation,
27 | trigger: (args: Prisma.SelectSubset) => {
28 | return mutation.trigger(args, options as any) as Promise;
29 | }
30 | };
31 | }
32 |
33 | export function useFindManyTodo(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) {
34 | return request.useModelQuery('Todo', 'findMany', args, options);
35 | }
36 |
37 | export function useInfiniteFindManyTodo>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) {
38 | return request.useInfiniteModelQuery('Todo', 'findMany', getNextArgs, options);
39 | }
40 |
41 | export function useFindUniqueTodo(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) {
42 | return request.useModelQuery('Todo', 'findUnique', args, options);
43 | }
44 |
45 | export function useFindFirstTodo(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) {
46 | return request.useModelQuery('Todo', 'findFirst', args, options);
47 | }
48 |
49 | export function useUpdateTodo(options?: MutationOptions | undefined, unknown, Prisma.TodoUpdateArgs>) {
50 | const mutation = request.useModelMutation('Todo', 'PUT', 'update', metadata, options, true);
51 | return {
52 | ...mutation,
53 | trigger: (args: Prisma.SelectSubset) => {
54 | return mutation.trigger(args, options as any) as Promise | undefined>;
55 | }
56 | };
57 | }
58 |
59 | export function useUpdateManyTodo(options?: MutationOptions) {
60 | const mutation = request.useModelMutation('Todo', 'PUT', 'updateMany', metadata, options, false);
61 | return {
62 | ...mutation,
63 | trigger: (args: Prisma.SelectSubset) => {
64 | return mutation.trigger(args, options as any) as Promise;
65 | }
66 | };
67 | }
68 |
69 | export function useUpsertTodo(options?: MutationOptions | undefined, unknown, Prisma.TodoUpsertArgs>) {
70 | const mutation = request.useModelMutation('Todo', 'POST', 'upsert', metadata, options, true);
71 | return {
72 | ...mutation,
73 | trigger: (args: Prisma.SelectSubset) => {
74 | return mutation.trigger(args, options as any) as Promise | undefined>;
75 | }
76 | };
77 | }
78 |
79 | export function useDeleteTodo(options?: MutationOptions | undefined, unknown, Prisma.TodoDeleteArgs>) {
80 | const mutation = request.useModelMutation('Todo', 'DELETE', 'delete', metadata, options, true);
81 | return {
82 | ...mutation,
83 | trigger: (args: Prisma.SelectSubset) => {
84 | return mutation.trigger(args, options as any) as Promise | undefined>;
85 | }
86 | };
87 | }
88 |
89 | export function useDeleteManyTodo(options?: MutationOptions) {
90 | const mutation = request.useModelMutation('Todo', 'DELETE', 'deleteMany', metadata, options, false);
91 | return {
92 | ...mutation,
93 | trigger: (args: Prisma.SelectSubset) => {
94 | return mutation.trigger(args, options as any) as Promise;
95 | }
96 | };
97 | }
98 |
99 | export function useAggregateTodo(args?: Prisma.Subset, options?: QueryOptions>) {
100 | return request.useModelQuery('Todo', 'aggregate', args, options);
101 | }
102 |
103 | export function useGroupByTodo>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.TodoGroupByArgs['orderBy'] } : { orderBy?: Prisma.TodoGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True
104 | ? `Error: "by" must not be empty.`
105 | : HavingValid extends Prisma.False
106 | ? {
107 | [P in HavingFields]: P extends ByFields
108 | ? never
109 | : P extends string
110 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".`
111 | : [
112 | Error,
113 | 'Field ',
114 | P,
115 | ` in "having" needs to be provided in "by"`,
116 | ]
117 | }[HavingFields]
118 | : 'take' extends Prisma.Keys
119 | ? 'orderBy' extends Prisma.Keys
120 | ? ByValid extends Prisma.True
121 | ? {}
122 | : {
123 | [P in OrderFields]: P extends ByFields
124 | ? never
125 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
126 | }[OrderFields]
127 | : 'Error: If you provide "take", you also need to provide "orderBy"'
128 | : 'skip' extends Prisma.Keys
129 | ? 'orderBy' extends Prisma.Keys
130 | ? ByValid extends Prisma.True
131 | ? {}
132 | : {
133 | [P in OrderFields]: P extends ByFields
134 | ? never
135 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
136 | }[OrderFields]
137 | : 'Error: If you provide "skip", you also need to provide "orderBy"'
138 | : ByValid extends Prisma.True
139 | ? {}
140 | : {
141 | [P in OrderFields]: P extends ByFields
142 | ? never
143 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
144 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ?
145 | Array &
146 | {
147 | [P in ((keyof T) & (keyof Prisma.TodoGroupByOutputType))]: P extends '_count'
148 | ? T[P] extends boolean
149 | ? number
150 | : Prisma.GetScalarType
151 | : Prisma.GetScalarType
152 | }
153 | > : InputErrors>) {
154 | return request.useModelQuery('Todo', 'groupBy', args, options);
155 | }
156 |
157 | export function useCountTodo(args?: Prisma.Subset, options?: QueryOptions : number>) {
158 | return request.useModelQuery('Todo', 'count', args, options);
159 | }
160 |
161 | export function useCheckTodo(args: { operation: PolicyCrudKind; where?: { id?: string; ownerId?: string; listId?: string; title?: string }; }, options?: QueryOptions) {
162 | return request.useModelQuery('Todo', 'check', args, options);
163 | }
164 |
--------------------------------------------------------------------------------
/lib/hooks/user.ts:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | * This file was generated by ZenStack CLI.
3 | ******************************************************************************/
4 |
5 | /* eslint-disable */
6 |
7 | import type { Prisma } from "@zenstackhq/runtime/models";
8 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime';
9 | import type { PolicyCrudKind } from '@zenstackhq/runtime'
10 | import metadata from './__model_meta';
11 | import * as request from '@zenstackhq/swr/runtime';
12 |
13 | export function useCreateUser(options?: MutationOptions | undefined, unknown, Prisma.UserCreateArgs>) {
14 | const mutation = request.useModelMutation('User', 'POST', 'create', metadata, options, true);
15 | return {
16 | ...mutation,
17 | trigger: (args: Prisma.SelectSubset) => {
18 | return mutation.trigger(args, options as any) as Promise | undefined>;
19 | }
20 | };
21 | }
22 |
23 | export function useCreateManyUser(options?: MutationOptions) {
24 | const mutation = request.useModelMutation('User', 'POST', 'createMany', metadata, options, false);
25 | return {
26 | ...mutation,
27 | trigger: (args: Prisma.SelectSubset) => {
28 | return mutation.trigger(args, options as any) as Promise;
29 | }
30 | };
31 | }
32 |
33 | export function useFindManyUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) {
34 | return request.useModelQuery('User', 'findMany', args, options);
35 | }
36 |
37 | export function useInfiniteFindManyUser>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) {
38 | return request.useInfiniteModelQuery('User', 'findMany', getNextArgs, options);
39 | }
40 |
41 | export function useFindUniqueUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) {
42 | return request.useModelQuery('User', 'findUnique', args, options);
43 | }
44 |
45 | export function useFindFirstUser(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) {
46 | return request.useModelQuery('User', 'findFirst', args, options);
47 | }
48 |
49 | export function useUpdateUser(options?: MutationOptions | undefined, unknown, Prisma.UserUpdateArgs>) {
50 | const mutation = request.useModelMutation('User', 'PUT', 'update', metadata, options, true);
51 | return {
52 | ...mutation,
53 | trigger: (args: Prisma.SelectSubset) => {
54 | return mutation.trigger(args, options as any) as Promise | undefined>;
55 | }
56 | };
57 | }
58 |
59 | export function useUpdateManyUser(options?: MutationOptions) {
60 | const mutation = request.useModelMutation('User', 'PUT', 'updateMany', metadata, options, false);
61 | return {
62 | ...mutation,
63 | trigger: (args: Prisma.SelectSubset) => {
64 | return mutation.trigger(args, options as any) as Promise;
65 | }
66 | };
67 | }
68 |
69 | export function useUpsertUser(options?: MutationOptions | undefined, unknown, Prisma.UserUpsertArgs>) {
70 | const mutation = request.useModelMutation('User', 'POST', 'upsert', metadata, options, true);
71 | return {
72 | ...mutation,
73 | trigger: (args: Prisma.SelectSubset) => {
74 | return mutation.trigger(args, options as any) as Promise | undefined>;
75 | }
76 | };
77 | }
78 |
79 | export function useDeleteUser(options?: MutationOptions | undefined, unknown, Prisma.UserDeleteArgs>) {
80 | const mutation = request.useModelMutation('User', 'DELETE', 'delete', metadata, options, true);
81 | return {
82 | ...mutation,
83 | trigger: (args: Prisma.SelectSubset) => {
84 | return mutation.trigger(args, options as any) as Promise | undefined>;
85 | }
86 | };
87 | }
88 |
89 | export function useDeleteManyUser(options?: MutationOptions) {
90 | const mutation = request.useModelMutation('User', 'DELETE', 'deleteMany', metadata, options, false);
91 | return {
92 | ...mutation,
93 | trigger: (args: Prisma.SelectSubset) => {
94 | return mutation.trigger(args, options as any) as Promise;
95 | }
96 | };
97 | }
98 |
99 | export function useAggregateUser(args?: Prisma.Subset, options?: QueryOptions>) {
100 | return request.useModelQuery('User', 'aggregate', args, options);
101 | }
102 |
103 | export function useGroupByUser>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True
104 | ? `Error: "by" must not be empty.`
105 | : HavingValid extends Prisma.False
106 | ? {
107 | [P in HavingFields]: P extends ByFields
108 | ? never
109 | : P extends string
110 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".`
111 | : [
112 | Error,
113 | 'Field ',
114 | P,
115 | ` in "having" needs to be provided in "by"`,
116 | ]
117 | }[HavingFields]
118 | : 'take' extends Prisma.Keys
119 | ? 'orderBy' extends Prisma.Keys
120 | ? ByValid extends Prisma.True
121 | ? {}
122 | : {
123 | [P in OrderFields]: P extends ByFields
124 | ? never
125 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
126 | }[OrderFields]
127 | : 'Error: If you provide "take", you also need to provide "orderBy"'
128 | : 'skip' extends Prisma.Keys
129 | ? 'orderBy' extends Prisma.Keys
130 | ? ByValid extends Prisma.True
131 | ? {}
132 | : {
133 | [P in OrderFields]: P extends ByFields
134 | ? never
135 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
136 | }[OrderFields]
137 | : 'Error: If you provide "skip", you also need to provide "orderBy"'
138 | : ByValid extends Prisma.True
139 | ? {}
140 | : {
141 | [P in OrderFields]: P extends ByFields
142 | ? never
143 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
144 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ?
145 | Array &
146 | {
147 | [P in ((keyof T) & (keyof Prisma.UserGroupByOutputType))]: P extends '_count'
148 | ? T[P] extends boolean
149 | ? number
150 | : Prisma.GetScalarType
151 | : Prisma.GetScalarType
152 | }
153 | > : InputErrors>) {
154 | return request.useModelQuery('User', 'groupBy', args, options);
155 | }
156 |
157 | export function useCountUser(args?: Prisma.Subset, options?: QueryOptions : number>) {
158 | return request.useModelQuery('User', 'count', args, options);
159 | }
160 |
161 | export function useCheckUser(args: { operation: PolicyCrudKind; where?: { id?: string; email?: string; password?: string; name?: string; image?: string }; }, options?: QueryOptions) {
162 | return request.useModelQuery('User', 'check', args, options);
163 | }
164 |
--------------------------------------------------------------------------------
/lib/hooks/list.ts:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | * This file was generated by ZenStack CLI.
3 | ******************************************************************************/
4 |
5 | /* eslint-disable */
6 |
7 | import type { Prisma } from "@zenstackhq/runtime/models";
8 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime';
9 | import type { PolicyCrudKind } from '@zenstackhq/runtime'
10 | import metadata from './__model_meta';
11 | import * as request from '@zenstackhq/swr/runtime';
12 |
13 | export function useCreateList(options?: MutationOptions | undefined, unknown, Prisma.ListCreateArgs>) {
14 | const mutation = request.useModelMutation('List', 'POST', 'create', metadata, options, true);
15 | return {
16 | ...mutation,
17 | trigger: (args: Prisma.SelectSubset) => {
18 | return mutation.trigger(args, options as any) as Promise | undefined>;
19 | }
20 | };
21 | }
22 |
23 | export function useCreateManyList(options?: MutationOptions) {
24 | const mutation = request.useModelMutation('List', 'POST', 'createMany', metadata, options, false);
25 | return {
26 | ...mutation,
27 | trigger: (args: Prisma.SelectSubset) => {
28 | return mutation.trigger(args, options as any) as Promise;
29 | }
30 | };
31 | }
32 |
33 | export function useFindManyList(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) {
34 | return request.useModelQuery('List', 'findMany', args, options);
35 | }
36 |
37 | export function useInfiniteFindManyList>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) {
38 | return request.useInfiniteModelQuery('List', 'findMany', getNextArgs, options);
39 | }
40 |
41 | export function useFindUniqueList(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) {
42 | return request.useModelQuery('List', 'findUnique', args, options);
43 | }
44 |
45 | export function useFindFirstList(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) {
46 | return request.useModelQuery('List', 'findFirst', args, options);
47 | }
48 |
49 | export function useUpdateList(options?: MutationOptions | undefined, unknown, Prisma.ListUpdateArgs>) {
50 | const mutation = request.useModelMutation('List', 'PUT', 'update', metadata, options, true);
51 | return {
52 | ...mutation,
53 | trigger: (args: Prisma.SelectSubset) => {
54 | return mutation.trigger(args, options as any) as Promise | undefined>;
55 | }
56 | };
57 | }
58 |
59 | export function useUpdateManyList(options?: MutationOptions) {
60 | const mutation = request.useModelMutation('List', 'PUT', 'updateMany', metadata, options, false);
61 | return {
62 | ...mutation,
63 | trigger: (args: Prisma.SelectSubset) => {
64 | return mutation.trigger(args, options as any) as Promise;
65 | }
66 | };
67 | }
68 |
69 | export function useUpsertList(options?: MutationOptions | undefined, unknown, Prisma.ListUpsertArgs>) {
70 | const mutation = request.useModelMutation('List', 'POST', 'upsert', metadata, options, true);
71 | return {
72 | ...mutation,
73 | trigger: (args: Prisma.SelectSubset) => {
74 | return mutation.trigger(args, options as any) as Promise | undefined>;
75 | }
76 | };
77 | }
78 |
79 | export function useDeleteList(options?: MutationOptions | undefined, unknown, Prisma.ListDeleteArgs>) {
80 | const mutation = request.useModelMutation('List', 'DELETE', 'delete', metadata, options, true);
81 | return {
82 | ...mutation,
83 | trigger: (args: Prisma.SelectSubset) => {
84 | return mutation.trigger(args, options as any) as Promise | undefined>;
85 | }
86 | };
87 | }
88 |
89 | export function useDeleteManyList(options?: MutationOptions) {
90 | const mutation = request.useModelMutation('List', 'DELETE', 'deleteMany', metadata, options, false);
91 | return {
92 | ...mutation,
93 | trigger: (args: Prisma.SelectSubset) => {
94 | return mutation.trigger(args, options as any) as Promise;
95 | }
96 | };
97 | }
98 |
99 | export function useAggregateList(args?: Prisma.Subset, options?: QueryOptions>) {
100 | return request.useModelQuery('List', 'aggregate', args, options);
101 | }
102 |
103 | export function useGroupByList>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.ListGroupByArgs['orderBy'] } : { orderBy?: Prisma.ListGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True
104 | ? `Error: "by" must not be empty.`
105 | : HavingValid extends Prisma.False
106 | ? {
107 | [P in HavingFields]: P extends ByFields
108 | ? never
109 | : P extends string
110 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".`
111 | : [
112 | Error,
113 | 'Field ',
114 | P,
115 | ` in "having" needs to be provided in "by"`,
116 | ]
117 | }[HavingFields]
118 | : 'take' extends Prisma.Keys
119 | ? 'orderBy' extends Prisma.Keys
120 | ? ByValid extends Prisma.True
121 | ? {}
122 | : {
123 | [P in OrderFields]: P extends ByFields
124 | ? never
125 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
126 | }[OrderFields]
127 | : 'Error: If you provide "take", you also need to provide "orderBy"'
128 | : 'skip' extends Prisma.Keys
129 | ? 'orderBy' extends Prisma.Keys
130 | ? ByValid extends Prisma.True
131 | ? {}
132 | : {
133 | [P in OrderFields]: P extends ByFields
134 | ? never
135 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
136 | }[OrderFields]
137 | : 'Error: If you provide "skip", you also need to provide "orderBy"'
138 | : ByValid extends Prisma.True
139 | ? {}
140 | : {
141 | [P in OrderFields]: P extends ByFields
142 | ? never
143 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
144 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ?
145 | Array &
146 | {
147 | [P in ((keyof T) & (keyof Prisma.ListGroupByOutputType))]: P extends '_count'
148 | ? T[P] extends boolean
149 | ? number
150 | : Prisma.GetScalarType
151 | : Prisma.GetScalarType
152 | }
153 | > : InputErrors>) {
154 | return request.useModelQuery('List', 'groupBy', args, options);
155 | }
156 |
157 | export function useCountList(args?: Prisma.Subset, options?: QueryOptions : number>) {
158 | return request.useModelQuery('List', 'count', args, options);
159 | }
160 |
161 | export function useCheckList(args: { operation: PolicyCrudKind; where?: { id?: string; spaceId?: string; ownerId?: string; title?: string; private?: boolean }; }, options?: QueryOptions) {
162 | return request.useModelQuery('List', 'check', args, options);
163 | }
164 |
--------------------------------------------------------------------------------
/lib/hooks/space.ts:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | * This file was generated by ZenStack CLI.
3 | ******************************************************************************/
4 |
5 | /* eslint-disable */
6 |
7 | import type { Prisma } from "@zenstackhq/runtime/models";
8 | import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime';
9 | import type { PolicyCrudKind } from '@zenstackhq/runtime'
10 | import metadata from './__model_meta';
11 | import * as request from '@zenstackhq/swr/runtime';
12 |
13 | export function useCreateSpace(options?: MutationOptions | undefined, unknown, Prisma.SpaceCreateArgs>) {
14 | const mutation = request.useModelMutation('Space', 'POST', 'create', metadata, options, true);
15 | return {
16 | ...mutation,
17 | trigger: (args: Prisma.SelectSubset) => {
18 | return mutation.trigger(args, options as any) as Promise | undefined>;
19 | }
20 | };
21 | }
22 |
23 | export function useCreateManySpace(options?: MutationOptions) {
24 | const mutation = request.useModelMutation('Space', 'POST', 'createMany', metadata, options, false);
25 | return {
26 | ...mutation,
27 | trigger: (args: Prisma.SelectSubset) => {
28 | return mutation.trigger(args, options as any) as Promise;
29 | }
30 | };
31 | }
32 |
33 | export function useFindManySpace(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>>) {
34 | return request.useModelQuery('Space', 'findMany', args, options);
35 | }
36 |
37 | export function useInfiniteFindManySpace>>(getNextArgs: GetNextArgs | undefined, R>, options?: InfiniteQueryOptions>>) {
38 | return request.useInfiniteModelQuery('Space', 'findMany', getNextArgs, options);
39 | }
40 |
41 | export function useFindUniqueSpace(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) {
42 | return request.useModelQuery('Space', 'findUnique', args, options);
43 | }
44 |
45 | export function useFindFirstSpace(args?: Prisma.SelectSubset, options?: QueryOptions & { $optimistic?: boolean }>) {
46 | return request.useModelQuery('Space', 'findFirst', args, options);
47 | }
48 |
49 | export function useUpdateSpace(options?: MutationOptions | undefined, unknown, Prisma.SpaceUpdateArgs>) {
50 | const mutation = request.useModelMutation('Space', 'PUT', 'update', metadata, options, true);
51 | return {
52 | ...mutation,
53 | trigger: (args: Prisma.SelectSubset) => {
54 | return mutation.trigger(args, options as any) as Promise | undefined>;
55 | }
56 | };
57 | }
58 |
59 | export function useUpdateManySpace(options?: MutationOptions) {
60 | const mutation = request.useModelMutation('Space', 'PUT', 'updateMany', metadata, options, false);
61 | return {
62 | ...mutation,
63 | trigger: (args: Prisma.SelectSubset) => {
64 | return mutation.trigger(args, options as any) as Promise;
65 | }
66 | };
67 | }
68 |
69 | export function useUpsertSpace(options?: MutationOptions | undefined, unknown, Prisma.SpaceUpsertArgs>) {
70 | const mutation = request.useModelMutation('Space', 'POST', 'upsert', metadata, options, true);
71 | return {
72 | ...mutation,
73 | trigger: (args: Prisma.SelectSubset) => {
74 | return mutation.trigger(args, options as any) as Promise | undefined>;
75 | }
76 | };
77 | }
78 |
79 | export function useDeleteSpace(options?: MutationOptions | undefined, unknown, Prisma.SpaceDeleteArgs>) {
80 | const mutation = request.useModelMutation('Space', 'DELETE', 'delete', metadata, options, true);
81 | return {
82 | ...mutation,
83 | trigger: (args: Prisma.SelectSubset) => {
84 | return mutation.trigger(args, options as any) as Promise | undefined>;
85 | }
86 | };
87 | }
88 |
89 | export function useDeleteManySpace(options?: MutationOptions) {
90 | const mutation = request.useModelMutation('Space', 'DELETE', 'deleteMany', metadata, options, false);
91 | return {
92 | ...mutation,
93 | trigger: (args: Prisma.SelectSubset) => {
94 | return mutation.trigger(args, options as any) as Promise;
95 | }
96 | };
97 | }
98 |
99 | export function useAggregateSpace(args?: Prisma.Subset, options?: QueryOptions>) {
100 | return request.useModelQuery('Space', 'aggregate', args, options);
101 | }
102 |
103 | export function useGroupBySpace>, Prisma.Extends<'take', Prisma.Keys>>, OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.SpaceGroupByArgs['orderBy'] } : { orderBy?: Prisma.SpaceGroupByArgs['orderBy'] }, OrderFields extends Prisma.ExcludeUnderscoreKeys>>, ByFields extends Prisma.MaybeTupleToUnion, ByValid extends Prisma.Has, HavingFields extends Prisma.GetHavingFields, HavingValid extends Prisma.Has, ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, InputErrors extends ByEmpty extends Prisma.True
104 | ? `Error: "by" must not be empty.`
105 | : HavingValid extends Prisma.False
106 | ? {
107 | [P in HavingFields]: P extends ByFields
108 | ? never
109 | : P extends string
110 | ? `Error: Field "${P}" used in "having" needs to be provided in "by".`
111 | : [
112 | Error,
113 | 'Field ',
114 | P,
115 | ` in "having" needs to be provided in "by"`,
116 | ]
117 | }[HavingFields]
118 | : 'take' extends Prisma.Keys
119 | ? 'orderBy' extends Prisma.Keys
120 | ? ByValid extends Prisma.True
121 | ? {}
122 | : {
123 | [P in OrderFields]: P extends ByFields
124 | ? never
125 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
126 | }[OrderFields]
127 | : 'Error: If you provide "take", you also need to provide "orderBy"'
128 | : 'skip' extends Prisma.Keys
129 | ? 'orderBy' extends Prisma.Keys
130 | ? ByValid extends Prisma.True
131 | ? {}
132 | : {
133 | [P in OrderFields]: P extends ByFields
134 | ? never
135 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
136 | }[OrderFields]
137 | : 'Error: If you provide "skip", you also need to provide "orderBy"'
138 | : ByValid extends Prisma.True
139 | ? {}
140 | : {
141 | [P in OrderFields]: P extends ByFields
142 | ? never
143 | : `Error: Field "${P}" in "orderBy" needs to be provided in "by"`
144 | }[OrderFields]>(args?: Prisma.SubsetIntersection & InputErrors, options?: QueryOptions<{} extends InputErrors ?
145 | Array