Skip to content

Commit 420f571

Browse files
committed
Feat: Add ra-data-localforage package
1 parent ea8c82d commit 420f571

File tree

7 files changed

+410
-1
lines changed

7 files changed

+410
-1
lines changed

Makefile

+5-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ build-ra-data-json-server:
7676
@echo "Transpiling ra-data-json-server files...";
7777
@cd ./packages/ra-data-json-server && yarn build
7878

79+
build-ra-data-localforage:
80+
@echo "Transpiling ra-data-localforage files...";
81+
@cd ./packages/ra-data-localforage && yarn build
82+
7983
build-ra-data-localstorage:
8084
@echo "Transpiling ra-data-localstorage files...";
8185
@cd ./packages/ra-data-localstorage && yarn build
@@ -108,7 +112,7 @@ build-data-generator:
108112
@echo "Transpiling data-generator files...";
109113
@cd ./examples/data-generator && yarn build
110114

111-
build: build-ra-core build-ra-ui-materialui build-ra-data-fakerest build-ra-data-json-server build-ra-data-localstorage build-ra-data-simple-rest build-ra-data-graphql build-ra-data-graphql-simple build-ra-i18n-polyglot build-ra-input-rich-text build-data-generator build-ra-language-english build-ra-language-french build-react-admin build-ra-no-code ## compile ES6 files to JS
115+
build: build-ra-core build-ra-ui-materialui build-ra-data-fakerest build-ra-data-json-server build-ra-data-localforage build-ra-data-localstorage build-ra-data-simple-rest build-ra-data-graphql build-ra-data-graphql-simple build-ra-i18n-polyglot build-ra-input-rich-text build-data-generator build-ra-language-english build-ra-language-french build-react-admin build-ra-no-code ## compile ES6 files to JS
112116

113117
doc: ## compile doc as html and launch doc web server
114118
@yarn doc

docs/DataProviderList.md

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ If you don't know where to start, use any of the following:
6464
* [marmelab/ra-data-json-server](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-json-server): Similar to the previous one, but requires an API powered by JSONServer.
6565
* [marmelab/ra-data-simple-rest](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-simple-rest): A basic REST adapter that reflects the structure of many APIs
6666
* [marmelab/ra-data-localstorage](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-localstorage): Persists user editions in local storage. This allows local-first apps, and can be useful in tests.
67+
* [marmelab/ra-data-localforage](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-localforage): Persists user editions in IndexedDB. Fallback to WebSQL or localStorage. This allows local-first apps, and can be useful in tests.
6768

