ra-data-simple-prisma
Version:
Simple react-admin dataprovider for prisma, supporting audit logs and roles
367 lines (317 loc) • 9.05 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.
### 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, {
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",
audit: ...
debug: ...
});
break;
case "deleteMany":
await deleteManyHandler<Prisma.PostDeleteManyArgs>(req.body, prismaClient, {
softDeleteField: "deletedAt",
audit: ...
debug: ...
});
break;
case "getList":
await getListHandler<Prisma.PostFindManyArgs>(
req.body,
prismaClient,
{
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 compoenent
await getInfiniteListHandler<Prisma.PostFindManyArgs>(
req.body,
prismaClient,
{
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,
);
break;
case "getManyReference":
await getManyReferenceHandler<Prisma.PostFindManyArgs>(
req.body,
prismaClient,
);
break;
case "getOne":
await getOneHandler<Prisma.PostFindUniqueArgs>(
req.body,
prismaClient,
{
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,
{
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;
}
}
```
### 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