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

Add localStorage data provider #5329

Merged
merged 1 commit into from
Oct 1, 2020
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
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ build-ra-data-json-server:
@echo "Transpiling ra-data-json-server files...";
@cd ./packages/ra-data-json-server && yarn -s build

build-ra-data-localstorage:
@echo "Transpiling ra-data-localstorage files...";
@cd ./packages/ra-data-localstorage && yarn -s build

build-ra-data-simple-rest:
@echo "Transpiling ra-data-simple-rest files...";
@cd ./packages/ra-data-simple-rest && yarn -s build
Expand All @@ -84,7 +88,7 @@ build-data-generator:
@echo "Transpiling data-generator files...";
@cd ./examples/data-generator && yarn -s build

build: build-ra-core build-ra-ui-materialui build-ra-data-fakerest build-ra-data-json-server 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 ## compile ES6 files to JS
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 ## compile ES6 files to JS

doc: ## compile doc as html and launch doc web server
@yarn -s doc
Expand Down
1 change: 1 addition & 0 deletions docs/DataProviders.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Developers from the react-admin community have open-sourced Data Providers for m
* **[JSON API](https://jsonapi.org/)**: [henvo/ra-jsonapi-client](https://github.com/henvo/ra-jsonapi-client)
* **[JSON HAL](https://tools.ietf.org/html/draft-kelly-json-hal-08)**: [ra-data-json-hal](https://www.npmjs.com/package/ra-data-json-hal)
* **[JSON server](https://github.com/typicode/json-server)**: [marmelab/ra-data-json-server](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-json-server).
* **[LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)**: [marmelab/ra-data-localstorage](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-localstorage)
* **[Loopback3](https://loopback.io/lb3)**: [darthwesker/react-admin-loopback](https://github.com/darthwesker/react-admin-loopback)
* **[Loopback4](https://loopback.io/)**: [elmaistrenko/react-admin-lb4](https://github.com/elmaistrenko/react-admin-lb4)
* **[Moleculer Microservices](https://github.com/RancaguaInnova/moleculer-data-provider)**: [RancaguaInnova/moleculer-data-provider](https://github.com/RancaguaInnova/moleculer-data-provider)
Expand Down
2 changes: 1 addition & 1 deletion examples/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@types/inflection": "^1.5.28",
"@types/recharts": "^1.8.10",
"data-generator-retail": "^3.9.0-beta.1",
"fakerest": "~2.1.0",
"fakerest": "^2.2.0",
"fetch-mock": "~6.3.0",
"json-graphql-server": "~2.1.3",
"proxy-polyfill": "^0.3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-data-fakerest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"watch": "tsc --outDir esm --module es2015 --watch"
},
"dependencies": {
"fakerest": "~2.1.0"
"fakerest": "^2.2.0"
},
"devDependencies": {
"cross-env": "^5.2.0",
Expand Down
12 changes: 9 additions & 3 deletions packages/ra-data-fakerest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,12 @@ export default (data, loggingEnabled = false): DataProvider => {
*/
const handle = (type, resource, params): Promise<any> => {
const collection = restServer.getCollection(resource);
if (!collection) {
return Promise.reject(
new Error(`Undefined collection "${resource}"`)
if (!collection && type !== 'create') {
const error = new UndefinedResourceError(
`Undefined collection "${resource}"`
);
error.code = 1; // make that error detectable
return Promise.reject(error);
}
let response;
try {
Expand Down Expand Up @@ -151,3 +153,7 @@ export default (data, loggingEnabled = false): DataProvider => {
handle('deleteMany', resource, params),
};
};

class UndefinedResourceError extends Error {
code: number;
}
79 changes: 79 additions & 0 deletions packages/ra-data-localstorage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ra-data-localstorage

A dataProvider for [react-admin](https://github.com/marmelab/react-admin) that uses a local database, persisted in localStorage.

The provider issues no HTTP requests, every operation happens locally in the browser. User editions are persisted across refreshes and bwwteen sessions. This allows local-first apps, and can be useful in tests.

## Installation

```sh
npm install --save ra-data-localstorage
```

## Usage

```jsx
// in src/App.js
import * as React from "react";
import { Admin, Resource } from 'react-admin';
import localStorageDataProvider from 'ra-data-localstorage';

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

const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={PostList} />
</Admin>
);

export default App;
```

### defaultData

By default, the data provider starts with no resource. To set default data if the storage is empty, pass a JSON object as the `defaultData` argument:

```js
const dataProvider = localStorageDataProvider({
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 = localStorageDataProvider({
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).
50 changes: 50 additions & 0 deletions packages/ra-data-localstorage/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "ra-data-local-storage",
"version": "3.9.0-beta.3",
"description": "Local storage data provider for react-admin",
"main": "lib/index.js",
"module": "esm/index.js",
"sideEffects": false,
"files": [
"LICENSE",
"*.md",
"lib",
"esm",
"src"
],
"repository": {
"type": "git",
"url": "git+https://github.com/marmelab/react-admin.git"
},
"keywords": [
"reactjs",
"react",
"react-admin",
"rest",
"fakerest",
"local"
],
"author": "François Zaninotto",
"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 ./lib && tsc",
"build-esm": "rimraf ./esm && tsc --outDir esm --module es2015",
"watch": "tsc --outDir esm --module es2015 --watch"
},
"dependencies": {
"ra-data-fakerest": "^3.9.0-beta.1",
"lodash": "~4.17.5"
},
"devDependencies": {
"cross-env": "^5.2.0",
"rimraf": "^2.6.3"
},
"peerDependencies": {
"ra-core": "^3.9.0-beta.1"
}
}
152 changes: 152 additions & 0 deletions packages/ra-data-localstorage/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* eslint-disable eqeqeq */
import fakeRestProvider from 'ra-data-fakerest';
import { DataProvider, Record } from 'ra-core';
import pullAt from 'lodash/pullAt';

/**
* Respond to react-admin data queries using a local database persisted in localStorage
*
* Useful for local-first web apps.
*
* @example // initialize with no data
*
* import localStorageDataProvider from 'ra-data-local-storage';
* const dataProvider = localStorageDataProvider();
*
* @example // initialize with default data (will be ignored if data has been modified by user)
*
* import localStorageDataProvider from 'ra-data-local-storage';
* const dataProvider = localStorageDataProvider({
* 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 ({
defaultData = {},
localStorageKey = 'ra-data-local-storage',
loggingEnabled = false,
localStorageUpdateDelay = 10, // milliseconds
}: {
defaultData: any;
localStorageKey: string;
loggingEnabled: boolean;
localStorageUpdateDelay: number;
}): DataProvider => {
const localStorageData = localStorage.getItem(localStorageKey);
const data = localStorageData ? JSON.parse(localStorageData) : defaultData;

// change data by executing callback, then persist in localStorage
const updateLocalStorage = callback => {
// modify localStorage after the next tick
setTimeout(() => {
callback();
localStorage.setItem(localStorageKey, JSON.stringify(data));
}, localStorageUpdateDelay);
};

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

return {
// read methods are just proxies to FakeRest
getList: <RecordType extends Record = Record>(resource, params) =>
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 Record = Record>(resource, params) =>
baseDataProvider.getOne<RecordType>(resource, params),
getMany: <RecordType extends Record = Record>(resource, params) =>
baseDataProvider.getMany<RecordType>(resource, params),
getManyReference: <RecordType extends Record = Record>(
resource,
params
) =>
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 localStorage
update: <RecordType extends Record = Record>(resource, params) => {
updateLocalStorage(() => {
const index = data[resource].findIndex(
record => record.id == params.id
);
data[resource][index] = {
...data[resource][index],
...params.data,
};
});
return baseDataProvider.update<RecordType>(resource, params);
},
updateMany: (resource, params) => {
updateLocalStorage(() => {
params.ids.forEach(id => {
const index = data[resource].findIndex(
record => record.id == id
);
data[resource][index] = {
...data[resource][index],
...params.data,
};
});
});
return baseDataProvider.updateMany(resource, params);
},
create: <RecordType extends Record = Record>(resource, params) => {
// we need to call the fakerest provider first to get the generated id
return baseDataProvider
.create<RecordType>(resource, params)
.then(response => {
updateLocalStorage(() => {
if (!data.hasOwnProperty(resource)) {
data[resource] = [];
}
data[resource].push(response.data);
});
return response;
});
},
delete: <RecordType extends Record = Record>(resource, params) => {
updateLocalStorage(() => {
const index = data[resource].findIndex(
record => record.id == params.id
);
pullAt(data[resource], [index]);
});
return baseDataProvider.delete<RecordType>(resource, params);
},
deleteMany: (resource, params) => {
updateLocalStorage(() => {
const indexes = params.ids.map(id =>
data[resource].findIndex(record => record.id == id)
);
pullAt(data[resource], indexes);
});
return baseDataProvider.deleteMany(resource, params);
},
};
};
11 changes: 11 additions & 0 deletions packages/ra-data-localstorage/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"declaration": true,
"allowJs": false
},
"exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
"include": ["src"]
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8081,10 +8081,10 @@ faker@^4.1.0:
resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f"
integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=

fakerest@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fakerest/-/fakerest-2.1.0.tgz#f66d0b8fbae7455efa7c666e3f6cdeec70a83860"
integrity sha512-c5x7iayu46Qa/h/W7k3PJiElNc56yxCkWY4mxs38be/D9YNfQSy8fn8xtlIPYGIUxDHAcpmVF9YXrA/SqJp1cA==
fakerest@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/fakerest/-/fakerest-2.2.0.tgz#68fc7663baee9243d998dcdb20c9f555ae7b6065"
integrity sha512-6ByPB2f7NfK7UJ7W4xt2oz6Cq/gefYCV5NJTCkDmZS+hh2scheLl7eLqOCmdx/yw67JinTvG8TgOufjhkM0UOg==
dependencies:
babel-runtime "^6.22.0"

Expand Down