Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor interceptors as middlewares #72

Merged
merged 34 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fda6faf
Refactor interceptors as middlewares
djhi Jun 3, 2024
c0c6b61
Fix Sinon
djhi Jun 3, 2024
af6b6e7
Rename BaseServer classes
djhi Jun 3, 2024
9096436
Add comment
djhi Jun 3, 2024
b1a0a00
Cleanup types
djhi Jun 4, 2024
5570922
Add withDelay middleware
djhi Jun 4, 2024
7e8e47e
Fix Sinon integration
djhi Jun 4, 2024
7b97ff8
Fix SinonServer tests
djhi Jun 4, 2024
a8d3050
Fix build
djhi Jun 5, 2024
d80d993
Update Upgrade Guide
djhi Jun 5, 2024
875b676
Fix upgrade guide
djhi Jun 6, 2024
d9a4982
Remove unnecessary sinon html file
djhi Jun 6, 2024
9ed57ce
Use FetchMockServer in example
djhi Jun 6, 2024
cb8e5f1
Refactor
djhi Jun 6, 2024
46a7912
Make msw example less verbose
djhi Jun 6, 2024
1ce980d
Remove debug code
djhi Jun 6, 2024
6d39911
Simplify sinon example
djhi Jun 6, 2024
e2f4c19
Better server side validation
djhi Jun 6, 2024
3c655d1
Remove unnecessary types
djhi Jun 6, 2024
8662b5e
Make sinon async compatible
djhi Jun 6, 2024
c59cce2
Reorganize tests
djhi Jun 6, 2024
7e883eb
Apply review suggestions
djhi Jun 6, 2024
0007f7b
Rename requestJson to requestBody
djhi Jun 6, 2024
f96e328
Update comments and readme
djhi Jun 6, 2024
b46a393
Simplify and document MSW
djhi Jun 6, 2024
6628014
Rewrite documentation
djhi Jun 6, 2024
a98bedd
Update documentation
djhi Jun 6, 2024
daf48e4
Don't extend Database
djhi Jun 6, 2024
55c76cb
Revert unnecessary changes
djhi Jun 6, 2024
f1c7732
Fix examples middlewares
djhi Jun 6, 2024
e0f62b9
Fix exports
djhi Jun 6, 2024
e993351
Add configuration section in docs
djhi Jun 6, 2024
f6ef7c2
Add concepts
djhi Jun 6, 2024
075ef27
Merge branch 'master' into refactor-interceptors
fzaninotto Jun 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ run-msw:
run-fetch-mock:
@NODE_ENV=development VITE_MOCK=fetch-mock npm run dev

run-sinon:
@NODE_ENV=development VITE_MOCK=sinon npm run dev

watch:
@NODE_ENV=development npm run build --watch

