diff --git a/examples/crm/src/dataProvider.ts b/examples/crm/src/dataProvider.ts index 95f1bd54548..68334568caa 100644 --- a/examples/crm/src/dataProvider.ts +++ b/examples/crm/src/dataProvider.ts @@ -3,14 +3,14 @@ import { withLifecycleCallbacks } from 'react-admin'; import generateData from './dataGenerator'; -const baseDataProvider = fakeRestDataProvider(generateData(), true); +const baseDataProvider = fakeRestDataProvider(generateData(), true, 300); const TASK_MARKED_AS_DONE = 'TASK_MARKED_AS_DONE'; const TASK_MARKED_AS_UNDONE = 'TASK_MARKED_AS_UNDONE'; const TASK_DONE_NOT_CHANGED = 'TASK_DONE_NOT_CHANGED'; let taskUpdateType = TASK_DONE_NOT_CHANGED; -const augmentedDataProvider = withLifecycleCallbacks(baseDataProvider, [ +export const dataProvider = withLifecycleCallbacks(baseDataProvider, [ { resource: 'contactNotes', afterCreate: async (result, dataProvider) => { @@ -109,13 +109,3 @@ const augmentedDataProvider = withLifecycleCallbacks(baseDataProvider, [ }, }, ]); - -export const dataProvider = new Proxy(augmentedDataProvider, { - get: (target, name: string) => (resource: string, params: any) => - new Promise(resolve => - setTimeout( - () => resolve(augmentedDataProvider[name](resource, params)), - 300 - ) - ), -}); diff --git a/examples/data-generator/src/types.ts b/examples/data-generator/src/types.ts index 89d52d68c8f..07ecf3063a3 100644 --- a/examples/data-generator/src/types.ts +++ b/examples/data-generator/src/types.ts @@ -6,7 +6,7 @@ import type { Invoice } from './invoices'; import type { Review } from './reviews'; import { Settings } from './finalize'; -export interface Db { +export interface Db extends Record { customers: Customer[]; categories: Category[]; products: Product[]; diff --git a/examples/demo/package.json b/examples/demo/package.json index dc178c8a8da..889b246a53f 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -12,14 +12,13 @@ "clsx": "^2.1.1", "data-generator-retail": "^5.0.0-alpha.0", "date-fns": "^3.6.0", - "fakerest": "^3.0.0", + "fakerest": "^4.0.0", "fetch-mock": "~9.11.0", "graphql": "^15.6.0", "graphql-tag": "^2.12.6", "inflection": "^3.0.0", "json-graphql-server": "~3.0.1", "query-string": "^7.1.3", - "ra-data-fakerest": "^5.0.0-alpha.0", "ra-data-graphql": "^5.0.0-alpha.0", "ra-data-graphql-simple": "^5.0.0-alpha.0", "ra-data-simple-rest": "^5.0.0-alpha.0", diff --git a/examples/demo/src/dataProvider/rest.ts b/examples/demo/src/dataProvider/rest.ts index 31c83485af3..90f5ed7879c 100644 --- a/examples/demo/src/dataProvider/rest.ts +++ b/examples/demo/src/dataProvider/rest.ts @@ -1,22 +1,3 @@ import simpleRestProvider from 'ra-data-simple-rest'; -const restProvider = simpleRestProvider('http://localhost:4000'); - -const delayedDataProvider = new Proxy(restProvider, { - get: (target, name, self) => { - // as we await for the dataProvider, JS calls then on it. We must trap that call or else the dataProvider will be called with the then method - if (name === 'then') { - return self; - } - return (resource: string, params: any) => - new Promise(resolve => - setTimeout( - () => - resolve(restProvider[name as string](resource, params)), - 500 - ) - ); - }, -}); - -export default delayedDataProvider; +export default simpleRestProvider('http://localhost:4000'); diff --git a/examples/demo/src/fakeServer/rest.ts b/examples/demo/src/fakeServer/rest.ts index bb05500ec86..b81a35e6e69 100644 --- a/examples/demo/src/fakeServer/rest.ts +++ b/examples/demo/src/fakeServer/rest.ts @@ -1,15 +1,18 @@ -import FakeRest from 'fakerest'; +import { FetchMockAdapter, withDelay } from 'fakerest'; import fetchMock from 'fetch-mock'; import generateData from 'data-generator-retail'; export default () => { const data = generateData(); - const restServer = new FakeRest.FetchServer('http://localhost:4000'); + const adapter = new FetchMockAdapter({ + baseUrl: 'http://localhost:4000', + data, + loggingEnabled: true, + middlewares: [withDelay(500)], + }); if (window) { - window.restServer = restServer; // give way to update data in the console + window.restServer = adapter.server; // give way to update data in the console } - restServer.init(data); - restServer.toggleLogging(); // logging is off by default, enable it - fetchMock.mock('begin:http://localhost:4000', restServer.getHandler()); + fetchMock.mock('begin:http://localhost:4000', adapter.getHandler()); return () => fetchMock.restore(); }; diff --git a/examples/demo/src/fakerest.d.ts b/examples/demo/src/fakerest.d.ts deleted file mode 100644 index b88450de3f9..00000000000 --- a/examples/demo/src/fakerest.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'fakerest'; diff --git a/examples/simple/src/dataProvider.tsx b/examples/simple/src/dataProvider.tsx index 5534862d4a0..ab11a5870d2 100644 --- a/examples/simple/src/dataProvider.tsx +++ b/examples/simple/src/dataProvider.tsx @@ -5,7 +5,7 @@ import data from './data'; import addUploadFeature from './addUploadFeature'; import { queryClient } from './queryClient'; -const dataProvider = withLifecycleCallbacks(fakeRestProvider(data, true), [ +const dataProvider = withLifecycleCallbacks(fakeRestProvider(data, true, 300), [ { resource: 'posts', beforeDelete: async ({ id }, dp) => { @@ -111,23 +111,8 @@ const sometimesFailsDataProvider = new Proxy(uploadCapableDataProvider, { }, }); -const delayedDataProvider = new Proxy(sometimesFailsDataProvider, { - get: (target, name) => (resource, params) => { - if (typeof name === 'symbol' || name === 'then') { - return; - } - return new Promise(resolve => - setTimeout( - () => - resolve(sometimesFailsDataProvider[name](resource, params)), - 300 - ) - ); - }, -}); - interface ResponseError extends Error { status?: number; } -export default delayedDataProvider; +export default sometimesFailsDataProvider; diff --git a/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx b/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx index 285c52a322a..94010a21251 100644 --- a/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx +++ b/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx @@ -40,21 +40,7 @@ const data = { ], }; -const baseDataProvider = fakeRestProvider(data); - -const dataProvider = new Proxy(baseDataProvider, { - get: (target, name) => (resource, params) => { - if (typeof name === 'symbol' || name === 'then') { - return; - } - return new Promise(resolve => - setTimeout( - () => resolve(baseDataProvider[name](resource, params)), - 300 - ) - ); - }, -}); +const dataProvider = fakeRestProvider(data, undefined, 300); const BookListView = () => { const { data, isPending, sort, setSort, filterValues, setFilters } = diff --git a/packages/ra-core/src/controller/list/ListBase.stories.tsx b/packages/ra-core/src/controller/list/ListBase.stories.tsx index 7b98dc8e417..6c943401f94 100644 --- a/packages/ra-core/src/controller/list/ListBase.stories.tsx +++ b/packages/ra-core/src/controller/list/ListBase.stories.tsx @@ -39,21 +39,7 @@ const data = { ], }; -const baseDataProvider = fakeRestProvider(data, true); - -const dataProvider = new Proxy(baseDataProvider, { - get: (target, name) => (resource, params) => { - if (typeof name === 'symbol' || name === 'then') { - return; - } - return new Promise(resolve => - setTimeout( - () => resolve(baseDataProvider[name](resource, params)), - 300 - ) - ); - }, -}); +const dataProvider = fakeRestProvider(data, true, 300); const BookListView = () => { const { diff --git a/packages/ra-core/src/dataProvider/useDelete.spec.tsx b/packages/ra-core/src/dataProvider/useDelete.spec.tsx index 96e35611389..0a6561589a2 100644 --- a/packages/ra-core/src/dataProvider/useDelete.spec.tsx +++ b/packages/ra-core/src/dataProvider/useDelete.spec.tsx @@ -289,22 +289,30 @@ describe('useDelete', () => { jest.spyOn(console, 'error').mockImplementation(() => {}); render(); screen.getByText('Delete first post').click(); - await waitFor(() => { - expect(screen.queryByText('success')).toBeNull(); - expect(screen.queryByText('something went wrong')).toBeNull(); - expect(screen.queryByText('Hello')).not.toBeNull(); - expect(screen.queryByText('World')).not.toBeNull(); - expect(screen.queryByText('mutating')).not.toBeNull(); - }); - await waitFor(() => { - expect(screen.queryByText('success')).toBeNull(); - expect( - screen.queryByText('something went wrong') - ).not.toBeNull(); - expect(screen.queryByText('Hello')).not.toBeNull(); - expect(screen.queryByText('World')).not.toBeNull(); - expect(screen.queryByText('mutating')).toBeNull(); - }); + await waitFor( + () => { + expect(screen.queryByText('success')).toBeNull(); + expect( + screen.queryByText('something went wrong') + ).toBeNull(); + expect(screen.queryByText('Hello')).not.toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }, + { timeout: 4000 } + ); + await waitFor( + () => { + expect(screen.queryByText('success')).toBeNull(); + expect( + screen.queryByText('something went wrong') + ).not.toBeNull(); + expect(screen.queryByText('Hello')).not.toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).toBeNull(); + }, + { timeout: 4000 } + ); }); it('when optimistic, displays result and success side effects right away', async () => { jest.spyOn(console, 'log').mockImplementation(() => {}); diff --git a/packages/ra-data-fakerest/README.md b/packages/ra-data-fakerest/README.md index 5d849201a85..1bb30348cb4 100644 --- a/packages/ra-data-fakerest/README.md +++ b/packages/ra-data-fakerest/README.md @@ -90,6 +90,34 @@ const App = () => ( ); ``` +## Delay + +You can pass a delay in milliseconds as the third argument to the constructor. This will simulate a network delay for each request. + +```jsx +// in src/App.js +import * as React from "react"; +import { Admin, Resource } from 'react-admin'; +import fakeDataProvider from 'ra-data-fakerest'; + +const dataProvider = fakeDataProvider({ /* data here */ }, false, 1000); + +const App = () => ( + + // ... + +); +``` + +## Inspecting the Data + +`ra-data-fakerest` makes its internal database accessible in the global scope under the `_database` key. You can use it to inspect the data in your browser console. + +```js +_database.getOne('posts', 1); +// { id: 1, title: 'FooBar' } +``` + ## Features This data provider uses [FakeRest](https://github.com/marmelab/FakeRest) under the hood. That means that it offers the same features: diff --git a/packages/ra-data-fakerest/package.json b/packages/ra-data-fakerest/package.json index c6a2eb6ac74..850f1e567ae 100644 --- a/packages/ra-data-fakerest/package.json +++ b/packages/ra-data-fakerest/package.json @@ -37,7 +37,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "fakerest": "^3.0.0" + "fakerest": "^4.0.0" }, "devDependencies": { "@types/jest": "^29.5.2", diff --git a/packages/ra-data-fakerest/src/index.ts b/packages/ra-data-fakerest/src/index.ts index 3115d3cee1e..93f4ea19882 100644 --- a/packages/ra-data-fakerest/src/index.ts +++ b/packages/ra-data-fakerest/src/index.ts @@ -1,4 +1,4 @@ -import FakeRest from 'fakerest'; +import { Database } from 'fakerest'; import { DataProvider } from 'ra-core'; /* eslint-disable no-console */ @@ -14,6 +14,16 @@ function log(type, resource, params, response) { } } +function delayed(response: any, delay?: number) { + // If there is no delay, we return the value right away/ + // This saves a tick in unit tests. + return delay + ? new Promise(resolve => { + setTimeout(() => resolve(response), delay); + }) + : response; +} + /** * Respond to react-admin data queries using a local JavaScript object * @@ -33,12 +43,11 @@ function log(type, resource, params, response) { * ], * }) */ -export default (data, loggingEnabled = false): DataProvider => { - const restServer = new FakeRest.Server(); - restServer.init(data); +export default (data, loggingEnabled = false, delay?: number): DataProvider => { + const database = new Database({ data }); if (typeof window !== 'undefined') { // give way to update data in the console - (window as any).restServer = restServer; + (window as any)._database = database; } function getResponse(type, resource, params) { @@ -47,65 +56,92 @@ export default (data, loggingEnabled = false): DataProvider => { const { page, perPage } = params.pagination; const { field, order } = params.sort; const query = { - sort: [field, order], - range: [(page - 1) * perPage, page * perPage - 1], + sort: [field, order] as [string, 'asc' | 'desc'], + range: [(page - 1) * perPage, page * perPage - 1] as [ + number, + number, + ], filter: params.filter, }; - return { - data: restServer.getAll(resource, query), - total: restServer.getCount(resource, { - filter: params.filter, - }), - }; + return delayed( + { + data: database.getAll(resource, query), + total: database.getCount(resource, { + filter: params.filter, + }), + }, + delay + ); } case 'getOne': - return { - data: restServer.getOne(resource, params.id, { ...params }), - }; + return delayed( + { + data: database.getOne(resource, params.id, { + ...params, + }), + }, + delay + ); case 'getMany': - return { - data: params.ids.map( - id => restServer.getOne(resource, id), - { ...params } - ), - }; + return delayed( + { + data: params.ids.map( + id => database.getOne(resource, id), + { ...params } + ), + }, + delay + ); case 'getManyReference': { const { page, perPage } = params.pagination; const { field, order } = params.sort; const query = { - sort: [field, order], - range: [(page - 1) * perPage, page * perPage - 1], + sort: [field, order] as [string, 'asc' | 'desc'], + range: [(page - 1) * perPage, page * perPage - 1] as [ + number, + number, + ], filter: { ...params.filter, [params.target]: params.id }, }; - return { - data: restServer.getAll(resource, query), - total: restServer.getCount(resource, { - filter: query.filter, - }), - }; + return delayed( + { + data: database.getAll(resource, query), + total: database.getCount(resource, { + filter: query.filter, + }), + }, + delay + ); } case 'update': - return { - data: restServer.updateOne(resource, params.id, { - ...params.data, - }), - }; + return delayed( + { + data: database.updateOne(resource, params.id, { + ...params.data, + }), + }, + delay + ); case 'updateMany': params.ids.forEach(id => - restServer.updateOne(resource, id, { + database.updateOne(resource, id, { ...params.data, }) ); - return { data: params.ids }; + return delayed({ data: params.ids }, delay); case 'create': - return { - data: restServer.addOne(resource, { ...params.data }), - }; + return delayed( + { data: database.addOne(resource, { ...params.data }) }, + delay + ); case 'delete': - return { data: restServer.removeOne(resource, params.id) }; + return delayed( + { data: database.removeOne(resource, params.id) }, + delay + ); case 'deleteMany': - params.ids.forEach(id => restServer.removeOne(resource, id)); - return { data: params.ids }; + params.ids.forEach(id => database.removeOne(resource, id)); + return delayed({ data: params.ids }, delay); default: return false; } @@ -118,7 +154,7 @@ export default (data, loggingEnabled = false): DataProvider => { * @returns {Promise} The response */ const handle = (type, resource, params): Promise => { - const collection = restServer.getCollection(resource); + const collection = database.getCollection(resource); if (!collection && type !== 'create') { const error = new UndefinedResourceError( `Undefined collection "${resource}"` diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx index c05f7d96a55..721398071a9 100644 --- a/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx +++ b/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx @@ -82,25 +82,12 @@ const data = { ], }; -const baseDataProvider = fakeRestProvider( +const dataProvider = fakeRestProvider( data, - process.env.NODE_ENV === 'development' + process.env.NODE_ENV === 'development', + 500 ); -const dataProvider = new Proxy(baseDataProvider, { - get: (target, name) => (resource, params) => { - if (typeof name === 'symbol' || name === 'then') { - return; - } - return new Promise(resolve => - setTimeout( - () => resolve(baseDataProvider[name](resource, params)), - 500 - ) - ); - }, -}); - const Admin = ({ children, dataProvider, layout }: any) => ( ( ); -const delayedDataProvider = new Proxy(dataProvider, { - get: (target, name) => (resource, params) => { - if (typeof name === 'symbol' || name === 'then') { - return; - } - return new Promise(resolve => - setTimeout(() => resolve(dataProvider[name](resource, params)), 300) - ); - }, -}); +const delayedDataProvider = fakeRestProvider( + data, + process.env.NODE_ENV !== 'test', + 300 +); + export const ManyResources = () => (