6869
**Tip**: Since dataProviders all present the same interface, you can use one dataProvider during early prototyping / development phases, then switch to the dataProvider that fits your production infrastructure.
6970

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# ra-data-localForage
2+
3+
A data provider for [react-admin](https://github.com/marmelab/react-admin) that uses a [localForage](https://localforage.github.io/localForage/). It uses asynchronous storage (IndexedDB or WebSQL) with a simple, localStorage-like API. It fallback to localStorage in browsers with no IndexedDB or WebSQL support.
4+
5+
The provider issues no HTTP requests, every operation happens locally in the browser. User editions are persisted across refreshes and between sessions. This allows local-first apps and can be useful in tests.
6+
7+
## Installation
8+
9+
```sh
10+
npm install --save ra-data-local-forage
11+
```
12+
13+
## Usage
14+
15+
```jsx
16+
// in src/App.js
17+
import * as React from "react";
18+
import { Admin, Resource } from 'react-admin';
19+
import localForageDataProvider from 'ra-data-local-forage';
20+
21+
import { PostList } from './posts';
22+
23+
const App = () => {
24+
const [dataProvider, setDataProvider] = React.useState<DataProvider | null>(null);
25+
26+
React.useEffect(() => {
27+
async function startDataProvider() {
28+
const localForageProvider = await localForageDataProvider();
29+
setDataProvider(localForageProvider);
30+
}
31+
32+
if (dataProvider === null) {
33+
startDataProvider();
34+
}
35+
}, [dataProvider]);
36+
37+
// hide the admin until the data provider is ready
38+
if (!dataProvider) return <p>Loading...</p>;
39+
40+
return (
41+
<Admin dataProvider={dataProvider}>
42+
<Resource name="posts" list={ListGuesser}/>
43+
</Admin>
44+
);
45+
};
46+
47+
export default App;
48+
```
49+
50+
### defaultData
51+
52+
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:
53+
54+
```js
55+
const dataProvider = await localForageDataProvider({
56+
defaultData: {
57+
posts: [
58+
{ id: 0, title: 'Hello, world!' },
59+
{ id: 1, title: 'FooBar' },
60+
],
61+
comments: [
62+
{ id: 0, post_id: 0, author: 'John Doe', body: 'Sensational!' },
63+
{ id: 1, post_id: 0, author: 'Jane Doe', body: 'I agree' },
64+
],
65+
}
66+
});
67+
```
68+
69+
The `defaultData` parameter must be an object literal with one key for each resource type. Values are arrays of resources. Resources must be object literals with at least an `id` key.
70+
71+
Foreign keys are also supported: just name the field `{related_resource_name}_id` and give an existing value.
72+
73+
### loggingEnabled
74+
75+
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:
76+
77+
```js
78+
const dataProvider = await localForageDataProvider({
79+
loggingEnabled: true
80+
});
81+
```
82+
83+
## Features
84+
85+
This data provider uses [FakeRest](https://github.com/marmelab/FakeRest) under the hood. That means that it offers the same features:
86+
87+
- pagination
88+
- sorting
89+
- filtering by column
90+
- filtering by the `q` full-text search
91+
- filtering numbers and dates greater or less than a value
92+
- embedding related resources
93+
94+
## License
95+
96+
This data provider is licensed under the MIT License and sponsored by [marmelab](https://marmelab.com).
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "ra-data-local-forage",
3+
"version": "4.2.2",
4+
"description": "LocalForage data provider for react-admin",
5+
"main": "dist/cjs/index.js",
6+
"module": "dist/esm/index.js",
7+
"types": "dist/cjs/index.d.ts",
8+
"sideEffects": false,
9+
"files": [
10+
"LICENSE",
11+
"*.md",
12+
"dist",
13+
"src"
14+
],
15+
"repository": {
16+
"type": "git",
17+
"url": "git+https://github.com/marmelab/react-admin.git"
18+
},
19+
"keywords": [
20+
"reactjs",
21+
"react",
22+
"react-admin",
23+
"rest",
24+
"fakerest",
25+
"local",
26+
"localForage",
27+
"IndexedDB",
28+
"WebSQL"
29+
],
30+
"author": "Anthony RIMET",
31+
"license": "MIT",
32+
"bugs": {
33+
"url": "https://github.com/marmelab/react-admin/issues"
34+
},
35+
"homepage": "https://github.com/marmelab/react-admin#readme",
36+
"scripts": {
37+
"build": "yarn run build-cjs && yarn run build-esm",
38+
"build-cjs": "rimraf ./dist/cjs && tsc --outDir dist/cjs",
39+
"build-esm": "rimraf ./dist/esm && tsc --outDir dist/esm --module es2015",
40+
"watch": "tsc --outDir dist/esm --module es2015 --watch"
41+
},
42+
"dependencies": {
43+
"localforage": "^1.7.1",
44+
"lodash": "~4.17.5",
45+
"ra-data-fakerest": "^4.2.1"
46+
},
47+
"devDependencies": {
48+
"cross-env": "^5.2.0",
49+
"rimraf": "^3.0.2",
50+
"typescript": "^4.4.0"
51+
},
52+
"peerDependencies": {
53+
"ra-core": "*"
54+
}
55+
}
+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/* eslint-disable import/no-anonymous-default-export */
2+
import fakeRestProvider from 'ra-data-fakerest';
3+
4+
import {
5+
CreateParams,
6+
DataProvider,
7+
GetListParams,
8+
GetOneParams,
9+
GetManyParams,
10+
GetManyReferenceParams,
11+
Identifier,
12+
DeleteParams,
13+
RaRecord,
14+
UpdateParams,
15+
UpdateManyParams,
16+
DeleteManyParams,
17+
} from 'ra-core';
18+
import pullAt from 'lodash/pullAt';
19+
import localforage from 'localforage';
20+
21+
/**
22+
* Respond to react-admin data queries using a localForage for storage.
23+
*
24+
* Useful for local-first web apps.
25+
*
26+
* @example // initialize with no data
27+
*
28+
* import localForageDataProvider from 'ra-data-local-forage';
29+
* const dataProvider = await localForageDataProvider();
30+
*
31+
* @example // initialize with default data (will be ignored if data has been modified by user)
32+
*
33+
* import localForageDataProvider from 'ra-data-local-forage';
34+
* const dataProvider = await localForageDataProvider({
35+
* defaultData: {
36+
* posts: [
37+
* { id: 0, title: 'Hello, world!' },
38+
* { id: 1, title: 'FooBar' },
39+
* ],
40+
* comments: [
41+
* { id: 0, post_id: 0, author: 'John Doe', body: 'Sensational!' },
42+
* { id: 1, post_id: 0, author: 'Jane Doe', body: 'I agree' },
43+
* ],
44+
* }
45+
* });
46+
*/
47+
export default async (
48+
params?: LocalForageDataProviderParams
49+
): Promise<DataProvider> => {
50+
const {
51+
defaultData = {},
52+
prefixLocalForageKey = 'ra-data-local-forage-',
53+
loggingEnabled = false,
54+
} = params || {};
55+
56+
const getLocalForageData = async (): Promise<any> => {
57+
const keys = await localforage.keys();
58+
const keyFiltered = keys.filter(key => {
59+
return key.includes(prefixLocalForageKey);
60+
});
61+
62+
if (keyFiltered.length === 0) {
63+
return undefined;
64+
}
65+
const localForageData: Record<string, any> = {};
66+
67+
for (const key of keyFiltered) {
68+
const keyWithoutPrefix = key.replace(prefixLocalForageKey, '');
69+
const res = await localforage.getItem(key);
70+
localForageData[keyWithoutPrefix] = res || [];
71+
}
72+
return localForageData;
73+
};
74+
75+
const localForageData = await getLocalForageData();
76+
const data = localForageData ? localForageData : defaultData;
77+
78+
// Persist in localForage
79+
const updateLocalForage = (resource: string) => {
80+
localforage.setItem(
81+
`${prefixLocalForageKey}${resource}`,
82+
data[resource]
83+
);
84+
};
85+
86+
const baseDataProvider = fakeRestProvider(
87+
data,
88+
loggingEnabled
89+
) as DataProvider;
90+
91+
return {
92+
// read methods are just proxies to FakeRest
93+
getList: <RecordType extends RaRecord = any>(
94+
resource: string,
95+
params: GetListParams
96+
) => {
97+
return baseDataProvider
98+
.getList<RecordType>(resource, params)
99+
.catch(error => {
100+
if (error.code === 1) {
101+
// undefined collection error: hide the error and return an empty list instead
102+
return { data: [], total: 0 };
103+
} else {
104+
throw error;
105+
}
106+
});
107+
},
108+
getOne: <RecordType extends RaRecord = any>(
109+
resource: string,
110+
params: GetOneParams<any>
111+
) => baseDataProvider.getOne<RecordType>(resource, params),
112+
getMany: <RecordType extends RaRecord = any>(
113+
resource: string,
114+
params: GetManyParams
115+
) => baseDataProvider.getMany<RecordType>(resource, params),
116+
getManyReference: <RecordType extends RaRecord = any>(
117+
resource: string,
118+
params: GetManyReferenceParams
119+
) =>
120+
baseDataProvider
121+
.getManyReference<RecordType>(resource, params)
122+
.catch(error => {
123+
if (error.code === 1) {
124+
// undefined collection error: hide the error and return an empty list instead
125+
return { data: [], total: 0 };
126+
} else {
127+
throw error;
128+
}
129+
}),
130+
131+
// update methods need to persist changes in localForage
132+
update: <RecordType extends RaRecord = any>(
133+
resource: string,
134+
params: UpdateParams<any>
135+
) => {
136+
const index = data[resource].findIndex(
137+
(record: { id: any }) => record.id === params.id
138+
);
139+
data[resource][index] = {
140+
...data[resource][index],
141+
...params.data,
142+
};
143+
updateLocalForage(resource);
144+
return baseDataProvider.update<RecordType>(resource, params);
145+
},
146+
updateMany: (resource: string, params: UpdateManyParams<any>) => {
147+
params.ids.forEach((id: Identifier) => {
148+
const index = data[resource].findIndex(
149+
(record: { id: Identifier }) => record.id === id
150+
);
151+
data[resource][index] = {
152+
...data[resource][index],
153+
...params.data,
154+
};
155+
});
156+
updateLocalForage(resource);
157+
return baseDataProvider.updateMany(resource, params);
158+
},
159+
create: <RecordType extends RaRecord = any>(
160+
resource: string,
161+
params: CreateParams<any>
162+
) => {
163+
// we need to call the fakerest provider first to get the generated id
164+
return baseDataProvider
165+
.create<RecordType>(resource, params)
166+
.then(response => {
167+
if (!data.hasOwnProperty(resource)) {
168+
data[resource] = [];
169+
}
170+
data[resource].push(response.data);
171+
updateLocalForage(resource);
172+
return response;
173+
});
174+
},
175+
delete: <RecordType extends RaRecord = any>(
176+
resource: string,
177+
params: DeleteParams<RecordType>
178+
) => {
179+
const index = data[resource].findIndex(
180+
(record: { id: any }) => record.id === params.id
181+
);
182+
pullAt(data[resource], [index]);
183+
updateLocalForage(resource);
184+
return baseDataProvider.delete<RecordType>(resource, params);
185+
},
186+
deleteMany: (resource: string, params: DeleteManyParams<any>) => {
187+
const indexes = params.ids.map((id: any) =>
188+
data[resource].findIndex((record: any) => record.id === id)
189+
);
190+
pullAt(data[resource], indexes);
191+
updateLocalForage(resource);
192+
return baseDataProvider.deleteMany(resource, params);
193+
},
194+
};
195+
};
196+
197+
export interface LocalForageDataProviderParams {
198+
defaultData?: any;
199+
prefixLocalForageKey?: string;
200+
loggingEnabled?: boolean;
201+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "lib",
5+
"rootDir": "src",
6+
"declaration": true,
7+
"declarationMap": true,
8+
"allowJs": false
9+
},
10+
"exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
11+
"include": ["src"]
12+
}

0 commit comments

Comments
 (0)