ra-data-local-storage
Version:
Local storage data provider for react-admin
141 lines • 5.64 kB
JavaScript
/* eslint-disable eqeqeq */
import fakeRestProvider from 'ra-data-fakerest';
import pullAt from 'lodash/pullAt.js';
/**
* Respond to react-admin data queries using a local database persisted in localStorage
*
* Useful for local-first web apps. The storage is shared between tabs.
*
* @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 (params) => {
const { defaultData = {}, localStorageKey = 'ra-data-local-storage', loggingEnabled = false, localStorageUpdateDelay = 10, // milliseconds
} = params || {};
const localStorageData = localStorage.getItem(localStorageKey);
let 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);
};
let baseDataProvider = fakeRestProvider(data, loggingEnabled);
window?.addEventListener('storage', event => {
if (event.key === localStorageKey) {
const newData = event.newValue ? JSON.parse(event.newValue) : {};
data = newData;
baseDataProvider = fakeRestProvider(newData, loggingEnabled);
}
});
return {
// read methods are just proxies to FakeRest
getList: (resource, params) => baseDataProvider
.getList(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: (resource, params) => baseDataProvider.getOne(resource, params),
getMany: (resource, params) => baseDataProvider.getMany(resource, params),
getManyReference: (resource, params) => baseDataProvider
.getManyReference(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: (resource, params) => {
checkResource(resource);
updateLocalStorage(() => {
const index = data[resource]?.findIndex(record => record.id == params.id);
data[resource][index] = {
...data[resource][index],
...params.data,
};
});
return baseDataProvider.update(resource, params);
},
updateMany: (resource, params) => {
checkResource(resource);
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: (resource, params) => {
checkResource(resource);
// we need to call the fakerest provider first to get the generated id
return baseDataProvider
.create(resource, params)
.then(response => {
updateLocalStorage(() => {
if (!data.hasOwnProperty(resource)) {
data[resource] = [];
}
data[resource].push(response.data);
});
return response;
});
},
delete: (resource, params) => {
checkResource(resource);
updateLocalStorage(() => {
const index = data[resource]?.findIndex(record => record.id == params.id);
pullAt(data[resource], [index]);
});
return baseDataProvider.delete(resource, params);
},
deleteMany: (resource, params) => {
checkResource(resource);
updateLocalStorage(() => {
const indexes = params.ids.map(id => data[resource]?.findIndex(record => record.id == id));
pullAt(data[resource], indexes);
});
return baseDataProvider.deleteMany(resource, params);
},
};
};
const checkResource = resource => {
if (['__proto__', 'constructor', 'prototype'].includes(resource)) {
// protection against prototype pollution
throw new Error(`Invalid resource key: ${resource}`);
}
};
//# sourceMappingURL=index.js.map