Skip to content

Commit

Permalink
Merge pull request #882 from CruGlobal/7856-native-rails-api
Browse files Browse the repository at this point in the history
[MPDX-7856] Use Rails GraphQL whenever possible
  • Loading branch information
canac authored Mar 28, 2024
2 parents 5003e45 + 52a17ad commit 0416fbd
Show file tree
Hide file tree
Showing 160 changed files with 1,206 additions and 1,984 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"forwardPorts": [3000],

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "yarn install && yarn gql && yarn gql:server",
"postCreateCommand": "yarn install && yarn gql",

// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node"
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: 📈 Run GraphQL Codegen
uses: nick-fields/retry@v3
with:
command: yarn gql && yarn gql:server
command: yarn gql
timeout_minutes: 1
retry_wait_seconds: 60
max_attempts: 5
Expand All @@ -53,7 +53,7 @@ jobs:
- name: 📈 Run GraphQL Codegen
uses: nick-fields/retry@v3
with:
command: yarn gql && yarn gql:server
command: yarn gql
timeout_minutes: 1
retry_wait_seconds: 60
max_attempts: 5
Expand All @@ -72,7 +72,7 @@ jobs:
- name: 📈 Run GraphQL Codegen
uses: nick-fields/retry@v3
with:
command: yarn gql && yarn gql:server
command: yarn gql
timeout_minutes: 1
retry_wait_seconds: 60
max_attempts: 5
Expand Down Expand Up @@ -131,7 +131,7 @@ jobs:
- name: 📈 Run GraphQL Codegen
uses: nick-fields/retry@v3
with:
command: yarn gql && yarn gql:server
command: yarn gql
timeout_minutes: 1
retry_wait_seconds: 60
max_attempts: 5
Expand Down
16 changes: 5 additions & 11 deletions .husky/post-checkout
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
digest='graphql-digest.txt'
if [ -e $digest ]; then
previous_hash=`cat $digest`
current_hash=`cat $(find . -type f -name "*.graphql") | openssl dgst -sha256`
if [ "$previous_hash" != "$current_hash" ]; then
# Only update the hash if the gql codegen succeeded
yarn gql && yarn gql:server && echo $current_hash > $digest
fi
else
current_hash=`cat $(find . -type f -name "*.graphql") | openssl dgst -sha256`
yarn gql && yarn gql:server && echo $current_hash > $digest
digest_file='src/graphql/graphql-digest.txt'
current_hash=`cat $(find . -type f -name "*.graphql") | openssl dgst -sha256`
if [ ! -e $digest_file ] || [ `cat $digest_file` != "$current_hash" ]; then
# Only update the hash if the gql codegen succeeded
yarn gql && echo $current_hash > $digest_file
fi
414 changes: 8 additions & 406 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,7 @@ Once you have these variables you can install the dependencies.
yarn
```

Next, generate types for REST -> GraphQL:

```bash
yarn gql:server
```

Then, run create GraphQL generated files:
Next, run the GraphQL codegen:

```bash
yarn gql
Expand Down Expand Up @@ -139,21 +133,36 @@ This project uses GraphQL to load data from the API server. GraphQL allows us to

### Apollo Studio

