ra-data-local-forage
Version:
LocalForage data provider for react-admin
218 lines • 8.32 kB
JavaScript
import fakeRestProvider from 'ra-data-fakerest';
import pullAt from 'lodash/pullAt.js';
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 = 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 = 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 (params) => {
const { defaultData = {}, prefixLocalForageKey = 'ra-data-local-forage-', loggingEnabled = false, } = params || {};
let data;
let baseDataProvider;
let initializePromise;
const getLocalForageData = async () => {
const keys = await localforage.keys();
const keyFiltered = keys.filter(key => {
return key.includes(prefixLocalForageKey);
});
if (keyFiltered.length === 0) {
return undefined;
}
const localForageData = {};
for (const key of keyFiltered) {
const keyWithoutPrefix = key.replace(prefixLocalForageKey, '');
const res = await localforage.getItem(key);
localForageData[keyWithoutPrefix] = res || [];
}
return localForageData;
};
const initialize = async () => {
if (!initializePromise) {
initializePromise = initializeProvider();
}
return initializePromise;
};
const initializeProvider = async () => {
const localForageData = await getLocalForageData();
data = localForageData ?? defaultData;
baseDataProvider = fakeRestProvider(data, loggingEnabled);
};
// Persist in localForage
const updateLocalForage = (resource) => {
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
localforage.setItem(`${prefixLocalForageKey}${resource}`, data[resource]);
};
return {
// read methods are just proxies to FakeRest
getList: async (resource, params) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
return 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: async (resource, params) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
return baseDataProvider.getOne(resource, params);
},
getMany: async (resource, params) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
return baseDataProvider.getMany(resource, params);
},
getManyReference: async (resource, params) => {
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
return 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 localForage
update: async (resource, params) => {
checkResource(resource);
await initialize();
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
const index = data[resource].findIndex((record) => record.id === params.id);
data[resource][index] = {
...data[resource][index],
...params.data,
};
updateLocalForage(resource);
return baseDataProvider.update(resource, params);
},
updateMany: async (resource, params) => {
checkResource(resource);
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
params.ids.forEach((id) => {
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
const index = data[resource].findIndex((record) => record.id === id);
data[resource][index] = {
...data[resource][index],
...params.data,
};
});
updateLocalForage(resource);
return baseDataProvider.updateMany(resource, params);
},
create: async (resource, params) => {
checkResource(resource);
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
// we need to call the fakerest provider first to get the generated id
return baseDataProvider
.create(resource, params)
.then(response => {
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
if (!data.hasOwnProperty(resource)) {
data[resource] = [];
}
data[resource].push(response.data);
updateLocalForage(resource);
return response;
});
},
delete: async (resource, params) => {
checkResource(resource);
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
const index = data[resource].findIndex((record) => record.id === params.id);
pullAt(data[resource], [index]);
updateLocalForage(resource);
return baseDataProvider.delete(resource, params);
},
deleteMany: async (resource, params) => {
checkResource(resource);
await initialize();
if (!baseDataProvider) {
throw new Error('The dataProvider is not initialized.');
}
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
const indexes = params.ids.map((id) => {
if (!data) {
throw new Error('The dataProvider is not initialized.');
}
return data[resource].findIndex((record) => record.id === id);
});
pullAt(data[resource], indexes);
updateLocalForage(resource);
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