-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: Add ra-data-localforage package
- Loading branch information
Showing
7 changed files
with
410 additions
and
1 deletion.
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 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. | ||
|
||
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. | ||
|
||
## 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.2", | ||
"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.1" | ||
}, | ||
"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 ? 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] = { | ||
...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.