From 11086d61a9f094248340cff2dcdc9bc772e4ba6a Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 13 Oct 2022 23:47:54 +0800 Subject: [PATCH 1/3] WIP --- package.json | 5 +- packages/runtime/hooks.d.ts | 9 + packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- .../schema/src/generator/next-auth/index.ts | 19 +- packages/schema/src/generator/prisma/index.ts | 4 +- .../schema/src/generator/react-hooks/index.ts | 9 +- samples/todo/components/AccessDenied.tsx | 21 --- samples/todo/components/AuthGuard.tsx | 16 ++ samples/todo/components/Avatar.tsx | 22 +++ samples/todo/components/LoginButton.tsx | 22 --- samples/todo/components/NavBar.tsx | 54 ++++++ samples/todo/components/Spaces.tsx | 28 +++ samples/todo/components/Todo.tsx | 9 + samples/todo/components/TodoList.tsx | 45 +++++ samples/todo/next.config.js | 11 +- samples/todo/package-lock.json | 141 ++++++++++++-- samples/todo/package.json | 12 +- samples/todo/pages/_app.tsx | 36 +++- samples/todo/pages/api/auth/[...nextauth].ts | 38 +++- samples/todo/pages/context.ts | 31 +++ samples/todo/pages/create-space.tsx | 117 ++++++++++++ samples/todo/pages/index.tsx | 177 ++++++++++-------- .../pages/space/[slug]/[listId]/index.tsx | 68 +++++++ samples/todo/pages/space/[slug]/index.tsx | 156 +++++++++++++++ samples/todo/public/avatar.jpg | Bin 0 -> 29019 bytes samples/todo/public/logo.png | Bin 0 -> 11466 bytes samples/todo/schema.zmodel | 92 ++++----- samples/todo/styles/globals.css | 4 + samples/todo/tailwind.config.js | 2 +- samples/todo/types/next-auth.d.ts | 2 +- 31 files changed, 943 insertions(+), 211 deletions(-) delete mode 100644 samples/todo/components/AccessDenied.tsx create mode 100644 samples/todo/components/AuthGuard.tsx create mode 100644 samples/todo/components/Avatar.tsx delete mode 100644 samples/todo/components/LoginButton.tsx create mode 100644 samples/todo/components/NavBar.tsx create mode 100644 samples/todo/components/Spaces.tsx create mode 100644 samples/todo/components/Todo.tsx create mode 100644 samples/todo/components/TodoList.tsx create mode 100644 samples/todo/pages/context.ts create mode 100644 samples/todo/pages/create-space.tsx create mode 100644 samples/todo/pages/space/[slug]/[listId]/index.tsx create mode 100644 samples/todo/pages/space/[slug]/index.tsx create mode 100644 samples/todo/public/avatar.jpg create mode 100644 samples/todo/public/logo.png diff --git a/package.json b/package.json index 603d0c2d8..bb134e6c5 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "version": "1.0.0", "description": "", "main": "index.js", - "scripts": {}, + "scripts": { + "build": "pnpm -r build", + "test": "pnpm -r test" + }, "keywords": [], "author": "", "license": "ISC" diff --git a/packages/runtime/hooks.d.ts b/packages/runtime/hooks.d.ts index 6394aede2..fe3eb2ecd 100644 --- a/packages/runtime/hooks.d.ts +++ b/packages/runtime/hooks.d.ts @@ -1 +1,10 @@ +import { ServerErrorCode } from '@zenstackhq/internal'; + export * from '.zenstack/lib/hooks'; +export type HooksError = { + status: number; + info: { + code: ServerErrorCode; + message: string; + }; +}; diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 7f8ae185d..613eb3636 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/runtime", - "version": "0.1.17", + "version": "0.1.18", "description": "ZenStack generated runtime code", "main": "index.js", "types": "index.d.ts", diff --git a/packages/schema/package.json b/packages/schema/package.json index 22f81d582..89fbaec52 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -2,7 +2,7 @@ "name": "zenstack", "displayName": "ZenStack CLI and Language Tools", "description": "ZenStack CLI and Language Tools", - "version": "0.1.22", + "version": "0.1.27", "engines": { "vscode": "^1.56.0" }, diff --git a/packages/schema/src/generator/next-auth/index.ts b/packages/schema/src/generator/next-auth/index.ts index e4947e3e1..77830073c 100644 --- a/packages/schema/src/generator/next-auth/index.ts +++ b/packages/schema/src/generator/next-auth/index.ts @@ -1,6 +1,7 @@ import { Context, Generator } from '../types'; import { Project } from 'ts-morph'; import * as path from 'path'; +import colors from 'colors'; export default class NextAuthGenerator implements Generator { async generate(context: Context) { @@ -11,6 +12,8 @@ export default class NextAuthGenerator implements Generator { this.generateAuthorize(project, context); await project.save(); + + console.log(colors.blue(` ✔️ Next-auth adapter generated`)); } generateIndex(project: Project, context: Context) { @@ -118,6 +121,10 @@ export default class NextAuthGenerator implements Generator { return async ( credentials: Record<'email' | 'password', string> | undefined ) => { + if (!credentials) { + throw new Error('Missing credentials'); + } + try { let maybeUser = await service.db.user.findFirst({ where: { @@ -132,14 +139,14 @@ export default class NextAuthGenerator implements Generator { }); if (!maybeUser) { - if (!credentials!.password || !credentials!.email) { + if (!credentials.password || !credentials.email) { throw new Error('Invalid Credentials'); } maybeUser = await service.db.user.create({ data: { - email: credentials!.email, - password: await hashPassword(credentials!.password), + email: credentials.email, + password: await hashPassword(credentials.password), }, select: { id: true, @@ -149,8 +156,12 @@ export default class NextAuthGenerator implements Generator { }, }); } else { + if (!maybeUser.password) { + throw new Error('Invalid User Record'); + } + const isValid = await verifyPassword( - credentials!.password, + credentials.password, maybeUser.password ); diff --git a/packages/schema/src/generator/prisma/index.ts b/packages/schema/src/generator/prisma/index.ts index 419fc90d3..fe2254677 100644 --- a/packages/schema/src/generator/prisma/index.ts +++ b/packages/schema/src/generator/prisma/index.ts @@ -15,7 +15,9 @@ export default class PrismaGenerator implements Generator { // generate prisma query guard await new QueryGuardGenerator(context).generate(); - console.log(colors.blue(` ✔️ Prisma schema and query code generated`)); + console.log( + colors.blue(` ✔️ Prisma schema and query guard generated`) + ); } async generatePrismaClient(schemaFile: string) { diff --git a/packages/schema/src/generator/react-hooks/index.ts b/packages/schema/src/generator/react-hooks/index.ts index 24fe01a89..5faa1e1b9 100644 --- a/packages/schema/src/generator/react-hooks/index.ts +++ b/packages/schema/src/generator/react-hooks/index.ts @@ -42,11 +42,11 @@ export default class ReactHooksGenerator implements Generator { }; function makeUrl(url: string, args: unknown) { - return args ? url + \`q=\${encodeURIComponent(JSON.stringify(args))}\` : url; + return args ? url + \`?q=\${encodeURIComponent(JSON.stringify(args))}\` : url; } - export function get(url: string, args?: unknown) { - return useSWR(makeUrl(url, args), fetcher); + export function get(url: string | null, args?: unknown) { + return useSWR(url && makeUrl(url, args), fetcher); } export async function post( @@ -179,7 +179,6 @@ export default class ReactHooksGenerator implements Generator { useFuncBody .addFunction({ name: 'get', - isAsync: true, typeParameters: [ `T extends P.Subset`, ], @@ -196,7 +195,7 @@ export default class ReactHooksGenerator implements Generator { }) .addBody() .addStatements([ - `return request.get>>(\`\${endpoint}/\${id}\`, args);`, + `return request.get>>(id ? \`\${endpoint}/\${id}\`: null, args);`, ]); // update diff --git a/samples/todo/components/AccessDenied.tsx b/samples/todo/components/AccessDenied.tsx deleted file mode 100644 index 02ec75ff2..000000000 --- a/samples/todo/components/AccessDenied.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { signIn } from 'next-auth/react'; -import Link from 'next/link'; - -export default function AccessDenied() { - return ( - <> -

Access Denied

-

- { - e.preventDefault(); - signIn(); - }} - > - You must be signed in to view this page - -