Expand Down
28 changes: 28 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,32 @@ server.init(data);
+ { id: 3, title: 'boz' },
+ ],
+});
```

## Request and Response Interceptors Have Been Replaced By Middlewares

Fakerest used to have request and response interceptors. We replaced those with middlewares. They allow much more use cases.

Migrate your request interceptors:

```diff
-restServer.addRequestInterceptor(function(request) {
+restServer.addMiddleware(async function(request, context, next) {
var start = (request.params._start - 1) || 0;
var end = request.params._end !== undefined ? (request.params._end - 1) : 19;
request.params.range = [start, end];
- return request; // always return the modified input
+ return next(request, context);
});
```

Migrate your response interceptors:

```diff
-restServer.addResponseInterceptor(function(response) {
+restServer.addMiddleware(async function(request, context, next) {
+ const response = await next(request, context);
response.body = { data: response.body, status: response.status };
return response;
});
```
35 changes: 30 additions & 5 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,31 @@ import React from 'react';
import {
Admin,
Create,
type DataProvider,
EditGuesser,
ListGuesser,
Resource,
ShowGuesser,
required,
AutocompleteInput,
} from 'react-admin';
import { dataProvider } from './dataProvider';
import { QueryClient } from 'react-query';

export const App = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});

export const App = ({ dataProvider }: { dataProvider: DataProvider }) => {
return (
<Admin dataProvider={dataProvider}>
<Admin
dataProvider={dataProvider}
authProvider={authProvider}
queryClient={queryClient}
>
<Resource
name="books"
list={ListGuesser}
Expand All @@ -24,18 +39,28 @@ export const App = () => {
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
recordRepresentation={(record) =>
`${record.first_name} ${record.last_name}`
}
/>
</Admin>
);
};

import { Edit, ReferenceInput, SimpleForm, TextInput } from 'react-admin';
import authProvider from './authProvider';

export const BookCreate = () => (
<Create>
<SimpleForm>
<ReferenceInput source="author_id" reference="authors" />
<TextInput source="title" />
<ReferenceInput source="author_id" reference="authors">
<AutocompleteInput validate={required()} />
</ReferenceInput>
<TextInput
source="title"
validate={required()}
defaultValue="Anna Karenina"
/>
</SimpleForm>
</Create>
);
51 changes: 51 additions & 0 deletions example/authProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type AuthProvider, HttpError } from 'react-admin';
import data from './users.json';

/**
* This authProvider is only for test purposes. Don't use it in production.
*/
export const authProvider: AuthProvider = {
login: ({ username, password }) => {
const user = data.users.find(
(u) => u.username === username && u.password === password,
);

if (user) {
const { password, ...userToPersist } = user;
localStorage.setItem('user', JSON.stringify(userToPersist));
return Promise.resolve();
}

return Promise.reject(
new HttpError('Unauthorized', 401, {
message: 'Invalid username or password',
}),
);
},
logout: () => {
localStorage.removeItem('user');
return Promise.resolve();
},
checkError: (error) => {
const status = error.status;
if (status === 401 || status === 403) {
localStorage.removeItem('auth');
return Promise.reject();
}
// other error code (404, 500, etc): no need to log out
return Promise.resolve();
},
checkAuth: () =>
localStorage.getItem('user') ? Promise.resolve() : Promise.reject(),
getPermissions: () => {
return Promise.resolve(undefined);
},
getIdentity: () => {
const persistedUser = localStorage.getItem('user');
const user = persistedUser ? JSON.parse(persistedUser) : null;

return Promise.resolve(user);
},
};

export default authProvider;
18 changes: 17 additions & 1 deletion example/dataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
import simpleRestProvider from 'ra-data-simple-rest';
import { fetchUtils } from 'react-admin';

export const dataProvider = simpleRestProvider('http://localhost:3000');
const httpClient = (url: string, options: any = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const persistedUser = localStorage.getItem('user');
const user = persistedUser ? JSON.parse(persistedUser) : null;
if (user) {
options.headers.set('Authorization', `Bearer ${user.id}`);
}
return fetchUtils.fetchJson(url, options);
};

export const dataProvider = simpleRestProvider(
'http://localhost:3000',
httpClient,
);
44 changes: 40 additions & 4 deletions example/fetchMock.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
import fetchMock from 'fetch-mock';
import FakeRest from 'fakerest';
import { FetchMockServer, withDelay } from 'fakerest';
import { data } from './data';
import { dataProvider as defaultDataProvider } from './dataProvider';

export const initializeFetchMock = () => {
const restServer = new FakeRest.FetchServer({
const restServer = new FetchMockServer({
baseUrl: 'http://localhost:3000',
data,
loggingEnabled: true,
});
if (window) {
// @ts-ignore
window.restServer = restServer; // give way to update data in the console
}
restServer.init(data);
restServer.toggleLogging(); // logging is off by default, enable it

restServer.addMiddleware(withDelay(300));
restServer.addMiddleware(async (request, context, next) => {
if (!request.headers?.get('Authorization')) {
return new Response(null, { status: 401 });
}
return next(request, context);
});
restServer.addMiddleware(async (request, context, next) => {
if (context.collection === 'books' && request.method === 'POST') {
if (
restServer.collections[context.collection].getCount({
filter: {
title: context.requestJson?.title,
},
}) > 0
) {
throw new Response(
JSON.stringify({
errors: {
title: 'An article with this title already exists. The title must be unique.',
},
}),
{
status: 400,
statusText: 'Title is required',
},
);
Comment on lines +36 to +46
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still use a throw Response here instead of returning an object to test that this still works

}
}

return next(request, context);
});
fetchMock.mock('begin:http://localhost:3000', restServer.getHandler());
};

export const dataProvider = defaultDataProvider;
39 changes: 32 additions & 7 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,44 @@ import { App } from './App';
switch (import.meta.env.VITE_MOCK) {
case 'fetch-mock':
import('./fetchMock')
.then(({ initializeFetchMock }) => {
.then(({ initializeFetchMock, dataProvider }) => {
initializeFetchMock();
return dataProvider;
})
.then(() => {
ReactDom.render(<App />, document.getElementById('root'));
.then((dataProvider) => {
ReactDom.render(
<App dataProvider={dataProvider} />,
document.getElementById('root'),
);
});
break;
case 'sinon':
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
import('./sinon')
.then(({ initializeSinon, dataProvider }) => {
initializeSinon();
return dataProvider;
})
.then((dataProvider) => {
ReactDom.render(
<App dataProvider={dataProvider} />,
document.getElementById('root'),
);
});
break;
default:
import('./msw')
.then(({ worker }) => {
return worker.start();
.then(({ worker, dataProvider }) => {
return worker
.start({
quiet: true,
onUnhandledRequest: 'bypass',
})
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
.then(() => dataProvider);
})
.then(() => {
ReactDom.render(<App />, document.getElementById('root'));
.then((dataProvider) => {
ReactDom.render(
<App dataProvider={dataProvider} />,
document.getElementById('root'),
);
});
}
52 changes: 45 additions & 7 deletions example/msw.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
import { setupWorker } from 'msw/browser';
import { getMswHandlers } from '../src/FakeRest';
import { HttpResponse } from 'msw';
import { MswServer, withDelay } from '../src/FakeRest';
import { data } from './data';
import { dataProvider as defaultDataProvider } from './dataProvider';

export const worker = setupWorker(
...getMswHandlers({
baseUrl: 'http://localhost:3000',
data,
}),
);
const restServer = new MswServer({
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
baseUrl: 'http://localhost:3000',
data,
});

restServer.addMiddleware(withDelay(300));
restServer.addMiddleware(async (request, context, next) => {
if (!request.headers?.get('Authorization')) {
throw new Response(null, { status: 401 });
}
return next(request, context);
});

restServer.addMiddleware(async (request, context, next) => {
if (context.collection === 'books' && request.method === 'POST') {
if (
restServer.collections[context.collection].getCount({
filter: {
title: context.requestJson?.title,
},
}) > 0
) {
throw new Response(
JSON.stringify({
errors: {
title: 'An article with this title already exists. The title must be unique.',
},
}),
{
status: 400,
statusText: 'Title is required',
},
);
Comment on lines +31 to +41
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still use a throw Response here instead of returning an object to test that this still works

}
}

return next(request, context);
});

export const worker = setupWorker(...restServer.getHandlers());

export const dataProvider = defaultDataProvider;
Loading
Loading