ra-data-simple-prisma
Version:
Simple react-admin dataprovider for prisma, supporting audit logs and roles
501 lines (426 loc) • 13.4 kB
Markdown
# 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