diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 88e8714b..e6f629a4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -35,7 +35,7 @@ jobs: - name: Run tests run: | yarn turbo test:e2e - BASE_URL=http://localhost:3000/legacy/admin yarn test:e2e + BASE_URL=http://localhost:3000/pagerouter/admin yarn test:e2e - uses: actions/upload-artifact@v3 if: always() with: diff --git a/apps/docs/pages/docs/api-docs.mdx b/apps/docs/pages/docs/api-docs.mdx index da4a0582..a008d235 100644 --- a/apps/docs/pages/docs/api-docs.mdx +++ b/apps/docs/pages/docs/api-docs.mdx @@ -1,86 +1,140 @@ +import { Tabs } from "nextra/components"; + # API -## `nextAdminRouter` function +## Functions -`nextAdminRouter` is a function that returns a promise of a _Node Router_ that you can use in your getServerSideProps function to start using Next Admin. Its usage is only related to Page router. + + + The following is used only for App router. -Usage example: + ## `getPropsFromParams` function -```ts -// pages/api/admin/[[...nextadmin]].ts -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const { nextAdminRouter } = await import( - "@premieroctet/next-admin/dist/router" - ); - const adminRouter = await nextAdminRouter(prisma, schema); - return adminRouter.run(req, res) as Promise< - GetServerSidePropsResult<{ [key: string]: any }> - >; -}; -``` + `getPropsFromParams` is a function that returns the props for the [`NextAdmin`](#nextadmin--component) component. It accepts one argument which is an object with the following properties: -It takes 3 parameters: + - `params`: the array of route params retrieved from the [optional catch-all segment](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#optional-catch-all-segments) + - `searchParams`: the query params [retrieved from the page](https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) + - `options`: the [options](#next-admin-options) object + - `schema`: the json schema generated by the `prisma generate` command + - `prisma`: your Prisma client instance + - `action`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to submit the form. It should be your own action, that wraps the `submitForm` action imported from `@premieroctet/next-admin/dist/actions`. -- Your Prisma client instance, _required_ -- Your Prisma schema, _required_ + + + The following is used only for Page router -and an _optional_ object of type [`NextAdminOptions`](#next-admin-options) to customize your admin with the following properties: + ## `nextAdminRouter` function -```ts -import { NextAdminOptions } from "@premieroctet/next-admin"; + `nextAdminRouter` is a function that returns a promise of a _Node Router_ that you can use in your getServerSideProps function to start using Next Admin. Its usage is only related to Page router. -const options: NextAdminOptions = { - model: { - User: { - toString: (user) => `${user.email} / ${user.name}`, - }, - }, -}; + Usage example: -const adminRouter = await nextAdminRouter(prisma, schema, options); -``` + ```ts + // pages/api/admin/[[...nextadmin]].ts + export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const { nextAdminRouter } = await import( + "@premieroctet/next-admin/dist/router" + ); + const adminRouter = await nextAdminRouter(prisma, schema); + return adminRouter.run(req, res) as Promise< + GetServerSidePropsResult<{ [key: string]: any }> + >; + }; + ``` -## `getPropsFromParams` function + It takes 3 parameters: -`getPropsFromParams` is a function that returns the props for the [`NextAdmin`](#nextadmin--component) component. It accepts one argument which is an object with the following properties: + - Your Prisma client instance, _required_ + - Your Prisma schema, _required_ -- `params`: the array of route params retrieved from the [optional catch-all segment](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#optional-catch-all-segments) -- `searchParams`: the query params [retrieved from the page](https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) -- `options`: the [options](#next-admin-options) object -- `schema`: the json schema generated by the `prisma generate` command -- `prisma`: your Prisma client instance -- `action`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to submit the form. It should be your own action, that wraps the `submitForm` action imported from `@premieroctet/next-admin/dist/actions`. + and an _optional_ object of type [`NextAdminOptions`](#next-admin-options) to customize your admin with the following properties: -## Authentication + ```ts + import { NextAdminOptions } from "@premieroctet/next-admin"; -The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check to the `getServerSideProps` function: + const options: NextAdminOptions = { + model: { + User: { + toString: (user) => `${user.email} / ${user.name}`, + }, + }, + }; -> The following example uses [next-auth](https://next-auth.js.org/) to handle authentication + const adminRouter = await nextAdminRouter(prisma, schema, options); + ``` -```ts -// pages/api/admin/[[...nextadmin]].ts + + -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const session = await getServerSession(req, res, authOptions); - const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check +## Authentication - if (!isAdmin) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } + + + The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check in the page: + + > The following example uses [next-auth](https://next-auth.js.org/) to handle authentication + + ```ts + // app/admin/[[...nextadmin]]/page.tsx + + export default async function AdminPage({ + params, + searchParams, + }: { + params: { [key: string]: string[] }; + searchParams: { [key: string]: string | string[] | undefined } | undefined; + }) { + const session = await getServerSession(authOptions); + const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check + + if (!isAdmin) { + redirect('/', { permanent: false }) + } + + const props = await getPropsFromParams({ + params: params.nextadmin, + searchParams, + options, + prisma, + schema, + action: submitFormAction, + }); + + return ; + } + ``` + + + + The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check to the `getServerSideProps` function: + + > The following example uses [next-auth](https://next-auth.js.org/) to handle authentication + + ```ts + // pages/api/admin/[[...nextadmin]].ts + + export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await getServerSession(req, res, authOptions); + const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check + + if (!isAdmin) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } - const { nextAdminRouter } = await import( - "@premieroctet/next-admin/dist/nextAdminRouter" - ); - return nextAdminRouter(client).run(req, res); -}; -``` + const { nextAdminRouter } = await import( + "@premieroctet/next-admin/dist/nextAdminRouter" + ); + return nextAdminRouter(client).run(req, res); + }; + ``` -For App router, this check will happen directly in your page. + + ## `` component diff --git a/apps/example/pages/legacy/[[...nextadmin]].tsx b/apps/example/pages/pagerouter/[[...nextadmin]].tsx similarity index 96% rename from apps/example/pages/legacy/[[...nextadmin]].tsx rename to apps/example/pages/pagerouter/[[...nextadmin]].tsx index 3140ad0f..185a01c2 100644 --- a/apps/example/pages/legacy/[[...nextadmin]].tsx +++ b/apps/example/pages/pagerouter/[[...nextadmin]].tsx @@ -8,7 +8,7 @@ import schema from "../../prisma/json-schema/json-schema.json"; const pageOptions = { ...options, - basePath: "/legacy/admin", + basePath: "/pagerouter/admin", }; export default function Admin(props: AdminComponentProps) { diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index 5aa01dfd..c790f687 100644 --- a/packages/next-admin/src/components/Form.tsx +++ b/packages/next-admin/src/components/Form.tsx @@ -170,10 +170,6 @@ const Form = ({ ); }; - useEffect(() => { - setValidation(validationProp); - }, [validationProp]); - const extraErrors: ErrorSchema | undefined = validation?.reduce( (acc, curr) => { // @ts-expect-error diff --git a/packages/next-admin/src/utils/actions.ts b/packages/next-admin/src/utils/actions.ts index 8ffa966a..03bb1709 100644 --- a/packages/next-admin/src/utils/actions.ts +++ b/packages/next-admin/src/utils/actions.ts @@ -1,5 +1,10 @@ import { ActionParams } from "../types"; +/** + * Following https://nextjs.org/docs/app/api-reference/functions/server-actions#binding-arguments + * We need the params and schema options to be there when the action is called. + * Other params (prisma, options) will be added by the app's action implementation. + */ export const createBoundServerAction = ( { params, schema }: ActionParams, action: (params: ActionParams, formData: FormData) => Promise diff --git a/packages/next-admin/src/utils/server.test.ts b/packages/next-admin/src/utils/server.test.ts new file mode 100644 index 00000000..2aa32b49 --- /dev/null +++ b/packages/next-admin/src/utils/server.test.ts @@ -0,0 +1,66 @@ +import { + getResourceFromParams, + getResourceFromUrl, + getResourceIdFromParam, + getResourceIdFromUrl, +} from "./server"; + +describe("Server utils", () => { + describe("getResourceFromUrl", () => { + it("should return a resource with /api/User", () => { + expect(getResourceFromUrl("/api/User", ["User"])).toEqual("User"); + }); + + it("should return a resource with /api/User/1", () => { + expect(getResourceFromUrl("/api/User/1", ["User"])).toEqual("User"); + }); + + it("should not return a resource with /api/Post", () => { + expect(getResourceFromUrl("/api/Post", ["User"])).toEqual(undefined); + }); + }); + + describe("getResourceFromParams", () => { + it("should return a resource with /api/User", () => { + expect(getResourceFromParams(["User"], ["User"])).toEqual("User"); + }); + + it("should return a resource with /api/User/1", () => { + expect(getResourceFromParams(["User", "1"], ["User"])).toEqual("User"); + }); + + it("should not return a resource with /api/Post", () => { + expect(getResourceFromParams(["Post"], ["User"])).toEqual(undefined); + }); + }); + + describe("getResourceIdFromUrl", () => { + it("should get the id from /api/User/1", () => { + expect(getResourceIdFromUrl("/api/User/1", "User")).toEqual(1); + }); + + it("should not return an id from /api/User/new", () => { + expect(getResourceIdFromUrl("/api/User/new", "User")).toEqual(undefined); + }); + + it("should not return an id from /api/Dummy/--__", () => { + expect(getResourceIdFromUrl("/api/Dummy/--__", "User")).toEqual( + undefined + ); + }); + }); + + describe("getResourceIdFromParam", () => { + it("should get the id from /api/User/1", () => { + expect(getResourceIdFromParam("1", "User")).toEqual(1); + }); + + it("should not return an id from /api/User/new", () => { + expect(getResourceIdFromParam("new", "User")).toEqual(undefined); + }); + + it("should not return an id from /api/Dummy/--__", () => { + expect(getResourceIdFromParam("--__", "User")).toEqual(NaN); + }); + }); +});