Apollo Studio is an invaluable tool/interface for testing out queries and mutations during development. If your dev server is running (use `yarn start` to start it), you can access Apollo Studio at http://localhost:3000/api/graphql. Once you navigate to Apollo Studio, you will need to set some a header to authenticate with the API server.
Apollo Studio is an invaluable tool/interface for testing out queries and mutations during development. As described in the [architecture](#architecture) section, there are two distinct GraphQL servers that MPDX communicates with: the GraphQL API server and the REST proxy server. To figure out which server to use, you may need check whether your field is in `src/graphql/rootFields.generated.ts`. If it is, you should use the GraphQL API server. Otherwise, use the REST proxy server.

#### Apollo Studio for the GraphQL API Server

You can access Apollo Studio for the GraphQL API server at https://studio.apollographql.com/sandbox/explorer?endpoint=https://api.stage.mpdx.org/graphql. Once you navigate to Apollo Studio, you will need to set a header to authenticate with the API server.

1. Go to https://stage.mpdx.org and login.
2. Open developer tools and run `localStorage.getItem('token')` in the console.
3. Copy the value of the token.
4. Back in Apollo Studio, find the Headers tab near the bottom and click the "Set shared headers" button.
5. In the modal, click the "+ New shared header button" and choose "Authorization" for the header key. For the value, type `Bearer` followed by a space followed by the value of the token.
6. After you click save, all queries and mutations you make in Apollo Studio will be authenticated.

#### Apollo Studio for the REST Proxy Server

If your dev server is running (use `yarn start` to start it), you can access Apollo Studio for the REST proxy server at http://localhost:3000/api/graphql-rest. Once you navigate to Apollo Studio, you will need to set a header to authenticate with the API server.

1. Go to http://localhost:3000 and login.
2. Open Chrome DevTools and go to the Application tab.
3. In the sidebar, click on Cookies > http://localhost:3000 in the Storage section.
4. Copy the value of the `next-auth.session-token` cookie.
5. Back in Apollo Studio, find the Headers tab near the bottom and click the "Set shared headers" button.
6. In the modal, click the "+ New shared header button" and choose "Authorization" for the header key and the word `Bearer` followed by a space followed by the value of the `next-auth.session-token` cookie.
6. In the modal, click the "+ New shared header button" and choose "Authorization" for the header key. For the value, type `Bearer` followed by a space followed by the value of the `next-auth.session-token` cookie.
7. After you click save, all queries and mutations you make in Apollo Studio will be authenticated.

Apollo also has a Chrome browser extension that will add an Apollo tab to Chrome DevTools. The extension lets you view the queries and mutations that the page has made and execute queries and mutations without needing to manually configure authorization headers. You can install it [here](https://chromewebstore.google.com/detail/jdkknkkbebbapilgoeccciglkfbmbnfm).

### Using a Query

To load data in your component, the first step will be to write an operation definition based on the query and fields that your component needs. The easiest way to do this is to go to [Apollo Studio](http://localhost:3000/api/graphql), click the plus sign next to the query you want to load, and then click the plus signs next to the fields you want to use in your component. On line 1, give the operation a name that describes the data it loads and is unique across the entire project (the `yarn gql` step below will fail and tell you if your operation name isn't unique). Also, make sure the operation starts with a capital letter. Then create a `.graphql` file with the same name as your component (i.e. if your component is `Partners.tsx`, the operation goes in `Partners.graphql`) and copy and paste the operation from Apollo Studio into it. It should look something like this:
To load data in your component, the first step will be to write an operation definition based on the query and fields that your component needs. The easiest way to do this is to go to [Apollo Studio](#apollo-studio), click the plus sign next to the query you want to load, and then click the plus signs next to the fields you want to use in your component. On line 1, give the operation a name that describes the data it loads and is unique across the entire project (the `yarn gql` step below will fail and tell you if your operation name isn't unique). Also, make sure the operation starts with a capital letter. Then create a `.graphql` file with the same name as your component (i.e. if your component is `Partners.tsx`, the operation goes in `Partners.graphql`) and copy and paste the operation from Apollo Studio into it. It should look something like this:

```gql
# Partners.graphql
Expand Down Expand Up @@ -328,17 +337,23 @@ To learn more about Apollo's cache normalization, [this](https://www.apollograph

### Architecture

Originally, the API server provided a REST API for reading and writing data that all clients used. However, with the rewrite of the web client in React, it was decided to use GraphQL. The API server now exposes a GraphQL endpoint in addition to the original REST API. However, the GraphQL API doesn't yet fully expose all of the data that was available in the REST API. To prevent the web client from having to query some data through GraphQL and other data through the REST API, this project includes a REST->GraphQL proxy. It essentially extends the API server's GraphQL schema with additional queries and mutations. When the web client uses those additional queries via the extended GraphQL API, the proxy makes a request to the REST API, manipulates the data as necessary, and returns the response back to the client in the GraphQL response. As complicated as this seems, the important part is that the client _doesn't have to know_ whether a particular query is ultimately satisfied by the API server's native GraphQL API or by the REST API. It can make queries and let the proxy worry about routing queries to the correct place.
Originally, the API server provided a REST API for reading and writing data that all clients used. However, with the rewrite of the web client in React, it was decided to use GraphQL. The API server now exposes a GraphQL endpoint in addition to the original REST API. To see the queries and mutations provided by the API server, you can go [here](https://studio.apollographql.com/sandbox/explorer?endpoint=https://api.mpdx.org/graphql).

However, the GraphQL API doesn't yet fully expose all of the data that was available in the REST API. To prevent the web client from having to query some data through GraphQL and other data through the REST API, this project includes a REST->GraphQL proxy. The REST proxy is a GraphQL server hosted at `/api/graphql-rest`. It runs as a Next.js lambda. It receives GraphQL requests, makes a fetch request to the REST API, manipulates the data as necessary, and returns the response back to the client in the GraphQL response. To see the queries and mutations provided by the REST proxy server, you can go [here](https://studio.apollographql.com/sandbox/explorer?endpoint=https://next.mpdx.org/api/graphql-rest).

Additionally, there is logic in the Apollo link to route GraphQL operations to the correct GraphQL server. The link inspects the operation, and if it contains fields provided by the GraphQL API server, it forwards the request to that server. And if it contains fields provided by the REST proxy server, it forwards the request to that server.

The only caveat is that operations cannot mix fields from the GraphQL API server and the REST proxy server. For example, if a component needs to query the `contact` field provided by the GraphQL API server and the `designationAccounts` field provided by the REST proxy server, it will have to split the operation into two separate operations. If the operation were sent as-is to the GraphQL API server, it would fail because it doesn't know about the `designationAccounts` field. And if it were sent to the REST proxy server, it would fail because it doesn't know about the `contact` field. To see the fields provided by the GraphQL API server, look at the generated file `src/graphql/rootFields.generated.ts`.

The extended GraphQL server is implemented in JavaScript and is available at `/api/graphql` on the domain that the web client is running on. To see the queries and mutations provided by the API server, you can go [here](https://studio.apollographql.com/sandbox/explorer?endpoint=https://api.mpdx.org/graphql). To see the extended set of queries and mutations provided by the web client's supergraph, you can go [here](https://studio.apollographql.com/sandbox/explorer?endpoint=https://next.mpdx.org/api/graphql).
As complicated as this seems, the important part is that the client _doesn't have to know_ whether a particular query is ultimately satisfied by the API server's native GraphQL API or by REST proxy. It can make queries and let the link figure out which server to route queries to.

### Adding REST Proxy Queries

To add a new GraphQL query that interacts with the REST API, you will need to follow several steps.

1. Find an existing query folder in `pages/api/Schema` (`CoachingAnswerSets` is a good starting point), make a copy of it, and rename it to the name of the query you are adding.
2. In the folder you just created, rename the `.graphql` file to the name of the query you are adding. Adjust the `extend type Query {}` section that `.graphql` file to contain the query or queries that you are adding, including their arguments and return types. Define the types of the return types of your query in the rest of the file.
3. Run `yarn gql:server` to generate TypeScript definitions for the new queries. You will need to run this after every time you modify a `.graphql` file in `pages/api/Schema`. If you want it to rerun automatically when it detects changes, run `yarn gql:server:w`.
3. Run `yarn gql` to generate TypeScript definitions for the new queries. You will need to run this after every time you modify a `.graphql` file in `pages/api/Schema`. If you want it to rerun automatically when it detects changes, run `yarn gql:w`.
4. In `pages/api/graphql-rest.page.ts` add a method to the `MpdxRestApi` class that makes a request to the REST API.
5. In the `dataHandler.ts` file for your query, rename the exported method to be appropriate for your query and modify it so that it takes the response from the REST API and returns data in the format returned by the GraphQL query. Make sure that the types match what you defined in the `.graphql` file. Also, make sure that the method you added to `graphql-rest.page.ts` imports and calls your datahandler.
6. In the `resolvers.ts` file for your query, make sure that the `Query` property on the exported resolvers contains one property for each query you are adding (and the same for the `Mutation` property if you are adding mutations). The second argument of each of those query resolver functions will be an object containing the inputs to your query. Make sure they match the inputs you defined in your `.graphql`. Also make sure that the resolver functions call the `dataSources.mpdxRestApi` method that you defined in `graphq-rest.page.ts`.
Expand Down
14 changes: 14 additions & 0 deletions __tests__/fixtures/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Session } from 'next-auth';

export const session: Session = {
expires: '2021-10-28T14:48:20.897Z',
user: {
name: 'First Last',
email: '[email protected]',
apiToken: 'apiToken',
userID: 'user-1',
admin: false,
developer: false,
impersonating: false,
},
};
4 changes: 2 additions & 2 deletions __tests__/pages/api/handoff.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getToken } from 'next-auth/jwt';
import { createMocks } from 'node-mocks-http';
import makeSsrClient from 'pages/api/utils/ssrClient';
import makeSsrClient from 'src/lib/apollo/ssrClient';
import handoff from '../../../pages/api/handoff.page';

jest.mock('next-auth/jwt', () => ({ getToken: jest.fn() }));
jest.mock('pages/api/utils/ssrClient', () => jest.fn());
jest.mock('src/lib/apollo/ssrClient', () => jest.fn());

describe('/api/handoff', () => {
const OLD_ENV = process.env;
Expand Down
4 changes: 2 additions & 2 deletions __tests__/pages/api/stopImpersonating.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getToken } from 'next-auth/jwt';
import { createMocks } from 'node-mocks-http';
import makeSsrClient from 'pages/api/utils/ssrClient';
import makeSsrClient from 'src/lib/apollo/ssrClient';
import stopImpersonating from '../../../pages/api/stop-impersonating.page';

jest.mock('next-auth/jwt', () => ({ getToken: jest.fn() }));
jest.mock('pages/api/utils/ssrClient', () => jest.fn());
jest.mock('src/lib/apollo/ssrClient', () => jest.fn());
// User one
const userOneImpersonate = 'userOne.impersonate.token';

Expand Down
2 changes: 1 addition & 1 deletion __tests__/util/graphqlMocking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { gql } from 'graphql-tag';
import seedrandom from 'seedrandom';
import { DeepPartial } from 'ts-essentials';
import schema from 'src/graphql/schema.graphql';
import { createCache } from 'src/lib/apolloCache';
import { createCache } from 'src/lib/apollo/cache';

const seed = 'seed';
const rng = seedrandom(seed);
Expand Down
21 changes: 7 additions & 14 deletions __tests__/util/setup.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import '@testing-library/jest-dom/extend-expect';
import 'isomorphic-fetch';
import { Settings } from 'luxon';
import { Session } from 'next-auth';
import { type useSession } from 'next-auth/react';
import { session } from '__tests__/fixtures/session';
import { toHaveGraphqlOperation } from '../extensions/toHaveGraphqlOperation';
import matchMediaMock from './matchMediaMock';

const session: Session = {
expires: '2021-10-28T14:48:20.897Z',
user: {
name: 'First Last',
email: '[email protected]',
apiToken: 'apiToken',
userID: 'user-1',
admin: false,
developer: false,
impersonating: false,
},
};

jest.mock('next-auth/react', () => {
return {
getSession: jest.fn().mockResolvedValue(session),
Expand All @@ -28,6 +16,7 @@ jest.mock('next-auth/react', () => {
data: session,
update: () => Promise.resolve(null),
}),
signIn: jest.fn().mockResolvedValue(undefined),
signOut: jest.fn().mockResolvedValue(undefined),
};
});
Expand All @@ -50,6 +39,10 @@ window.document.createRange = (): Range =>
} as unknown as Node,
} as unknown as Range);

Object.defineProperty(window, 'location', {
value: { ...window.location, assign: jest.fn(), replace: jest.fn() },
});

window.HTMLElement.prototype.scrollIntoView = jest.fn();

beforeEach(() => {
Expand Down
1 change: 0 additions & 1 deletion amplify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ frontend:
- yarn -v
- yarn
- yarn disable-telemetry
- yarn gql:server
- yarn gql
build:
commands:
Expand Down
19 changes: 0 additions & 19 deletions codegen-server.yml

This file was deleted.

38 changes: 32 additions & 6 deletions codegen.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
overwrite: true
schema:
- ${API_URL:https://api.stage.mpdx.org/graphql}
- ./pages/api/Schema/**/*.graphql
documents: '**/*.graphql'
generates:
./src/graphql/types.generated.ts:
schema:
- ${API_URL:https://api.stage.mpdx.org/graphql}
- ./pages/api/Schema/**/*.graphql
plugins:
- add:
content: '/* eslint-disable */'
Expand All @@ -14,6 +13,10 @@ generates:
ISO8601Date: string
ISO8601DateTime: string
./:
schema:
- ${API_URL:https://api.stage.mpdx.org/graphql}
- ./pages/api/Schema/**/*.graphql
documents: '**/*.graphql'
preset: near-operation-file
presetConfig:
baseTypesPath: src/graphql/types.generated.ts
Expand All @@ -25,13 +28,36 @@ generates:
config:
preResolveTypes: false
./src/graphql/schema.graphql:
schema:
- ${API_URL:https://api.stage.mpdx.org/graphql}
- ./pages/api/Schema/**/*.graphql
plugins:
- schema-ast
config:
federation: true
./src/graphql/possibleTypes.generated.ts:
schema:
- ${API_URL:https://api.stage.mpdx.org/graphql}
- ./pages/api/Schema/**/*.graphql
plugins:
- fragment-matcher
./pages/api/graphql-rest.page.generated.ts:
schema:
- ${API_URL:https://api.stage.mpdx.org/graphql}
- ./pages/api/Schema/**/*.graphql
plugins:
- add:
content: '/* eslint-disable */'
- typescript
- typescript-resolvers
config:
useIndexSignature: true
contextType: ./graphql-rest.page#Context
scalars:
ISO8601Date: string
ISO8601DateTime: string
./src/graphql/rootFields.generated.ts:
schema: ${API_URL:https://api.stage.mpdx.org/graphql}
plugins:
- ./extractRootFields.js
hooks:
afterAllFileWrite:
- prettier --write
Loading

0 comments on commit 0416fbd

Please sign in to comment.