- - ); -} diff --git a/samples/todo/components/AuthGuard.tsx b/samples/todo/components/AuthGuard.tsx new file mode 100644 index 000000000..d056e0cff --- /dev/null +++ b/samples/todo/components/AuthGuard.tsx @@ -0,0 +1,16 @@ +import { signIn, useSession } from 'next-auth/react'; + +type Props = { + children: JSX.Element | JSX.Element[]; +}; + +export default function AuthGuard({ children }: Props) { + const { status } = useSession(); + if (status === 'loading') { + return

Loading...

; + } + if (status === 'unauthenticated') { + signIn(); + } + return <>{children}; +} diff --git a/samples/todo/components/Avatar.tsx b/samples/todo/components/Avatar.tsx new file mode 100644 index 000000000..ae8626957 --- /dev/null +++ b/samples/todo/components/Avatar.tsx @@ -0,0 +1,22 @@ +import { UserIcon } from '@heroicons/react/24/outline'; +import { User } from 'next-auth'; +import Image from 'next/image'; + +type Props = { + user: User; + size?: number; +}; + +export default function Avatar({ user, size }: Props) { + return ( +
+ avatar +
+ ); +} diff --git a/samples/todo/components/LoginButton.tsx b/samples/todo/components/LoginButton.tsx deleted file mode 100644 index f70de7283..000000000 --- a/samples/todo/components/LoginButton.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useSession, signIn, signOut } from 'next-auth/react'; - -export default function Component() { - const { data: session } = useSession(); - if (session) { - return ( - <> -
Signed in as {session.user?.email}
- - - ); - } - return ( - <> - - - ); -} diff --git a/samples/todo/components/NavBar.tsx b/samples/todo/components/NavBar.tsx new file mode 100644 index 000000000..d523859a1 --- /dev/null +++ b/samples/todo/components/NavBar.tsx @@ -0,0 +1,54 @@ +import Image from 'next/image'; +import Avatar from './Avatar'; +import { signOut } from 'next-auth/react'; +import Link from 'next/link'; +import { Space } from '@zenstackhq/runtime/types'; +import { User } from 'next-auth'; + +type Props = { + space: Space | undefined; + user: User | undefined; +}; + +export default function NavBar({ user, space }: Props) { + return ( +
+ +
+
+ + +
+
+
+ ); +} diff --git a/samples/todo/components/Spaces.tsx b/samples/todo/components/Spaces.tsx new file mode 100644 index 000000000..437058979 --- /dev/null +++ b/samples/todo/components/Spaces.tsx @@ -0,0 +1,28 @@ +import { useSpace } from '@zenstackhq/runtime/hooks'; +import Link from 'next/link'; + +export default function Spaces() { + const { find } = useSpace(); + const spaces = find(); + + return ( + + ); +} diff --git a/samples/todo/components/Todo.tsx b/samples/todo/components/Todo.tsx new file mode 100644 index 000000000..f416c626b --- /dev/null +++ b/samples/todo/components/Todo.tsx @@ -0,0 +1,9 @@ +import { Todo } from '@zenstackhq/runtime/types'; + +type Props = { + value: Todo; +}; + +export default function Component({ value }: Props) { + return
{value.title}
; +} diff --git a/samples/todo/components/TodoList.tsx b/samples/todo/components/TodoList.tsx new file mode 100644 index 000000000..83fc3c524 --- /dev/null +++ b/samples/todo/components/TodoList.tsx @@ -0,0 +1,45 @@ +import Image from 'next/image'; +import { List } from '@zenstackhq/runtime/types'; +import { customAlphabet } from 'nanoid'; +import { LockClosedIcon } from '@heroicons/react/24/outline'; +import { User } from 'next-auth'; +import Avatar from './Avatar'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +type Props = { + value: List & { owner: User }; +}; + +export default function TodoList({ value }: Props) { + const router = useRouter(); + return ( + + +
+ Cover +
+
+

+ {value.title || 'Missing Title'} +

+
+ + {value.private && ( +
+ +
+ )} +
+
+
+ + ); +} diff --git a/samples/todo/next.config.js b/samples/todo/next.config.js index ae887958d..f9cb6f102 100644 --- a/samples/todo/next.config.js +++ b/samples/todo/next.config.js @@ -1,7 +1,10 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, - swcMinify: true, -} + reactStrictMode: true, + swcMinify: true, + images: { + domains: ['lh3.googleusercontent.com', 'picsum.photos'], + }, +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/samples/todo/package-lock.json b/samples/todo/package-lock.json index ee1b16bd6..465c9ee41 100644 --- a/samples/todo/package-lock.json +++ b/samples/todo/package-lock.json @@ -8,16 +8,20 @@ "name": "todo", "version": "0.1.0", "dependencies": { + "@heroicons/react": "^2.0.12", "@prisma/client": "^4.4.0", "@zenstackhq/runtime": "latest", "daisyui": "^2.31.0", + "nanoid": "^4.0.0", "next": "12.3.1", "next-auth": "^4.10.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-toastify": "^9.0.8", "swr": "^1.3.0" }, "devDependencies": { + "@tailwindcss/line-clamp": "^0.4.2", "@types/node": "^14.17.3", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", @@ -189,6 +193,14 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/@heroicons/react": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.12.tgz", + "integrity": "sha512-FZxKh3i9aKIDxyALTgIpSF2t6V6/eZfF5mRu41QlwkX3Oxzecdm1u6dpft6PQGxIBwO7TKYWaMAYYL8mp/EaOg==", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -504,6 +516,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@tailwindcss/line-clamp": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz", + "integrity": "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==", + "dev": true, + "peerDependencies": { + "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" + } + }, "node_modules/@ts-morph/common": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.17.0.tgz", @@ -712,9 +733,9 @@ } }, "node_modules/@zenstackhq/runtime": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-0.1.17.tgz", - "integrity": "sha512-7wzHGJYjWsvH2njYruZVLrWk6TpLZKYOEjzS/MAxoN0HdIHaQ7DISWfgWQdmSBaWSQoR1qvA1A7A+AhxvahZnA==", + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-0.1.18.tgz", + "integrity": "sha512-W6F0wkNb7kOyL8DRGXSEWkzdwUJEQIdWkvjSqh0Itdk+OAY66lDMKhKfbEmmlDxhurkoOJ4YQK6Qxj6UA+AjkQ==", "peerDependencies": { "@types/bcryptjs": "^2.4.2", "@zenstackhq/internal": "^0.1.0", @@ -1180,6 +1201,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/code-block-writer": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", @@ -2870,14 +2899,14 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz", + "integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^14 || ^16 || >=18" } }, "node_modules/natural-compare": { @@ -2968,6 +2997,17 @@ } } }, + "node_modules/next/node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -3413,6 +3453,17 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/preact": { "version": "10.11.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.1.tgz", @@ -3561,6 +3612,18 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-toastify": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.8.tgz", + "integrity": "sha512-EwM+teWt49HSHx+67qI08yLAW1zAsBxCXLCsUfxHYv1W7/R3ZLhrqKalh7j+kjgPna1h5LQMSMwns4tB4ww2yQ==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4437,9 +4500,9 @@ } }, "node_modules/zenstack": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.1.21.tgz", - "integrity": "sha512-uhCnyuhUY4XzYz7zeiidq/vIdAAF354t2Nc1YGxX+uvON781w26IUxy2VOmG02P7W3lLBL8zNfE6hNLpqRbzHA==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.1.27.tgz", + "integrity": "sha512-pkNh8RB6Ir7KaLeXOZj+0dwgoExfD/RtYsSxVmXi+0hZamAQe09c6Zpj4YWc72aditNpW3exA/CSvXzsv3paog==", "dev": true, "dependencies": { "@zenstackhq/internal": "0.1.6", @@ -4596,6 +4659,12 @@ "strip-json-comments": "^3.1.1" } }, + "@heroicons/react": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.12.tgz", + "integrity": "sha512-FZxKh3i9aKIDxyALTgIpSF2t6V6/eZfF5mRu41QlwkX3Oxzecdm1u6dpft6PQGxIBwO7TKYWaMAYYL8mp/EaOg==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -4766,6 +4835,13 @@ "tslib": "^2.4.0" } }, + "@tailwindcss/line-clamp": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz", + "integrity": "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==", + "dev": true, + "requires": {} + }, "@ts-morph/common": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.17.0.tgz", @@ -4919,9 +4995,9 @@ } }, "@zenstackhq/runtime": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-0.1.17.tgz", - "integrity": "sha512-7wzHGJYjWsvH2njYruZVLrWk6TpLZKYOEjzS/MAxoN0HdIHaQ7DISWfgWQdmSBaWSQoR1qvA1A7A+AhxvahZnA==", + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-0.1.18.tgz", + "integrity": "sha512-W6F0wkNb7kOyL8DRGXSEWkzdwUJEQIdWkvjSqh0Itdk+OAY66lDMKhKfbEmmlDxhurkoOJ4YQK6Qxj6UA+AjkQ==", "requires": {} }, "acorn": { @@ -5246,6 +5322,11 @@ "readdirp": "~3.6.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "code-block-writer": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", @@ -6520,9 +6601,9 @@ "dev": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz", + "integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==" }, "natural-compare": { "version": "1.4.0", @@ -6556,6 +6637,11 @@ "use-sync-external-store": "1.2.0" }, "dependencies": { + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + }, "postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -6822,6 +6908,13 @@ "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" + }, + "dependencies": { + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + } } }, "postcss-import": { @@ -6971,6 +7064,14 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "react-toastify": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.8.tgz", + "integrity": "sha512-EwM+teWt49HSHx+67qI08yLAW1zAsBxCXLCsUfxHYv1W7/R3ZLhrqKalh7j+kjgPna1h5LQMSMwns4tB4ww2yQ==", + "requires": { + "clsx": "^1.1.1" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -7623,9 +7724,9 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "zenstack": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.1.21.tgz", - "integrity": "sha512-uhCnyuhUY4XzYz7zeiidq/vIdAAF354t2Nc1YGxX+uvON781w26IUxy2VOmG02P7W3lLBL8zNfE6hNLpqRbzHA==", + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.1.27.tgz", + "integrity": "sha512-pkNh8RB6Ir7KaLeXOZj+0dwgoExfD/RtYsSxVmXi+0hZamAQe09c6Zpj4YWc72aditNpW3exA/CSvXzsv3paog==", "dev": true, "requires": { "@zenstackhq/internal": "0.1.6", diff --git a/samples/todo/package.json b/samples/todo/package.json index f417bab70..bc301b8a9 100644 --- a/samples/todo/package.json +++ b/samples/todo/package.json @@ -7,23 +7,27 @@ "build": "next build", "start": "next start", "lint": "next lint", - "db-gen": "prisma generate --schema .zenstack/schema.prisma", - "db-push": "prisma db push --schema .zenstack/schema.prisma", - "db-migrate": "prisma migrate dev --schema .zenstack/schema.prisma", - "db-reset": "prisma migrate reset --schema .zenstack/schema.prisma", + "db-gen": "prisma generate --schema node_modules/.zenstack/schema.prisma", + "db-push": "prisma db push --schema node_modules/.zenstack/schema.prisma", + "db-migrate": "prisma migrate dev --schema node_modules/.zenstack/schema.prisma", + "db-reset": "prisma migrate reset --schema node_modules/.zenstack/schema.prisma", "generate": "zenstack generate ./schema.zmodel" }, "dependencies": { + "@heroicons/react": "^2.0.12", "@prisma/client": "^4.4.0", "@zenstackhq/runtime": "latest", "daisyui": "^2.31.0", + "nanoid": "^4.0.0", "next": "12.3.1", "next-auth": "^4.10.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-toastify": "^9.0.8", "swr": "^1.3.0" }, "devDependencies": { + "@tailwindcss/line-clamp": "^0.4.2", "@types/node": "^14.17.3", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", diff --git a/samples/todo/pages/_app.tsx b/samples/todo/pages/_app.tsx index 505acdf46..8a02e1ce0 100644 --- a/samples/todo/pages/_app.tsx +++ b/samples/todo/pages/_app.tsx @@ -1,11 +1,45 @@ import '../styles/globals.css'; import type { AppProps } from 'next/app'; import { SessionProvider } from 'next-auth/react'; +import 'react-toastify/dist/ReactToastify.css'; +import { ToastContainer } from 'react-toastify'; +import NavBar from 'components/NavBar'; +import { + SpaceContext, + useCurrentSpace, + useCurrentUser, + UserContext, +} from './context'; + +function AppContent(props: { children: JSX.Element | JSX.Element[] }) { + const user = useCurrentUser(); + const space = useCurrentSpace(); + + return ( + + +
+ + {props.children} +
+
+
+ ); +} function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { return ( - + +
+ + +
+
); } diff --git a/samples/todo/pages/api/auth/[...nextauth].ts b/samples/todo/pages/api/auth/[...nextauth].ts index b0df63407..c6c632b57 100644 --- a/samples/todo/pages/api/auth/[...nextauth].ts +++ b/samples/todo/pages/api/auth/[...nextauth].ts @@ -1,4 +1,4 @@ -import NextAuth, { NextAuthOptions } from 'next-auth'; +import NextAuth, { NextAuthOptions, User } from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import GoogleProvider from 'next-auth/providers/google'; import { @@ -6,6 +6,9 @@ import { NextAuthAdapter as Adapter, } from '@zenstackhq/runtime/auth'; import service from '@zenstackhq/runtime'; +import { nanoid } from 'nanoid'; +import { SpaceUserRole } from '@zenstackhq/runtime/types'; +import { signIn } from 'next-auth/react'; export const authOptions: NextAuthOptions = { // Configure one or more authentication providers @@ -21,6 +24,7 @@ export const authOptions: NextAuthOptions = { clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET!, }), + CredentialsProvider({ credentials: { email: { @@ -49,6 +53,38 @@ export const authOptions: NextAuthOptions = { }; }, }, + + events: { + async signIn({ user }: { user: User }) { + const spaceCount = await service.db.spaceUser.count({ + where: { + userId: user.id, + }, + }); + if (spaceCount > 0) { + return; + } + + console.log( + `User ${user.id} doesn't belong to any space. Creating one.` + ); + const space = await service.db.space.create({ + data: { + name: `${user.name || user.email}'s space`, + slug: nanoid(8), + members: { + create: [ + { + userId: user.id, + role: SpaceUserRole.ADMIN, + }, + ], + }, + }, + }); + console.log(`Space created:`, space); + }, + }, }; export default NextAuth(authOptions); diff --git a/samples/todo/pages/context.ts b/samples/todo/pages/context.ts new file mode 100644 index 000000000..0fa24e751 --- /dev/null +++ b/samples/todo/pages/context.ts @@ -0,0 +1,31 @@ +import { useSpace } from '@zenstackhq/runtime/hooks'; +import { Space } from '@zenstackhq/runtime/types'; +import { User } from 'next-auth'; +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/router'; +import { createContext } from 'react'; + +export const UserContext = createContext(undefined); + +export function useCurrentUser() { + const { data: session } = useSession(); + return session?.user; +} + +export const SpaceContext = createContext(undefined); + +export function useCurrentSpace() { + const router = useRouter(); + const { find } = useSpace(); + const spaces = find({ + where: { + slug: router.query.slug as string, + }, + }); + + if (!router.query.slug) { + return undefined; + } + + return spaces.data?.[0]; +} diff --git a/samples/todo/pages/create-space.tsx b/samples/todo/pages/create-space.tsx new file mode 100644 index 000000000..b8b2023e2 --- /dev/null +++ b/samples/todo/pages/create-space.tsx @@ -0,0 +1,117 @@ +import { NextPage } from 'next'; +import { FormEvent, useState } from 'react'; +import { useSpace, type HooksError } from '@zenstackhq/runtime/hooks'; +import AuthGuard from 'components/AuthGuard'; +import { ServerErrorCode } from '@zenstackhq/runtime/server'; +import { toast } from 'react-toastify'; +import { useRouter } from 'next/router'; +import { useSession } from 'next-auth/react'; +import { SpaceUserRole } from '@zenstackhq/runtime/types'; + +const CreateSpace: NextPage = () => { + const { data: session } = useSession(); + const [name, setName] = useState(''); + const [slug, setSlug] = useState(''); + + const { create } = useSpace(); + const router = useRouter(); + + const onSubmit = async (event: FormEvent) => { + event.preventDefault(); + try { + const space = await create({ + data: { + name, + slug, + members: { + create: [ + { + userId: session!.user.id, + role: SpaceUserRole.ADMIN, + }, + ], + }, + }, + }); + console.log('Space created:', space); + toast.success("Space created successfull! You'll be redirected."); + + setTimeout(() => { + router.push(`/space/${space.slug}`); + }, 2000); + } catch (err) { + console.error(err); + if ( + (err as HooksError).info?.code === + ServerErrorCode.UNIQUE_CONSTRAINT_VIOLATION + ) { + toast.error('Space slug alread in use'); + } else { + toast.error(`Error occurred: ${err}`); + } + } + }; + + return ( + +
+
+

Create a space

+
+
+ + ) => + setName(e.currentTarget.value) + } + /> +
+
+ + ) => + setSlug(e.currentTarget.value) + } + /> +
+
+ +
+ 20 || + !slug.match(/^[0-9a-zA-Z]{4,16}$/) + } + value="Create" + className="btn btn-primary px-8" + /> + +
+
+
+
+ ); +}; + +export default CreateSpace; diff --git a/samples/todo/pages/index.tsx b/samples/todo/pages/index.tsx index b290e3fae..883d36122 100644 --- a/samples/todo/pages/index.tsx +++ b/samples/todo/pages/index.tsx @@ -1,97 +1,116 @@ import type { NextPage } from 'next'; -import LoginButton from '../components/LoginButton'; -import { useSession } from 'next-auth/react'; +import { useSession, signIn } from 'next-auth/react'; import { useTodoCollection } from '@zenstackhq/runtime/hooks'; import { TodoCollection } from '@zenstackhq/runtime/types'; +import Spaces from 'components/Spaces'; +import Link from 'next/link'; const Home: NextPage = () => { - const { data: session } = useSession(); - const { - create: createTodoCollection, - find: findTodoCollection, - del: deleteTodoCollection, - } = useTodoCollection(); - const { data: todoCollections } = findTodoCollection(); + const { data: session, status: sessionStatus } = useSession(); - async function onCreateTodoCollection() { - await createTodoCollection({ - data: { - title: 'My Todo Collection', - ownerId: session!.user.id, - spaceId: 'f0c9fc5c-e6e5-4146-a540-214f6ac5701c', - }, - }); + if (sessionStatus === 'unauthenticated') { + // kick back to signin + signIn(); } - async function onCreateFilledTodoCollection() { - await createTodoCollection({ - data: { - title: 'My Todo Collection', - ownerId: session!.user.id, - spaceId: 'f0c9fc5c-e6e5-4146-a540-214f6ac5701c', - todos: { - create: [ - { title: 'First Todo', ownerId: session!.user.id }, - ], - }, - }, - }); - } + // const { + // create: createTodoCollection, + // find: findTodoCollection, + // del: deleteTodoCollection, + // } = useTodoCollection(); - async function onDeleteTodoCollection(todoList: TodoCollection) { - await deleteTodoCollection(todoList.id); - } + // const { data: todoCollections } = findTodoCollection(); + + // async function onCreateTodoCollection() { + // await createTodoCollection({ + // data: { + // title: 'My Todo Collection', + // ownerId: session!.user.id, + // spaceId: 'f0c9fc5c-e6e5-4146-a540-214f6ac5701c', + // }, + // }); + // } + + // async function onCreateFilledTodoCollection() { + // await createTodoCollection({ + // data: { + // title: 'My Todo Collection', + // ownerId: session!.user.id, + // spaceId: 'f0c9fc5c-e6e5-4146-a540-214f6ac5701c', + // todos: { + // create: [ + // { title: 'First Todo', ownerId: session!.user.id }, + // ], + // }, + // }, + // }); + // } + + // async function onDeleteTodoCollection(todoList: TodoCollection) { + // await deleteTodoCollection(todoList.id); + // } - function renderTodoCollections() { - return ( - <> -
    - {todoCollections?.map((collection) => ( -
  • -

    {collection.title}

    - -
  • - ))} -
- - ); + // function renderTodoCollections() { + // return ( + // <> + //
    + // {todoCollections?.map((collection) => ( + //
  • + //

    {collection.title}

    + // + //
  • + // ))} + //
+ // + // ); + // } + + if (!session) { + return
Loading ...
; } return ( -
-

Wonderful Todo

-
- -
+ <> +
+

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

+
+

+ Choose a space to start, or{' '} + + + create a new one. + + +

+ +
+ {/* - {session && ( - <> - + - - -

Todo Lists

- {renderTodoCollections()} - - )} -
+

Todo Lists

+ {renderTodoCollections()} */} +
+ ); }; diff --git a/samples/todo/pages/space/[slug]/[listId]/index.tsx b/samples/todo/pages/space/[slug]/[listId]/index.tsx new file mode 100644 index 000000000..972386465 --- /dev/null +++ b/samples/todo/pages/space/[slug]/[listId]/index.tsx @@ -0,0 +1,68 @@ +import { useList, useTodo } from '@zenstackhq/runtime/hooks'; +import { useRouter } from 'next/router'; +import { PlusIcon } from '@heroicons/react/24/outline'; +import { ChangeEvent, KeyboardEvent, useState } from 'react'; +import { useCurrentUser } from 'pages/context'; +import TodoComponent from 'components/Todo'; + +export default function TodoList() { + const user = useCurrentUser(); + const router = useRouter(); + const { get: getList } = useList(); + const { create: createTodo, find: findTodos } = useTodo(); + const [title, setTitle] = useState(''); + + const { data: list } = getList(router.query.listId as string); + const { data: todos } = findTodos({ + where: { + listId: list?.id, + }, + }); + + if (!list) { + return

Loading ...

; + } + + const _createTodo = async () => { + const todo = await createTodo({ + data: { + title, + ownerId: user!.id, + listId: list!.id, + }, + }); + console.log(`Todo created: ${todo}`); + setTitle(''); + }; + + return ( +
+

{list?.title}

+
+ ) => { + if (e.key === 'Enter') { + setTitle(e.currentTarget.value); + _createTodo(); + } + }} + onChange={(e: ChangeEvent) => { + setTitle(e.currentTarget.value); + }} + /> + +
+
    + {todos?.map((todo) => ( + + ))} +
