Skip to content

Commit

Permalink
Simplify ra-data-local-forage setup
Browse files Browse the repository at this point in the history
  • Loading branch information
djhi committed Jan 20, 2025
1 parent 661f10c commit 9aba0d1
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 66 deletions.
41 changes: 22 additions & 19 deletions examples/simple/src/dataProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import fakeRestProvider from 'ra-data-fakerest';
import fakeRestProvider from 'ra-data-local-forage';
import { DataProvider, withLifecycleCallbacks, HttpError } from 'react-admin';
import get from 'lodash/get';
import data from './data';
import addUploadFeature from './addUploadFeature';
import { queryClient } from './queryClient';

const dataProvider = withLifecycleCallbacks(fakeRestProvider(data, true, 300), [
{
resource: 'posts',
beforeDelete: async ({ id }, dp) => {
// delete related comments
const { data: comments } = await dp.getList('comments', {
filter: { post_id: id },
pagination: { page: 1, perPage: 100 },
sort: { field: 'id', order: 'DESC' },
});
await dp.deleteMany('comments', {
ids: comments.map(comment => comment.id),
});
// The queryClient would be unaware of the deleted comments without this.
queryClient.invalidateQueries({ queryKey: ['comments'] });
return { id };
const dataProvider = withLifecycleCallbacks(
fakeRestProvider({ defaultData: data, loggingEnabled: true }),
[
{
resource: 'posts',
beforeDelete: async ({ id }, dp) => {
// delete related comments
const { data: comments } = await dp.getList('comments', {
filter: { post_id: id },
pagination: { page: 1, perPage: 100 },
sort: { field: 'id', order: 'DESC' },
});
await dp.deleteMany('comments', {
ids: comments.map(comment => comment.id),
});
// The queryClient would be unaware of the deleted comments without this.
queryClient.invalidateQueries({ queryKey: ['comments'] });
return { id };
},
},
},
]);
]
);

const addTagsSearchSupport = (dataProvider: DataProvider) => ({
...dataProvider,
Expand Down
21 changes: 3 additions & 18 deletions packages/ra-data-localforage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,9 @@ import { Admin, Resource } from 'react-admin';
import localForageDataProvider from 'ra-data-local-forage';

import { PostList } from './posts';
const dataProvider = localForageDataProvider();

const App = () => {
const [dataProvider, setDataProvider] = React.useState<DataProvider | null>(null);

React.useEffect(() => {
async function startDataProvider() {
const localForageProvider = await localForageDataProvider();
setDataProvider(localForageProvider);
}

if (dataProvider === null) {
startDataProvider();
}
}, [dataProvider]);

// hide the admin until the data provider is ready
if (!dataProvider) return <p>Loading...</p>;

return (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={ListGuesser}/>
Expand All @@ -52,7 +37,7 @@ export default App;
By default, the data provider starts with no resource. To set default data if the IndexedDB is empty, pass a JSON object as the `defaultData` argument:

```js
const dataProvider = await localForageDataProvider({
const dataProvider = localForageDataProvider({
defaultData: {
posts: [
{ id: 0, title: 'Hello, world!' },
Expand All @@ -75,7 +60,7 @@ Foreign keys are also supported: just name the field `{related_resource_name}_id
As this data provider doesn't use the network, you can't debug it using the network tab of your browser developer tools. However, it can log all calls (input and output) in the console, provided you set the `loggingEnabled` parameter:

```js
const dataProvider = await localForageDataProvider({
const dataProvider = localForageDataProvider({
loggingEnabled: true
});
```
Expand Down
135 changes: 106 additions & 29 deletions packages/ra-data-localforage/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import localforage from 'localforage';
* @example // initialize with no data
*
* import localForageDataProvider from 'ra-data-local-forage';
* const dataProvider = await localForageDataProvider();
* const dataProvider = localForageDataProvider();
*
* @example // initialize with default data (will be ignored if data has been modified by user)
*
* import localForageDataProvider from 'ra-data-local-forage';
* const dataProvider = await localForageDataProvider({
* const dataProvider = localForageDataProvider({
* defaultData: {
* posts: [
* { id: 0, title: 'Hello, world!' },
Expand All @@ -43,15 +43,17 @@ import localforage from 'localforage';
* }
* });
*/
export default async (
params?: LocalForageDataProviderParams
): Promise<DataProvider> => {
export default (params?: LocalForageDataProviderParams): DataProvider => {
const {
defaultData = {},
prefixLocalForageKey = 'ra-data-local-forage-',
loggingEnabled = false,
} = params || {};

let data: Record<string, any> | undefined;
let baseDataProvider: DataProvider | undefined;
let initializePromise: Promise<void> | undefined;

const getLocalForageData = async (): Promise<any> => {
const keys = await localforage.keys();
const keyFiltered = keys.filter(key => {
Expand All @@ -71,28 +73,44 @@ export default async (
return localForageData;
};

const localForageData = await getLocalForageData();
const data = localForageData ?? defaultData;
const initialize = async () => {
if (!initializePromise) {
initializePromise = initializeProvider();
}
return initializePromise;
};

const initializeProvider = async () => {
const localForageData = await getLocalForageData();
data = localForageData ?? defaultData;

baseDataProvider = fakeRestProvider(
data,
loggingEnabled
) as DataProvider;
};

// Persist in localForage
const updateLocalForage = (resource: string) => {
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
localforage.setItem(
`${prefixLocalForageKey}${resource}`,
data[resource]
);
};

const baseDataProvider = fakeRestProvider(
data,
loggingEnabled
) as DataProvider;

return {
// read methods are just proxies to FakeRest
getList: <RecordType extends RaRecord = any>(
getList: async <RecordType extends RaRecord = any>(
resource: string,
params: GetListParams
) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
return baseDataProvider
.getList<RecordType>(resource, params)
.catch(error => {
Expand All @@ -104,19 +122,35 @@ export default async (
}
});
},
getOne: <RecordType extends RaRecord = any>(
getOne: async <RecordType extends RaRecord = any>(
resource: string,
params: GetOneParams<any>
) => baseDataProvider.getOne<RecordType>(resource, params),
getMany: <RecordType extends RaRecord = any>(
) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
return baseDataProvider.getOne<RecordType>(resource, params);
},
getMany: async <RecordType extends RaRecord = any>(
resource: string,
params: GetManyParams<RecordType>
) => baseDataProvider.getMany<RecordType>(resource, params),
getManyReference: <RecordType extends RaRecord = any>(
) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
return baseDataProvider.getMany<RecordType>(resource, params);
},
getManyReference: async <RecordType extends RaRecord = any>(
resource: string,
params: GetManyReferenceParams
) =>
baseDataProvider
) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
return baseDataProvider
.getManyReference<RecordType>(resource, params)
.catch(error => {
if (error.code === 1) {
Expand All @@ -125,13 +159,22 @@ export default async (
} else {
throw error;
}
}),
});
},

// update methods need to persist changes in localForage
update: <RecordType extends RaRecord = any>(
update: async <RecordType extends RaRecord = any>(
resource: string,
params: UpdateParams<any>
) => {
await initialize();
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}

const index = data[resource].findIndex(
(record: { id: any }) => record.id === params.id
);
Expand All @@ -142,8 +185,16 @@ export default async (
updateLocalForage(resource);
return baseDataProvider.update<RecordType>(resource, params);
},
updateMany: (resource: string, params: UpdateManyParams<any>) => {
updateMany: async (resource: string, params: UpdateManyParams<any>) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}

params.ids.forEach((id: Identifier) => {
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
const index = data[resource].findIndex(
(record: { id: Identifier }) => record.id === id
);
Expand All @@ -155,14 +206,21 @@ export default async (
updateLocalForage(resource);
return baseDataProvider.updateMany(resource, params);
},
create: <RecordType extends Omit<RaRecord, 'id'> = any>(
create: async <RecordType extends Omit<RaRecord, 'id'> = any>(
resource: string,
params: CreateParams<any>
) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
// we need to call the fakerest provider first to get the generated id
return baseDataProvider
.create<RecordType>(resource, params)
.then(response => {
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
if (!data.hasOwnProperty(resource)) {
data[resource] = [];
}
Expand All @@ -171,21 +229,40 @@ export default async (
return response;
});
},
delete: <RecordType extends RaRecord = any>(
delete: async <RecordType extends RaRecord = any>(
resource: string,
params: DeleteParams<RecordType>
) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
const index = data[resource].findIndex(
(record: { id: any }) => record.id === params.id
);
pullAt(data[resource], [index]);
updateLocalForage(resource);
return baseDataProvider.delete<RecordType>(resource, params);
},
deleteMany: (resource: string, params: DeleteManyParams<any>) => {
const indexes = params.ids.map((id: any) =>
data[resource].findIndex((record: any) => record.id === id)
);
deleteMany: async (resource: string, params: DeleteManyParams<any>) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
const indexes = params.ids.map((id: any) => {
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
return data[resource].findIndex(
(record: any) => record.id === id
);
});
pullAt(data[resource], indexes);
updateLocalForage(resource);
return baseDataProvider.deleteMany(resource, params);
Expand Down

0 comments on commit 9aba0d1

Please sign in to comment.