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

Simplify ra-data-localforage setup #10455

Merged
merged 2 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
33 changes: 33 additions & 0 deletions packages/ra-data-localforage/src/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Resource } from 'ra-core';
import {
AdminContext,
AdminUI,
EditGuesser,
ListGuesser,
} from 'ra-ui-materialui';
import localforageDataProvider from './index';

export default {
title: 'ra-data-local-forage',
};

export const Basic = () => {
const dataProvider = localforageDataProvider({
prefixLocalForageKey: 'story-app-',
defaultData: {
posts: [
{ id: 1, title: 'Hello, world!' },
{ id: 2, title: 'FooBar' },
],
},
});

return (
<AdminContext dataProvider={dataProvider}>
<AdminUI>
<Resource name="posts" list={ListGuesser} edit={EditGuesser} />
</AdminUI>
</AdminContext>
);
};
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
Loading