+
+ ); +} diff --git a/samples/todo/pages/space/[slug]/index.tsx b/samples/todo/pages/space/[slug]/index.tsx new file mode 100644 index 000000000..928f95269 --- /dev/null +++ b/samples/todo/pages/space/[slug]/index.tsx @@ -0,0 +1,156 @@ +import { SpaceContext, UserContext } from '../../context'; +import { ChangeEvent, FormEvent, useContext, useState } from 'react'; +import { useList } from '@zenstackhq/runtime/hooks'; +import { toast } from 'react-toastify'; +import TodoList from 'components/TodoList'; + +function CreateDialog() { + const user = useContext(UserContext); + const space = useContext(SpaceContext); + + const [modalOpen, setModalOpen] = useState(false); + const [title, setTitle] = useState(''); + const [_private, setPrivate] = useState(false); + + const { create } = useList(); + + const onSubmit = async (event: FormEvent) => { + event.preventDefault(); + + try { + await create({ + data: { + title, + private: _private, + spaceId: space!.id, + ownerId: user!.id, + }, + }); + } catch (err) { + toast.error(`Failed to create list: ${err}`); + return; + } + + toast.success('List created successfully!'); + + // reset states + setTitle(''); + setPrivate(false); + + // close modal + setModalOpen(false); + }; + + return ( + <> + ) => { + setModalOpen(e.currentTarget.checked); + }} + /> +
+
+

+ Create a Todo list +

+
+
+
+ + + ) => setTitle(e.currentTarget.value)} + /> +
+
+ + + ) => setPrivate(e.currentTarget.checked)} + /> +
+
+
+ + +
+
+
+
+ + ); +} + +export default function SpaceHome() { + const space = useContext(SpaceContext); + const { find } = useList(); + + if (!space) { + return undefined; + } + + const lists = find({ + where: { + space: { + id: space.id, + }, + }, + include: { + owner: true, + }, + }); + + return ( +
+ + +
    + {lists.data?.map((list) => ( +
  • + +
  • + ))} +
+ + +
+ ); +} diff --git a/samples/todo/public/avatar.jpg b/samples/todo/public/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34e82838c53e49f6edec915a2e3d25874c3db92d GIT binary patch literal 29019 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 F|Ji%HlGy+N literal 0 HcmV?d00001 diff --git a/samples/todo/public/logo.png b/samples/todo/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..48bbe06d95a31bcd0e3db4c18cd60de37d1579c4 GIT binary patch literal 11466 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 Date: Fri, 14 Oct 2022 12:58:49 +0800 Subject: [PATCH 2/3] Progress --- packages/internal/package.json | 2 +- packages/internal/src/index.ts | 1 + packages/internal/src/request.ts | 60 ++++++++---- packages/internal/src/request/index.ts | 75 --------------- packages/schema/package.json | 2 +- .../schema/src/generator/react-hooks/index.ts | 93 +------------------ samples/todo/components/BreadCrumb.tsx | 44 +++++++++ samples/todo/components/Todo.tsx | 60 +++++++++++- samples/todo/package-lock.json | 80 ++++++++++++---- samples/todo/package.json | 1 + .../pages/space/[slug]/[listId]/index.tsx | 72 ++++++++------ samples/todo/pages/space/[slug]/index.tsx | 47 +++++----- 12 files changed, 282 insertions(+), 255 deletions(-) delete mode 100644 packages/internal/src/request/index.ts create mode 100644 samples/todo/components/BreadCrumb.tsx diff --git a/packages/internal/package.json b/packages/internal/package.json index a4f35c6fa..21c053db5 100644 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/internal", - "version": "0.1.6", + "version": "0.1.16", "description": "ZenStack internal runtime library", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/packages/internal/src/index.ts b/packages/internal/src/index.ts index 2e3073df3..e50b82227 100644 --- a/packages/internal/src/index.ts +++ b/packages/internal/src/index.ts @@ -1,2 +1,3 @@ export * from './types'; export * from './request-handler'; +export * as request from './request'; diff --git a/packages/internal/src/request.ts b/packages/internal/src/request.ts index b62269e7b..d596e4104 100644 --- a/packages/internal/src/request.ts +++ b/packages/internal/src/request.ts @@ -1,5 +1,5 @@ import useSWR, { useSWRConfig } from 'swr'; -import type { ScopedMutator } from 'swr/dist/types'; +import type { MutatorCallback, MutatorOptions } from 'swr/dist/types'; const fetcher = async (url: string, options?: RequestInit) => { const res = await fetch(url, options); @@ -15,17 +15,17 @@ const fetcher = async (url: string, options?: RequestInit) => { }; function makeUrl(url: string, args: unknown) { - return args ? url + `q=${encodeURIComponent(JSON.stringify(args))}` : url; + return args ? url + `?q=${encodeURIComponent(JSON.stringify(args))}` : url; } -export function get(url: string, args?: unknown) { - return useSWR(makeUrl(url, args), fetcher); +export function get(url: string | null, args?: unknown) { + return useSWR(url && makeUrl(url, args), fetcher); } export async function post( url: string, data: Data, - mutate: ScopedMutator + mutate: Mutator ) { const r: Result = await fetcher(url, { method: 'POST', @@ -34,14 +34,14 @@ export async function post( }, body: JSON.stringify(data), }); - mutate(url); + mutate(url, true); return r; } export async function put( url: string, data: Data, - mutate: ScopedMutator + mutate: Mutator ) { const r: Result = await fetcher(url, { method: 'PUT', @@ -50,26 +50,52 @@ export async function put( }, body: JSON.stringify(data), }); - mutate(url, r); + mutate(url, true); return r; } -export async function del( - url: string, - args: unknown, - mutate: ScopedMutator -) { +export async function del(url: string, args: unknown, mutate: Mutator) { const reqUrl = makeUrl(url, args); const r: Result = await fetcher(reqUrl, { method: 'DELETE', }); const path = url.split('/'); path.pop(); - mutate(path.join('/')); + mutate(path.join('/'), true); return r; } -export function getMutate() { - const { mutate } = useSWRConfig(); - return mutate; +type Mutator = ( + key: string, + prefix: boolean, + data?: any | Promise | MutatorCallback, + opts?: boolean | MutatorOptions +) => Promise; + +export function getMutate(): Mutator { + // https://swr.vercel.app/docs/advanced/cache#mutate-multiple-keys-from-regex + const { cache, mutate } = useSWRConfig(); + return ( + key: string, + prefix: boolean, + data?: any | Promise | MutatorCallback, + opts?: boolean | MutatorOptions + ) => { + if (!prefix) { + return mutate(key, data, opts); + } + + if (!(cache instanceof Map)) { + throw new Error( + 'mutate requires the cache provider to be a Map instance' + ); + } + + const keys = Array.from(cache.keys()).filter( + (k) => typeof k === 'string' && k.startsWith(key) + ) as string[]; + console.log('Mutating keys:', JSON.stringify(keys)); + const mutations = keys.map((key) => mutate(key, data, opts)); + return Promise.all(mutations); + }; } diff --git a/packages/internal/src/request/index.ts b/packages/internal/src/request/index.ts deleted file mode 100644 index b62269e7b..000000000 --- a/packages/internal/src/request/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import useSWR, { useSWRConfig } from 'swr'; -import type { ScopedMutator } from 'swr/dist/types'; - -const fetcher = async (url: string, options?: RequestInit) => { - const res = await fetch(url, options); - if (!res.ok) { - const error: Error & { info?: any; status?: number } = new Error( - 'An error occurred while fetching the data.' - ); - error.info = await res.json(); - error.status = res.status; - throw error; - } - return res.json(); -}; - -function makeUrl(url: string, args: unknown) { - return args ? url + `q=${encodeURIComponent(JSON.stringify(args))}` : url; -} - -export function get(url: string, args?: unknown) { - return useSWR(makeUrl(url, args), fetcher); -} - -export async function post( - url: string, - data: Data, - mutate: ScopedMutator -) { - const r: Result = await fetcher(url, { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify(data), - }); - mutate(url); - return r; -} - -export async function put( - url: string, - data: Data, - mutate: ScopedMutator -) { - const r: Result = await fetcher(url, { - method: 'PUT', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify(data), - }); - mutate(url, r); - return r; -} - -export async function del( - url: string, - args: unknown, - mutate: ScopedMutator -) { - const reqUrl = makeUrl(url, args); - const r: Result = await fetcher(reqUrl, { - method: 'DELETE', - }); - const path = url.split('/'); - path.pop(); - mutate(path.join('/')); - return r; -} - -export function getMutate() { - const { mutate } = useSWRConfig(); - return mutate; -} diff --git a/packages/schema/package.json b/packages/schema/package.json index 89fbaec52..3c609835d 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -2,7 +2,7 @@ "name": "zenstack", "displayName": "ZenStack CLI and Language Tools", "description": "ZenStack CLI and Language Tools", - "version": "0.1.27", + "version": "0.1.32", "engines": { "vscode": "^1.56.0" }, diff --git a/packages/schema/src/generator/react-hooks/index.ts b/packages/schema/src/generator/react-hooks/index.ts index 5faa1e1b9..66685b6dd 100644 --- a/packages/schema/src/generator/react-hooks/index.ts +++ b/packages/schema/src/generator/react-hooks/index.ts @@ -5,7 +5,7 @@ import { paramCase } from 'change-case'; import { DataModel } from '@lang/generated/ast'; import colors from 'colors'; import { extractDataModelsWithAllowRules } from '../utils'; -import { API_ROUTE_NAME } from '../constants'; +import { API_ROUTE_NAME, INTERNAL_PACKAGE } from '../constants'; export default class ReactHooksGenerator implements Generator { async generate(context: Context) { @@ -14,7 +14,6 @@ export default class ReactHooksGenerator implements Generator { const models = extractDataModelsWithAllowRules(context.schema); this.generateIndex(project, context, models); - this.generateRequestRuntime(project, context); models.forEach((d) => this.generateModelHooks(project, context, d)); @@ -23,93 +22,6 @@ export default class ReactHooksGenerator implements Generator { console.log(colors.blue(' ✔️ React hooks generated')); } - private generateRequestRuntime(project: Project, context: Context) { - const content = ` - import useSWR, { useSWRConfig } from 'swr'; - import type { ScopedMutator } from 'swr/dist/types'; - - const fetcher = async (url: string, options?: RequestInit) => { - const res = await fetch(url, options); - if (!res.ok) { - const error: Error & { info?: any; status?: number } = new Error( - 'An error occurred while fetching the data.' - ); - error.info = await res.json(); - error.status = res.status; - throw error; - } - return res.json(); - }; - - function makeUrl(url: string, args: unknown) { - return args ? url + \`?q=\${encodeURIComponent(JSON.stringify(args))}\` : url; - } - - export function get(url: string | null, args?: unknown) { - return useSWR(url && makeUrl(url, args), fetcher); - } - - export async function post( - url: string, - data: Data, - mutate: ScopedMutator - ) { - const r: Result = await fetcher(url, { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify(data), - }); - mutate(url); - return r; - } - - export async function put( - url: string, - data: Data, - mutate: ScopedMutator - ) { - const r: Result = await fetcher(url, { - method: 'PUT', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify(data), - }); - mutate(url, r); - return r; - } - - export async function del( - url: string, - args: unknown, - mutate: ScopedMutator - ) { - const reqUrl = makeUrl(url, args); - const r: Result = await fetcher(reqUrl, { - method: 'DELETE', - }); - const path = url.split('/'); - path.pop(); - mutate(path.join('/')); - return r; - } - - export function getMutate() { - const { mutate } = useSWRConfig(); - return mutate; - } - `; - - const sf = project.createSourceFile( - path.join(context.outDir, `src/hooks/request.ts`), - content, - { overwrite: true } - ); - sf.formatText(); - } - private generateModelHooks( project: Project, context: Context, @@ -127,8 +39,7 @@ export default class ReactHooksGenerator implements Generator { isTypeOnly: true, moduleSpecifier: '../../.prisma', }); - - sf.addStatements([`import * as request from './request';`]); + sf.addStatements(`import { request } from '${INTERNAL_PACKAGE}';`); sf.addStatements( `const endpoint = '/api/${API_ROUTE_NAME}/data/${model.name}';` diff --git a/samples/todo/components/BreadCrumb.tsx b/samples/todo/components/BreadCrumb.tsx new file mode 100644 index 000000000..a02c08a24 --- /dev/null +++ b/samples/todo/components/BreadCrumb.tsx @@ -0,0 +1,44 @@ +import { useList } from '@zenstackhq/runtime/hooks'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useCurrentSpace } from 'pages/context'; + +export default function BreadCrumb() { + const router = useRouter(); + const space = useCurrentSpace(); + const { get: getList } = useList(); + + const parts = router.asPath.split('/').filter((p) => p); + + const [base, slug, listId] = parts; + if (base !== 'space') { + return <>; + } + + const items: Array<{ text: string; link: string }> = []; + + items.push({ text: 'Home', link: '/' }); + items.push({ text: space?.name || '', link: `/space/${slug}` }); + + if (listId) { + const { data } = getList(listId); + items.push({ + text: data?.title || '', + link: `/space/${slug}/${listId}`, + }); + } + + return ( +
+
    + {items.map((item, i) => ( +
  • + + {item.text} + +
  • + ))} +
