-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
Feat: Add ra-data-localforage package #7959
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# ra-data-localForage | ||
|
||
A data provider for [react-admin](https://github.com/marmelab/react-admin) that uses [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. | ||
|
||
The provider issues no HTTP requests, every operation happen locally in the browser. User editions are persisted across refreshes and between sessions. This allows local-first apps and can be useful in tests. | ||
|
||
## Installation | ||
|
||
```sh | ||
npm install --save ra-data-local-forage | ||
``` | ||
|
||
## Usage | ||
|
||
```jsx | ||
// in src/App.js | ||
import * as React from "react"; | ||
import { Admin, Resource } from 'react-admin'; | ||
import localForageDataProvider from 'ra-data-local-forage'; | ||
|
||
import { PostList } from './posts'; | ||
|
||
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}/> | ||
</Admin> | ||
); | ||
}; | ||
|
||
export default App; | ||
``` | ||
|
||
### defaultData | ||
|
||
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({ | ||
defaultData: { | ||
posts: [ | ||
{ id: 0, title: 'Hello, world!' }, | ||
{ id: 1, title: 'FooBar' }, | ||
], | ||
comments: [ | ||
{ id: 0, post_id: 0, author: 'John Doe', body: 'Sensational!' }, | ||
{ id: 1, post_id: 0, author: 'Jane Doe', body: 'I agree' }, | ||
], | ||
} | ||
}); | ||
``` | ||
|
||
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. | ||
|
||
Foreign keys are also supported: just name the field `{related_resource_name}_id` and give an existing value. | ||
|
||
### loggingEnabled | ||
|
||
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({ | ||
loggingEnabled: true | ||
}); | ||
``` | ||
|
||
## Features | ||
|
||
This data provider uses [FakeRest](https://github.com/marmelab/FakeRest) under the hood. That means that it offers the same features: | ||
|
||
- pagination | ||
- sorting | ||
- filtering by column | ||
- filtering by the `q` full-text search | ||
- filtering numbers and dates greater or less than a value | ||
- embedding related resources | ||
|
||
## License | ||
|
||
This data provider is licensed under the MIT License and sponsored by [marmelab](https://marmelab.com). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{ | ||
"name": "ra-data-local-forage", | ||
"version": "4.2.3", | ||
"description": "LocalForage data provider for react-admin", | ||
"main": "dist/cjs/index.js", | ||
"module": "dist/esm/index.js", | ||
"types": "dist/cjs/index.d.ts", | ||
"sideEffects": false, | ||
"files": [ | ||
"LICENSE", | ||
"*.md", | ||
"dist", | ||
"src" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/marmelab/react-admin.git" | ||
}, | ||
"keywords": [ | ||
"reactjs", | ||
"react", | ||
"react-admin", | ||
"rest", | ||
"fakerest", | ||
"local", | ||
"localForage", | ||
"IndexedDB", | ||
"WebSQL" | ||
], | ||
"author": "Anthony RIMET", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/marmelab/react-admin/issues" | ||
}, | ||
"homepage": "https://github.com/marmelab/react-admin#readme", | ||
"scripts": { | ||
"build": "yarn run build-cjs && yarn run build-esm", | ||
"build-cjs": "rimraf ./dist/cjs && tsc --outDir dist/cjs", | ||
"build-esm": "rimraf ./dist/esm && tsc --outDir dist/esm --module es2015", | ||
"watch": "tsc --outDir dist/esm --module es2015 --watch" | ||
}, | ||
"dependencies": { | ||
"localforage": "^1.7.1", | ||
"lodash": "~4.17.5", | ||
"ra-data-fakerest": "^4.2.3" | ||
}, | ||
"devDependencies": { | ||
"cross-env": "^5.2.0", | ||
"rimraf": "^3.0.2", | ||
"typescript": "^4.4.0" | ||
}, | ||
"peerDependencies": { | ||
"ra-core": "*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
/* eslint-disable import/no-anonymous-default-export */ | ||
import fakeRestProvider from 'ra-data-fakerest'; | ||
|
||
import { | ||
CreateParams, | ||
DataProvider, | ||
GetListParams, | ||
GetOneParams, | ||
GetManyParams, | ||
GetManyReferenceParams, | ||
Identifier, | ||
DeleteParams, | ||
RaRecord, | ||
UpdateParams, | ||
UpdateManyParams, | ||
DeleteManyParams, | ||
} from 'ra-core'; | ||
import pullAt from 'lodash/pullAt'; | ||
import localforage from 'localforage'; | ||
|
||
/** | ||
* Respond to react-admin data queries using a localForage for storage. | ||
* | ||
* Useful for local-first web apps. | ||
* | ||
* @example // initialize with no data | ||
* | ||
* import localForageDataProvider from 'ra-data-local-forage'; | ||
* const dataProvider = await 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({ | ||
* defaultData: { | ||
* posts: [ | ||
* { id: 0, title: 'Hello, world!' }, | ||
* { id: 1, title: 'FooBar' }, | ||
* ], | ||
* comments: [ | ||
* { id: 0, post_id: 0, author: 'John Doe', body: 'Sensational!' }, | ||
* { id: 1, post_id: 0, author: 'Jane Doe', body: 'I agree' }, | ||
* ], | ||
* } | ||
* }); | ||
*/ | ||
export default async ( | ||
params?: LocalForageDataProviderParams | ||
): Promise<DataProvider> => { | ||
const { | ||
defaultData = {}, | ||
prefixLocalForageKey = 'ra-data-local-forage-', | ||
loggingEnabled = false, | ||
} = params || {}; | ||
|
||
const getLocalForageData = async (): Promise<any> => { | ||
const keys = await localforage.keys(); | ||
const keyFiltered = keys.filter(key => { | ||
return key.includes(prefixLocalForageKey); | ||
}); | ||
|
||
if (keyFiltered.length === 0) { | ||
return undefined; | ||
} | ||
const localForageData: Record<string, any> = {}; | ||
|
||
for (const key of keyFiltered) { | ||
const keyWithoutPrefix = key.replace(prefixLocalForageKey, ''); | ||
const res = await localforage.getItem(key); | ||
localForageData[keyWithoutPrefix] = res || []; | ||
} | ||
return localForageData; | ||
}; | ||
|
||
const localForageData = await getLocalForageData(); | ||
const data = localForageData ?? defaultData; | ||
|
||
// Persist in localForage | ||
const updateLocalForage = (resource: string) => { | ||
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>( | ||
resource: string, | ||
params: GetListParams | ||
) => { | ||
return baseDataProvider | ||
.getList<RecordType>(resource, params) | ||
.catch(error => { | ||
if (error.code === 1) { | ||
// undefined collection error: hide the error and return an empty list instead | ||
return { data: [], total: 0 }; | ||
} else { | ||
throw error; | ||
} | ||
}); | ||
}, | ||
getOne: <RecordType extends RaRecord = any>( | ||
resource: string, | ||
params: GetOneParams<any> | ||
) => baseDataProvider.getOne<RecordType>(resource, params), | ||
getMany: <RecordType extends RaRecord = any>( | ||
resource: string, | ||
params: GetManyParams | ||
) => baseDataProvider.getMany<RecordType>(resource, params), | ||
getManyReference: <RecordType extends RaRecord = any>( | ||
resource: string, | ||
params: GetManyReferenceParams | ||
) => | ||
baseDataProvider | ||
.getManyReference<RecordType>(resource, params) | ||
.catch(error => { | ||
if (error.code === 1) { | ||
// undefined collection error: hide the error and return an empty list instead | ||
return { data: [], total: 0 }; | ||
} else { | ||
throw error; | ||
} | ||
}), | ||
|
||
// update methods need to persist changes in localForage | ||
update: <RecordType extends RaRecord = any>( | ||
resource: string, | ||
params: UpdateParams<any> | ||
) => { | ||
const index = data[resource].findIndex( | ||
(record: { id: any }) => record.id === params.id | ||
); | ||
data[resource][index] = { | ||
...data[resource][index], | ||
...params.data, | ||
}; | ||
updateLocalForage(resource); | ||
return baseDataProvider.update<RecordType>(resource, params); | ||
}, | ||
updateMany: (resource: string, params: UpdateManyParams<any>) => { | ||
params.ids.forEach((id: Identifier) => { | ||
const index = data[resource].findIndex( | ||
(record: { id: Identifier }) => record.id === id | ||
); | ||
data[resource][index] = { | ||
Check notice Code scanning Prototype-polluting assignment
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from [library input](1).
|
||
...data[resource][index], | ||
...params.data, | ||
}; | ||
}); | ||
updateLocalForage(resource); | ||
return baseDataProvider.updateMany(resource, params); | ||
}, | ||
create: <RecordType extends RaRecord = any>( | ||
resource: string, | ||
params: CreateParams<any> | ||
) => { | ||
// we need to call the fakerest provider first to get the generated id | ||
return baseDataProvider | ||
.create<RecordType>(resource, params) | ||
.then(response => { | ||
if (!data.hasOwnProperty(resource)) { | ||
data[resource] = []; | ||
} | ||
data[resource].push(response.data); | ||
updateLocalForage(resource); | ||
return response; | ||
}); | ||
}, | ||
delete: <RecordType extends RaRecord = any>( | ||
resource: string, | ||
params: DeleteParams<RecordType> | ||
) => { | ||
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) | ||
); | ||
pullAt(data[resource], indexes); | ||
updateLocalForage(resource); | ||
return baseDataProvider.deleteMany(resource, params); | ||
}, | ||
}; | ||
}; | ||
|
||
export interface LocalForageDataProviderParams { | ||
defaultData?: any; | ||
prefixLocalForageKey?: string; | ||
loggingEnabled?: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "lib", | ||
"rootDir": "src", | ||
"declaration": true, | ||
"declarationMap": true, | ||
"allowJs": false | ||
}, | ||
"exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"], | ||
"include": ["src"] | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check notice
Code scanning
Prototype-polluting assignment