# 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