+
+ ); +} diff --git a/samples/todo/components/Todo.tsx b/samples/todo/components/Todo.tsx index f416c626b..e218a767e 100644 --- a/samples/todo/components/Todo.tsx +++ b/samples/todo/components/Todo.tsx @@ -1,9 +1,61 @@ -import { Todo } from '@zenstackhq/runtime/types'; +import { useTodo } from '@zenstackhq/runtime/hooks'; +import { Todo, User } from '@zenstackhq/runtime/types'; +import moment from 'moment'; +import { ChangeEvent, useEffect, useState } from 'react'; +import Avatar from './Avatar'; type Props = { - value: Todo; + value: Todo & { owner: User }; + updated?: (value: Todo) => any; }; -export default function Component({ value }: Props) { - return
{value.title}
; +export default function Component({ value, updated }: Props) { + const [completed, setCompleted] = useState(!!value.completedAt); + const { update } = useTodo(); + + useEffect(() => { + if (!!value.completedAt !== completed) { + update(value.id, { + data: { completedAt: completed ? new Date() : null }, + }).then((newValue) => { + if (updated) { + updated(newValue); + } + }); + } + }); + + return ( +
+
+

+ {value.title} +

+ ) => + setCompleted(e.currentTarget.checked) + } + /> +
+
+

+ {value.completedAt + ? `Completed ${moment(value.completedAt).fromNow()}` + : value.createdAt === value.updatedAt + ? `Created ${moment(value.createdAt).fromNow()}` + : `Updated ${moment(value.updatedAt).fromNow()}`} +

+ +
+
+ ); } diff --git a/samples/todo/package-lock.json b/samples/todo/package-lock.json index 465c9ee41..def53c89e 100644 --- a/samples/todo/package-lock.json +++ b/samples/todo/package-lock.json @@ -10,8 +10,10 @@ "dependencies": { "@heroicons/react": "^2.0.12", "@prisma/client": "^4.4.0", + "@zenstackhq/internal": "^0.1.16", "@zenstackhq/runtime": "latest", "daisyui": "^2.31.0", + "moment": "^2.29.4", "nanoid": "^4.0.0", "next": "12.3.1", "next-auth": "^4.10.3", @@ -719,10 +721,11 @@ } }, "node_modules/@zenstackhq/internal": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.1.6.tgz", - "integrity": "sha512-E2iPDgih6StV+RioV0NWiLC6/m9i90CYs5t2dHSygKj2614+8oxcvf8OzQGOgtgEm12HaqRJHwonZWIo+NV3ag==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.1.16.tgz", + "integrity": "sha512-u14IiTdOQj71277GLGKOOiTvUDKjnz6txxyykboXpR3HFEARge2/UW6CAmmfJo7HzjYm4BS9X9EE5tym6Vx4fA==", "dependencies": { + "bcryptjs": "^2.4.3", "deepcopy": "^2.1.0", "swr": "^1.3.0" }, @@ -1003,8 +1006,7 @@ "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "peer": true + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -2892,6 +2894,14 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4500,12 +4510,12 @@ } }, "node_modules/zenstack": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.1.27.tgz", - "integrity": "sha512-pkNh8RB6Ir7KaLeXOZj+0dwgoExfD/RtYsSxVmXi+0hZamAQe09c6Zpj4YWc72aditNpW3exA/CSvXzsv3paog==", + "version": "0.1.32", + "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.1.32.tgz", + "integrity": "sha512-xu/248b/PzV8AEwEfL1rpXcNMAcf9wTCIOC6xkurrl4Cba/J1DxFdsLvmJEg88lJlNplNSUAjdD4zMnWhVdEpA==", "dev": true, "dependencies": { - "@zenstackhq/internal": "0.1.6", + "@zenstackhq/internal": "0.1.9", "change-case": "^4.1.2", "chevrotain": "^9.1.0", "colors": "^1.4.0", @@ -4525,6 +4535,22 @@ "engines": { "vscode": "^1.56.0" } + }, + "node_modules/zenstack/node_modules/@zenstackhq/internal": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.1.9.tgz", + "integrity": "sha512-OIwUkfHoK7zNa8EWiveyIAa8KOngcXfuYpi//r/kkjS238wLbnf5CCElx4KKp4zKCeWd1urhb6nstcN6HBjtwg==", + "dev": true, + "dependencies": { + "bcryptjs": "^2.4.3", + "deepcopy": "^2.1.0", + "swr": "^1.3.0" + }, + "peerDependencies": { + "next": "12.3.1", + "react": "^17.0.2 || ^18", + "react-dom": "^17.0.2 || ^18" + } } }, "dependencies": { @@ -4986,10 +5012,11 @@ } }, "@zenstackhq/internal": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.1.6.tgz", - "integrity": "sha512-E2iPDgih6StV+RioV0NWiLC6/m9i90CYs5t2dHSygKj2614+8oxcvf8OzQGOgtgEm12HaqRJHwonZWIo+NV3ag==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.1.16.tgz", + "integrity": "sha512-u14IiTdOQj71277GLGKOOiTvUDKjnz6txxyykboXpR3HFEARge2/UW6CAmmfJo7HzjYm4BS9X9EE5tym6Vx4fA==", "requires": { + "bcryptjs": "^2.4.3", "deepcopy": "^2.1.0", "swr": "^1.3.0" } @@ -5182,8 +5209,7 @@ "bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "peer": true + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, "binary-extensions": { "version": "2.2.0", @@ -6594,6 +6620,11 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7724,12 +7755,12 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "zenstack": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.1.27.tgz", - "integrity": "sha512-pkNh8RB6Ir7KaLeXOZj+0dwgoExfD/RtYsSxVmXi+0hZamAQe09c6Zpj4YWc72aditNpW3exA/CSvXzsv3paog==", + "version": "0.1.32", + "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.1.32.tgz", + "integrity": "sha512-xu/248b/PzV8AEwEfL1rpXcNMAcf9wTCIOC6xkurrl4Cba/J1DxFdsLvmJEg88lJlNplNSUAjdD4zMnWhVdEpA==", "dev": true, "requires": { - "@zenstackhq/internal": "0.1.6", + "@zenstackhq/internal": "0.1.9", "change-case": "^4.1.2", "chevrotain": "^9.1.0", "colors": "^1.4.0", @@ -7742,6 +7773,19 @@ "vscode-languageclient": "^7.0.0", "vscode-languageserver": "^7.0.0", "vscode-uri": "^3.0.2" + }, + "dependencies": { + "@zenstackhq/internal": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.1.9.tgz", + "integrity": "sha512-OIwUkfHoK7zNa8EWiveyIAa8KOngcXfuYpi//r/kkjS238wLbnf5CCElx4KKp4zKCeWd1urhb6nstcN6HBjtwg==", + "dev": true, + "requires": { + "bcryptjs": "^2.4.3", + "deepcopy": "^2.1.0", + "swr": "^1.3.0" + } + } } } } diff --git a/samples/todo/package.json b/samples/todo/package.json index bc301b8a9..8e1ec46a2 100644 --- a/samples/todo/package.json +++ b/samples/todo/package.json @@ -18,6 +18,7 @@ "@prisma/client": "^4.4.0", "@zenstackhq/runtime": "latest", "daisyui": "^2.31.0", + "moment": "^2.29.4", "nanoid": "^4.0.0", "next": "12.3.1", "next-auth": "^4.10.3", diff --git a/samples/todo/pages/space/[slug]/[listId]/index.tsx b/samples/todo/pages/space/[slug]/[listId]/index.tsx index 972386465..9b46dbf72 100644 --- a/samples/todo/pages/space/[slug]/[listId]/index.tsx +++ b/samples/todo/pages/space/[slug]/[listId]/index.tsx @@ -4,6 +4,7 @@ import { PlusIcon } from '@heroicons/react/24/outline'; import { ChangeEvent, KeyboardEvent, useState } from 'react'; import { useCurrentUser } from 'pages/context'; import TodoComponent from 'components/Todo'; +import BreadCrumb from 'components/BreadCrumb'; export default function TodoList() { const user = useCurrentUser(); @@ -13,10 +14,16 @@ export default function TodoList() { const [title, setTitle] = useState(''); const { data: list } = getList(router.query.listId as string); - const { data: todos } = findTodos({ + const { data: todos, mutate: invalidateTodos } = findTodos({ where: { listId: list?.id, }, + include: { + owner: true, + }, + orderBy: { + updatedAt: 'desc', + }, }); if (!list) { @@ -36,33 +43,44 @@ export default function TodoList() { }; return ( -
-

{list?.title}

-
- ) => { - if (e.key === 'Enter') { + <> +
+ +
+
+

{list?.title}

+
+ ) => { + if (e.key === 'Enter') { + setTitle(e.currentTarget.value); + _createTodo(); + } + }} + onChange={(e: ChangeEvent) => { setTitle(e.currentTarget.value); - _createTodo(); - } - }} - onChange={(e: ChangeEvent) => { - setTitle(e.currentTarget.value); - }} - /> - + }} + /> + +
+
    + {todos?.map((todo) => ( + { + invalidateTodos(); + }} + /> + ))} +
-
    - {todos?.map((todo) => ( - - ))} -
-
+ ); } diff --git a/samples/todo/pages/space/[slug]/index.tsx b/samples/todo/pages/space/[slug]/index.tsx index 928f95269..512787557 100644 --- a/samples/todo/pages/space/[slug]/index.tsx +++ b/samples/todo/pages/space/[slug]/index.tsx @@ -3,6 +3,7 @@ import { ChangeEvent, FormEvent, useContext, useState } from 'react'; import { useList } from '@zenstackhq/runtime/hooks'; import { toast } from 'react-toastify'; import TodoList from 'components/TodoList'; +import BreadCrumb from 'components/BreadCrumb'; function CreateDialog() { const user = useContext(UserContext); @@ -118,39 +119,43 @@ export default function SpaceHome() { const space = useContext(SpaceContext); const { find } = useList(); - if (!space) { - return undefined; - } - const lists = find({ where: { space: { - id: space.id, + id: space?.id, }, }, include: { owner: true, }, + orderBy: { + updatedAt: 'desc', + }, }); return ( -
- + <> +
+ +
+
+ -
    - {lists.data?.map((list) => ( -
  • - -
  • - ))} -
+
    + {lists.data?.map((list) => ( +
  • + +
  • + ))} +
- -
+ +
+ ); } From 7c37b7a909b2e6823ba4cb50dfa7a4ddee0899de Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 14 Oct 2022 14:55:56 +0800 Subject: [PATCH 3/3] wip --- packages/internal/package.json | 2 +- packages/internal/src/handler/data/handler.ts | 39 ++---- samples/todo/components/ManageMembers.tsx | 116 ++++++++++++++++++ samples/todo/components/NavBar.tsx | 5 +- samples/todo/components/SpaceMembers.tsx | 76 ++++++++++++ samples/todo/components/Todo.tsx | 2 +- samples/todo/package-lock.json | 14 +-- samples/todo/pages/index.tsx | 77 ------------ .../pages/space/[slug]/[listId]/index.tsx | 3 +- samples/todo/pages/space/[slug]/index.tsx | 16 ++- 10 files changed, 224 insertions(+), 126 deletions(-) create mode 100644 samples/todo/components/ManageMembers.tsx create mode 100644 samples/todo/components/SpaceMembers.tsx diff --git a/packages/internal/package.json b/packages/internal/package.json index 21c053db5..0a43eea65 100644 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/internal", - "version": "0.1.16", + "version": "0.1.18", "description": "ZenStack internal runtime library", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/packages/internal/src/handler/data/handler.ts b/packages/internal/src/handler/data/handler.ts index ca9cee526..9d8c9271f 100644 --- a/packages/internal/src/handler/data/handler.ts +++ b/packages/internal/src/handler/data/handler.ts @@ -12,6 +12,7 @@ import { QueryProcessor } from './query-processor'; const PRISMA_ERROR_MAPPING: Record = { P2002: ServerErrorCode.UNIQUE_CONSTRAINT_VIOLATION, P2003: ServerErrorCode.REFERENCE_CONSTRAINT_VIOLATION, + P2025: ServerErrorCode.REFERENCE_CONSTRAINT_VIOLATION, }; export default class DataHandler implements RequestHandler { @@ -125,9 +126,7 @@ export default class DataHandler implements RequestHandler { r = await db.findMany(processedArgs); } - console.log( - `Finding ${model}:\n${JSON.stringify(processedArgs, undefined, 2)}` - ); + console.log(`Finding ${model}:\n${JSON.stringify(processedArgs)}`); await this.queryProcessor.postProcess( model, processedArgs, @@ -163,13 +162,7 @@ export default class DataHandler implements RequestHandler { ); const r = await db.$transaction(async (tx: any) => { - console.log( - `Create ${model}:\n${JSON.stringify( - processedArgs, - undefined, - 2 - )}` - ); + console.log(`Create ${model}:\n${JSON.stringify(processedArgs)}`); const created = await tx[model].create(processedArgs); let queryArgs = { @@ -184,11 +177,7 @@ export default class DataHandler implements RequestHandler { context ); console.log( - `Finding created ${model}:\n${JSON.stringify( - queryArgs, - undefined, - 2 - )}` + `Finding created ${model}:\n${JSON.stringify(queryArgs)}` ); const found = await tx[model].findFirst(queryArgs); if (!found) { @@ -247,9 +236,7 @@ export default class DataHandler implements RequestHandler { updateArgs.where = { ...updateArgs.where, id }; const r = await db.$transaction(async (tx: any) => { - console.log( - `Update ${model}:\n${JSON.stringify(updateArgs, undefined, 2)}` - ); + console.log(`Update ${model}:\n${JSON.stringify(updateArgs)}`); const updated = await tx[model].update(updateArgs); // make sure after update, the entity passes policy check @@ -265,11 +252,7 @@ export default class DataHandler implements RequestHandler { context ); console.log( - `Finding post-updated ${model}:\n${JSON.stringify( - queryArgs, - undefined, - 2 - )}` + `Finding post-updated ${model}:\n${JSON.stringify(queryArgs)}` ); const found = await tx[model].findFirst(queryArgs); if (!found) { @@ -321,9 +304,7 @@ export default class DataHandler implements RequestHandler { ); delArgs.where = { ...delArgs.where, id }; - console.log( - `Deleting ${model}:\n${JSON.stringify(delArgs, undefined, 2)}` - ); + console.log(`Deleting ${model}:\n${JSON.stringify(delArgs)}`); const db = (this.service.db as any)[model]; const r = await db.delete(delArgs); await this.queryProcessor.postProcess( @@ -353,11 +334,7 @@ export default class DataHandler implements RequestHandler { context ); console.log( - `Finding to-be-deleted ${model}:\n${JSON.stringify( - readArgs, - undefined, - 2 - )}` + `Finding to-be-deleted ${model}:\n${JSON.stringify(readArgs)}` ); const read = await db.findFirst(readArgs); if (!read) { diff --git a/samples/todo/components/ManageMembers.tsx b/samples/todo/components/ManageMembers.tsx new file mode 100644 index 000000000..450c20fc7 --- /dev/null +++ b/samples/todo/components/ManageMembers.tsx @@ -0,0 +1,116 @@ +import { PlusIcon } from '@heroicons/react/24/outline'; +import { ServerErrorCode } from '@zenstackhq/internal'; +import { HooksError, useSpaceUser } from '@zenstackhq/runtime/hooks'; +import { Space, SpaceUserRole } from '@zenstackhq/runtime/types'; +import { ChangeEvent, KeyboardEvent, useState } from 'react'; +import { toast } from 'react-toastify'; +import Avatar from './Avatar'; + +type Props = { + space: Space; +}; + +export default function ManageMembers({ space }: Props) { + const [email, setEmail] = useState(''); + const [role, setRole] = useState(SpaceUserRole.USER); + + const { find, create: addMember } = useSpaceUser(); + const { data: members } = find({ + where: { + spaceId: space.id, + }, + include: { + user: true, + }, + }); + + const inviteUser = async () => { + try { + const r = await addMember({ + data: { + user: { + connect: { + email, + }, + }, + space: { + connect: { + id: space.id, + }, + }, + role, + }, + }); + console.log('SpaceUser created:', r); + } catch (err: any) { + console.error(JSON.stringify(err)); + if (err.info?.code) { + const { info } = err as HooksError; + if (info.code === ServerErrorCode.UNIQUE_CONSTRAINT_VIOLATION) { + toast.error('User is already a member of the space'); + } else if ( + info.code === ServerErrorCode.REFERENCE_CONSTRAINT_VIOLATION + ) { + toast.error('User is not found for this email'); + } + } else { + toast.error(`Error occurred: ${err}`); + } + } + }; + + return ( +
+
+ ) => { + setEmail(e.currentTarget.value); + }} + onKeyUp={(e: KeyboardEvent) => { + if (e.key === 'Enter') { + inviteUser(); + } + }} + /> + + + + +
+ +
    + {members?.map((member) => ( +
  • +
    + +

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

    +

    {member.role}

    +
    +
    + +
    +
  • + ))} +
