Skip to content

Commit

Permalink
feat: alias prefix for dataprovider
Browse files Browse the repository at this point in the history
feat: rework options

alias prefix prefixes all queries and mutations. has to be configured on the backend as well

BREAKING CHANGE: some (undocumented) options are no longer supported
  • Loading branch information
macrozone committed Aug 13, 2020
1 parent 40acd00 commit ae62805
Show file tree
Hide file tree
Showing 13 changed files with 388 additions and 234 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ Packages to connect [react-admin](https://marmelab.com/react-admin) 🧩 with [p
- [dataprovider](./packages/dataprovider/README.md): the dataprovider for the frontend
- [backend](./packages/backend/README.md): tools to help making a nexus-prisma backend compatible with the data-provider


## Usage


you need to add the [dataprovider](./packages/dataprovider/README.md) to your react-admin app.
you need to add the [dataprovider](./packages/dataprovider/README.md) to your react-admin app.
Your backend needs additional resolver for all the resoures you gonna need. [backend](./packages/backend/README.md) package will help you with making your backend compatible with the dataprovider!

See the readmes above for further information.

- [dataprovider (frontend)](./packages/dataprovider/README.md)
- [backend](./packages/backend/README.md)

## Develop

Expand All @@ -27,7 +27,6 @@ Some tests use an actual introspection result from a nexus-prisma backend. If yo
- edit [testSchema.ts](packages/dataprovider/test-data/testSchema.ts) to modify the nexus types
- call `yarn install` in the root to update all types and the test introspection schema


## Prior work

loosly based on https://github.com/Weakky/ra-data-opencrud, thanks @Weakky
126 changes: 91 additions & 35 deletions packages/backend/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
# `@ra-data-prisma/backend`

this package makes your graphql-nexus api compatible with the dataprovider by exposing all needed Queries and Mutations.

It can be used for nexus framework and @nexus/schema

## Usage with nexus framework

`yarn add @ra-data-prisma/backend`

make sure to use `nexus-plugin-prisma`.

**important: set `paginationStrategy: "prisma"` and `experimentalCRUD: true` as options for `nexus-plugin-prisma`**

`nexusAddCrudResolvers(schemaFromNexus, resources, options)` will make your resources compatible with react-admin:

```
import { schema, use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
import { shield } from "nexus-plugin-shield"; // recommended for security, see below
import { nexusAddCrudResolvers } from "@ra-data-prisma/backend";
// import your nexus models here
use(
prisma({
paginationStrategy: "prisma",
features: {
crud: true,
},
})
);
// there is a new key-value api to define all resources:
// 👇 here is the magic
nexusAddCrudResolvers(schema, { // 👈 passing schema is currently needed (maybe solved with a proper nexus plugin)
User: {
// options for users here
},
BlogPost: {
// options for blogposts here
},
}, {
// common options here
});
```

make sure to pass `schema` as first argument to `nexusAddCrudResolvers`, which is simply `import { schema } from "nexus"`.

_(using `import { schema } from "nexus"` directly inside the package here did not work for me as it lead to weird type errors. If someone knows how to fix it, please open a PR!)_

## Usage with @nexus/schema

`yarn add @ra-data-prisma/backend`
Expand All @@ -8,7 +60,7 @@ make sure that you use `@nexus/schema` version `^0.14.0` and `nexus-prisma` vers

**important: set `paginationStrategy: "prisma"` and `experimentalCRUD: true` as options for nexusPrismaPlugin`**

`addCrudResolvers(modelName)` will make your Model compatible with react-admin. It will become a `Resource` to react-admin:
`addCrudResolvers(modelName, options)` will make your Model compatible with react-admin. It will become a `Resource` to react-admin:

```
Expand Down Expand Up @@ -51,10 +103,13 @@ const schema = makeSchema({
```

`addCrudResolvers` will add all needed `Mutation`'s and `Query`'s.
Make sure that you restrict access to these resolvers using something like [graphql-shield](https://github.com/maticzav/graphql-shield)
use `addCrudResolvers` for every Model that you want to manage in react-admin. Additionaly if you have a relation between two Models, call it for both Models even if you only want to show one in a list

## Security

Make sure that you restrict access to these resolvers using either [nexus-plugin-shield](https://github.com/lvauvillier/nexus-plugin-shield) (nexus framework) or [graphql-shield](https://github.com/maticzav/graphql-shield) (@nexus/schema)

this could look like this:
this could look like this (for @nexus/schema)

```
import { rule, allow, shield } from "graphql-shield";
Expand Down Expand Up @@ -100,50 +155,51 @@ export default new ApolloServer({
```

in larger projects its recommended that you simply expose a second graphql-endpoint that contains only the admin-CRUD queries and mutations.
The permission handling will then be very simple:
### prefix all queries and mutations

To make it more obvious which resolvers are for the admin area (and therefore need access control), we recommend to set `aliasPrefix`:

_nexus framework_

```
const permissions = shield(
{
nexusAddCrudResolvers(schema, {
User: {
},
{
fallbackRule: isAdmin
}
);
BlogPost: {
},
}, {
aliasPrefix: "admin" // 👈 this will prefix all queries and mutations with `admin`
});
```

use `addCrudResolvers` for every Model that you want to manage in react-admin. Additionaly if you have a relation between two Models, call it for both Models even if you only want to show one in a list
For example for resource `User` your queries will be:

- adminUser
- adminUsers
- adminCreateOneUser
- etc.

## Usage with nexus framework (experimental)
_nexus schema_

```
import { schema, use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
addCrudResolvers("User", {aliasPrefix: "admin"})
import { shield } from "nexus-plugin-shield"; // recommended, it uses graphql-shield like above
```

import { nexusAddCrudResolvers } from "@ra-data-prisma/backend";
\*\*Make sure that your dataprovider uses the same `aliasPrefix` as well.

// import your nexus models here
### alternative approach

use(
prisma({
paginationStrategy: "prisma",
features: {
crud: true,
},
})
);
in larger projects it could be a good idea that you simply expose a second graphql-endpoint that contains only the admin-CRUD queries and mutations.
The permission handling will then be very simple:

// there is a new key-value api to define all resources:
nexusAddCrudResolvers(schema, {
User: {
// options here
},
BlogPost: {
// options here
```
const permissions = shield(
{
},
});
{
fallbackRule: isAdmin
}
);
```
20 changes: 9 additions & 11 deletions packages/dataprovider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,7 @@ Data provider for [react admin](https://github.com/marmelab/react-admin)

make sure you backend api is compatible by using the other package in this repo [backend](../backend/README.md)

```
import buildGraphQLProvider from '@ra-data-prisma/dataprovider'
const dataprovider = await buildGraphQLProvider({
clientOptions: { uri: "/api/graphql" }
})
```

and add it to your admin app. We suggest to use our hook for that:
Add the dataprovider to your react-admin app:

```
Expand All @@ -31,7 +22,10 @@ import useAuthProvider from "./useAuthProvider"
const AdminApp = () => {
const dataProvider = useDataProvider()
const dataProvider = useDataProvider({
clientOptions: { uri: "/graphql" }
aliasPrefix: "admin" // 👈 set this, if you use a aliasPrefix on your backend as well (recommended)
})
const authProvider = useAuthProvider()
if (!dataProvider) {
Expand Down Expand Up @@ -61,6 +55,10 @@ export default AdminApp

## Features

### aliasPrefix

Set `aliasPrefix` if you have set it on the backend as well (see [backend](./packages/backend/README.md) ).

### Search & Filtering

this dataprovider supports all filtering and searching and adds some convenience to it:
Expand Down
63 changes: 63 additions & 0 deletions packages/dataprovider/src/buildDataProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import merge from "lodash/merge";
import buildRaGraphqlDataProvider from "ra-data-graphql";
import { DELETE, DELETE_MANY, UPDATE, UPDATE_MANY } from "react-admin";
import { buildQueryFactory } from "./buildQuery";
import { Options } from "./types";

import { makeIntrospectionOptions } from "./utils/makeIntrospectionOptions";

export const defaultOptions: Options = {
clientOptions: { uri: "/graphql" },
};

const buildDataProvider = (options: Options) => {
return buildRaGraphqlDataProvider(
merge(
{},
defaultOptions,
{
buildQuery: buildQueryFactory,
introspection: makeIntrospectionOptions(options),
},
options,
),
).then((graphQLDataProvider) => {
return (
fetchType: string,
resource: string,
params: { [key: string]: any },
): Promise<any> => {
// Temporary work-around until we make use of updateMany and deleteMany mutations
if (fetchType === DELETE_MANY) {
const { ids, ...otherParams } = params;
return Promise.all(
params.ids.map((id: string) =>
graphQLDataProvider(DELETE, resource, {
id,
...otherParams,
}),
),
).then((results) => {
return { data: results.map(({ data }: any) => data.id) };
});
}

if (fetchType === UPDATE_MANY) {
const { ids, ...otherParams } = params;
return Promise.all(
params.ids.map((id: string) =>
graphQLDataProvider(UPDATE, resource, {
id,
...otherParams,
}),
),
).then((results) => {
return { data: results.map(({ data }: any) => data.id) };
});
}
return graphQLDataProvider(fetchType, resource, params);
};
});
};

export default buildDataProvider;
70 changes: 70 additions & 0 deletions packages/dataprovider/src/buildGqlQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { getTestIntrospection } from "./testUtils/getTestIntrospection";

describe("buildGqlQuery", () => {
let testIntrospection: IntrospectionResult;
let testIntrospectionWithPrefix: IntrospectionResult;
let testUserResource: Resource;
beforeAll(async () => {
testIntrospection = await getTestIntrospection();
Expand Down Expand Up @@ -479,5 +480,74 @@ describe("buildGqlQuery", () => {
`,
);
});

describe("with prefixed introspections", () => {
beforeAll(async () => {
testIntrospection = await getTestIntrospection({
aliasPrefix: "admin",
});
testUserResource = testIntrospection.resources.find(
(r) => r.type.kind === "OBJECT" && r.type.name === "User",
);
});

it("allows to prefix all queries with a prefix", () => {
expect(
buildGqlQuery(testIntrospection)(
testUserResource,
GET_LIST,
{ where },
null,
),
).toEqualGraphql(
gql`
query adminUsers($where: UserWhereInput) {
items: adminUsers(where: $where) {
id
email
firstName
lastName
yearOfBirth
roles {
id
}
gender
wantsNewsletter
userSocialMedia {
id
instagram
twitter
user {
id
}
}
blogPosts {
id
}
}
total: adminUsersCount(where: $where)
}
`,
);
});
it("allows to prefix all mutations with a prefix", () => {
expect(
buildGqlQuery(testIntrospection)(
testUserResource,
DELETE,
{ where },
null,
),
).toEqualGraphql(
gql`
mutation adminDeleteOneUser($where: UserWhereUniqueInput!) {
data: adminDeleteOneUser(where: $where) {
id
}
}
`,
);
});
});
});
});
Loading

0 comments on commit ae62805

Please sign in to comment.