UNPKG

ra-data-simple-prisma

Version:

Simple react-admin dataprovider for prisma, supporting audit logs and roles

501 lines (426 loc) 13.4 kB
# React Admin + Prisma 🤝 Create a fullstack react-admin app adding just one file on the server! Most of the examples will use Next.js but you can use any node-based server-side framework. ### Installation ``` npm i ra-data-simple-prisma yarn add ra-data-simple-prisma pnpm i ra-data-simple-prisma ``` ### Frontend: import the DataProvider ```js import { Admin, Resource } from "react-admin"; import { dataProvider } from "ra-data-simple-prisma"; const ReactAdmin = () => { return ( <Admin dataProvider={dataProvider("/api", options)}> <Resource name="users" /> </Admin> ); }; export default ReactAdmin; ``` ### Backend: import the request handlers Simplest implementation ever: ```js // -- Example for Next Pages router -- // /api/[resource].ts <= catch all resource requests import { defaultHandler } from "ra-data-simple-prisma"; import { prismaClient } from "../prisma/client"; // <= Your prisma client instance export default async function handler(req, res) { const result = await defaultHandler(req.body, prismaClient); res.json(result); } // -- Example for Next App router -- // /app/api/[resource]/route.ts <= catch all resource requests import { defaultHandler } from "ra-data-simple-prisma"; import { prismaClient } from "../prisma/client"; // <= Your prisma client instance import { NextResponse } from "next/server"; const handler = async (req: Request) => { const body = await req.json(); const result = await defaultHandler(body, prismaClient); return NextResponse.json(result); }; export { handler as GET, handler as POST }; ``` ### (List) Filters: Available Operators To be used with an underscore after the `source` name - contains: prisma native operator (Default for string) - endsWith: prisma native operator - enum: to be used with enums, where exact match is required - eq: equals - exact: equals - gt: prisma native operator - gte: prisma native operator - has: prisma native operator - lt: prisma native operator - lte: prisma native operator - not: prisma native operator - search: prisma native operator - startsWith: prisma native operator - pgjson: if using postgres drill down the json field Example ```ts <List {...props} filters={[ <SelectInput label="Status" source={"status_enum"} />, <DateInput label="Created After or on" source={"created_at_gte"} />, <TextInput label="Full-text Body search" source={"body_search"} />, <TextInput label="User's language" source={"user.settings.language_enum"} // <= drill down in relationships />, <TextInput label="Metadata's subkey" source={"metadata_pgjson.key.subkey"} />, ]} > ``` ### Prisma Logical Operators Support - AND - OR - NOT Enabling complex filtering capabilities in React Admin applications. Previously, `ra-data-simple-prisma` did not support Prisma's logical operators, limiting users to simple field-based filtering. Complex queries requiring logical combinations of conditions were not possible. We can use it on navigation for example ```ts const OR = [ { amount: { gte: 1000 }, status: "ACTIVE" }, { amount: { lt: 500 }, status: "REJECTED" }, ]; navigate(`/resource_name?filter=${JSON.stringify({ OR })}`); ``` ### With audit log ```js export default function handler(req) { const session = await getServerSession(...); await defaultHandler(req.body, prismaClient, { audit: { model: prismaClient.audit_log, authProvider: authProvider(session) }, }); ... } ``` audit: - model: The prisma model of the `audit log` table eg. `prisma.auditLog` - authProvider: Insert your AuthProvider from React-Admin - columns?: Map fields to your database columns `{id: "_id", date: "created_at"}` - enabledForAction?: Enabled for which action eg. `{create: true, update: true, delete: false}` - enabledResources?: List of resources which are to be audited. Defaults to all. The `payload` column stores a `AuditLogPayload` object: ```ts type AuditLogPayload = { id: Identifier; // record id data?: object; // the new values sent by react-admin (present on create / update) previousData?: object; // the record values before the change (present on update) }; ``` - `data` is populated from `request.params.data` — i.e. the fields the user submitted in the create/update form. - `previousData` is populated from `request.params.previousData` — i.e. the full record as it existed before the update. React-Admin sends this automatically when editing a record. - Neither field is present on `delete` actions (only `id` is stored). ### Overrides All dataProvider methods can be overridden for a given resource, or all. ```js // /api/post.ts <= override default handler for specific resource export default function handler(req) { switch (req.body.method) { case "create": await createHandler<Prisma.PostCreateArgs>(req.body, prismaClient, { allowOnlyFields: { title: true, body: true, tagIds: true, }, connect: { tags: "id", // or tagIds: { tag: "id", }, // or mediaIds: { postToMediaRels: { media: "id", } }, }, audit: ... debug: ... }); return NextResponse.json(...); case "delete": await deleteHandler<Prisma.PostDeleteArgs>(req.body, prismaClient, { softDeleteField: "deletedAt", primaryKey: ... // defaults to "id" audit: ... debug: ... }); break; case "deleteMany": await deleteManyHandler<Prisma.PostDeleteManyArgs>(req.body, prismaClient, { softDeleteField: "deletedAt", primaryKey: ... // defaults to "id" audit: ... debug: ... }); break; case "getList": await getListHandler<Prisma.PostFindManyArgs>( req.body, prismaClient, { primaryKey: ... // defaults to "id" select: ... where: ... noNullsOnSort: ... filterMode: ... debug: ... include: { tags: true }, transformRow: (post: ServerPost, postIndex: number, posts: ServerPost[]): AugmentedPost => { return { ...post tagIds: post.tags.map((tag) => tag.id); } }, } ); // OR, if using InfiniteList component await getInfiniteListHandler<Prisma.PostFindManyArgs>( req.body, prismaClient, { primaryKey: ... // defaults to "id" select: ... where: ... noNullsOnSort: ... filterMode: ... debug: ... include: { tags: true }, transformRow: (post: ServerPost, postIndex: number, posts: ServerPost[]): AugmentedPost => { return { ...post tagIds: post.tags.map((tag) => tag.id); } }, } ); break; case "getMany": await getManyHandler<Prisma.PostFindManyArgs>( req.body, prismaClient, { primaryKey: ... // defaults to "id" } ); break; case "getManyReference": await getManyReferenceHandler<Prisma.PostFindManyArgs>( req.body, prismaClient, { primaryKey: ... // defaults to "id" } ); break; case "getOne": await getOneHandler<Prisma.PostFindUniqueArgs>( req.body, prismaClient, { primaryKey: ... // defaults to "id" select: ... include: ... transform: (post: any) => { post._computedProp = ... }, transform: async ( post: QueryPost ): Promise<QueryPost & { _extraPropAfterTransform: true }> => { return { ...post, _extraPropAfterTransform: await Promise.resolve(true), }; }, } ) break; case "update": await updateHandler<Prisma.PostUpdateArgs>( req.body, prismaClient, { primaryKey: ... // defaults to "id", also used by updateMany allowOnlyFields: { title: true, body: true, tagIds: true, }, skipFields: { computedField: true }, set: { tags: "id", }, allowNestedUpdate: { user_settings: true, fixed_settings: false, }, allowNestedUpsert: { other_settings: true }, allowJsonUpdate: { raw_data_field: true; } } ); break; case "updateMany": await updateManyHandler<Prisma.PostUpdateManyArgs>( req.body, prismaClient, { skipFields: { computedField: true }, set: { tags: "id", }, } ); break; default: // <= fall back on default handler await defaultHandler(req.body, prismaClient, { audit: ... create: ... delete: ... getList: ... getMany: ... getManyReference: ... getOne: ... update: ... }); break; } } ``` ### Custom Primary Key By default all handlers use `id` as the primary key field, matching the react-admin data connector convention. If your Prisma model uses a different primary key name (e.g. `Id`, `StatusId`, `postId`) you can configure it per-handler via the `primaryKey` option. This affects: - how the `WHERE` clause is built for single-record lookups and multi-record `{ in: ids }` filters - how incoming write payloads are handled and returned records are normalized for react-admin compatibility ```ts // /api/status.ts — model with a non-standard primary key "StatusId" case "getOne": return getOneHandler(req.body, prismaClient, { primaryKey: "StatusId", }); case "getMany": return getManyHandler(req.body, prismaClient, { primaryKey: "StatusId", }); ... ``` ### Allow Only Fields Both `createHandler` and `updateHandler` support an `allowOnlyFields` option that acts as an explicit allow-list of fields that may be written. ```ts // create await createHandler(req.body, prismaClient, { allowOnlyFields: { title: true, body: true, tagIds: true, }, }); // update await updateHandler(req.body, prismaClient, { allowOnlyFields: { title: true, body: true, tagIds: true, }, }); ``` > **Note:** Fields with an empty string value (`""`) and internal `_`-prefixed fields (e.g. `_count`) are stripped automatically before the allow-list is checked, so they will never trigger an error. ### Omit Fields The `omit` option is supported by `getListHandler`, `getOneHandler`, `getManyHandler`, `getManyReferenceHandler`, `createHandler`, and `updateHandler`. It maps directly to [Prisma's `omit` clause](https://www.prisma.io/docs/orm/prisma-client/queries/select-fields#omitting-fields), letting you exclude specific fields from query results without having to enumerate all the fields you _do_ want (as you would with `select`). This is useful for stripping sensitive fields (e.g. `password`, `secret`) or large fields you never need in the admin UI. ```ts // Exclude the password hash from every user record returned await getListHandler<Prisma.UserFindManyArgs>(req.body, prismaClient, { omit: { password_hash: true, }, }); await getOneHandler<Prisma.UserFindUniqueArgs>(req.body, prismaClient, { omit: { password_hash: true, }, }); await getManyHandler<Prisma.UserFindManyArgs>(req.body, prismaClient, { omit: { password_hash: true, }, }); await getManyReferenceHandler<Prisma.UserFindManyArgs>(req.body, prismaClient, { omit: { password_hash: true, }, }); // Also available on write handlers — omits fields from the returned record await createHandler<Prisma.UserCreateArgs>(req.body, prismaClient, { omit: { password_hash: true, }, }); await updateHandler<Prisma.UserUpdateArgs>(req.body, prismaClient, { omit: { password_hash: true, }, }); ``` > **Note:** `omit` and `select` are mutually exclusive in Prisma — passing both will cause a runtime error. ### Helpers Stuff you can use to write your own custom logic - extractOrderBy - extractSkipTake - extractWhere ### Permissions In your Api handler, call the function `canAccess` to infer if the user (session) can perform that particular action. Example in [admin demo](apps/admin/auth/checkAccess.ts) It will need the permission object which looks like this ``` export const permissionsConfig: PermissionsConfig = { OWNER: [{ action: "*", resource: "*" }], //admin can do anything COLLABORATOR: [ //collaborator can do anything except edit, delete, create admin users { action: "*", resource: "*" }, { type: "deny", action: ["edit", "delete", "create"], resource: "adminUser", }, ], READER: [{ action: ["list", "show", "export"], resource: "*" }], }; ``` ### Publish Use the example app to test the changes. In root folder run ``` pnpm publish ``` ### License MIT