+
+ ); +} diff --git a/samples/todo/components/NavBar.tsx b/samples/todo/components/NavBar.tsx index d523859a1..4ac9f9824 100644 --- a/samples/todo/components/NavBar.tsx +++ b/samples/todo/components/NavBar.tsx @@ -23,8 +23,11 @@ export default function NavBar({ user, space }: Props) { height={32} />
- {space?.name} + {space?.name || 'Welcome Todo App'}
+

+ Powered by ZenStack +

diff --git a/samples/todo/components/SpaceMembers.tsx b/samples/todo/components/SpaceMembers.tsx new file mode 100644 index 000000000..9be76c06c --- /dev/null +++ b/samples/todo/components/SpaceMembers.tsx @@ -0,0 +1,76 @@ +import { useSpaceUser } from '@zenstackhq/runtime/hooks'; +import { useCurrentSpace } from 'pages/context'; +import { PlusIcon } from '@heroicons/react/24/outline'; +import Avatar from './Avatar'; +import ManageMembers from './ManageMembers'; +import { Space } from '@zenstackhq/runtime/types'; + +function ManagementDialog(space?: Space) { + if (!space) return undefined; + return ( + <> + + + +
+
+

+ Manage Members of {space.name} +

+ +
+ +
+ +
+ +
+
+
+ + ); +} + +export default function SpaceMembers() { + const space = useCurrentSpace(); + + const { find: findMembers } = useSpaceUser(); + const { data: members } = findMembers({ + where: { + spaceId: space?.id, + }, + include: { + user: true, + }, + orderBy: { + role: 'desc', + }, + }); + + return ( +
+ {ManagementDialog(space)} + {members && ( + + )} +
+ ); +} diff --git a/samples/todo/components/Todo.tsx b/samples/todo/components/Todo.tsx index e218a767e..c349a57b4 100644 --- a/samples/todo/components/Todo.tsx +++ b/samples/todo/components/Todo.tsx @@ -29,7 +29,7 @@ export default function Component({ value, updated }: Props) {

{ signIn(); } - // const { - // create: createTodoCollection, - // find: findTodoCollection, - // del: deleteTodoCollection, - // } = useTodoCollection(); - - // const { data: todoCollections } = findTodoCollection(); - - // async function onCreateTodoCollection() { - // await createTodoCollection({ - // data: { - // title: 'My Todo Collection', - // ownerId: session!.user.id, - // spaceId: 'f0c9fc5c-e6e5-4146-a540-214f6ac5701c', - // }, - // }); - // } - - // async function onCreateFilledTodoCollection() { - // await createTodoCollection({ - // data: { - // title: 'My Todo Collection', - // ownerId: session!.user.id, - // spaceId: 'f0c9fc5c-e6e5-4146-a540-214f6ac5701c', - // todos: { - // create: [ - // { title: 'First Todo', ownerId: session!.user.id }, - // ], - // }, - // }, - // }); - // } - - // async function onDeleteTodoCollection(todoList: TodoCollection) { - // await deleteTodoCollection(todoList.id); - // } - - // function renderTodoCollections() { - // return ( - // <> - //
    - // {todoCollections?.map((collection) => ( - //
  • - //

    {collection.title}

    - // - //
  • - // ))} - //
- // - // ); - // } - if (!session) { return
Loading ...
; } @@ -93,22 +32,6 @@ const Home: NextPage = () => {

- {/* - - - -

Todo Lists

- {renderTodoCollections()} */}
); diff --git a/samples/todo/pages/space/[slug]/[listId]/index.tsx b/samples/todo/pages/space/[slug]/[listId]/index.tsx index 9b46dbf72..73ca504e6 100644 --- a/samples/todo/pages/space/[slug]/[listId]/index.tsx +++ b/samples/todo/pages/space/[slug]/[listId]/index.tsx @@ -57,7 +57,6 @@ export default function TodoList() { value={title} onKeyUp={(e: KeyboardEvent) => { if (e.key === 'Enter') { - setTitle(e.currentTarget.value); _createTodo(); } }} @@ -65,7 +64,7 @@ export default function TodoList() { setTitle(e.currentTarget.value); }} /> - diff --git a/samples/todo/pages/space/[slug]/index.tsx b/samples/todo/pages/space/[slug]/index.tsx index 512787557..277fd5f73 100644 --- a/samples/todo/pages/space/[slug]/index.tsx +++ b/samples/todo/pages/space/[slug]/index.tsx @@ -4,6 +4,7 @@ import { useList } from '@zenstackhq/runtime/hooks'; import { toast } from 'react-toastify'; import TodoList from 'components/TodoList'; import BreadCrumb from 'components/BreadCrumb'; +import SpaceMembers from 'components/SpaceMembers'; function CreateDialog() { const user = useContext(UserContext); @@ -139,12 +140,15 @@ export default function SpaceHome() {
- +
+ + +
    {lists.data?.map((list) => (