Skip to content

Commit 249ebba

Browse files
committed
Guard local data provider mutations against missing ids
1 parent 5a98a66 commit 249ebba

File tree

4 files changed

+341
-43
lines changed

4 files changed

+341
-43
lines changed

packages/ra-data-local-forage/src/index.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ jest.mock('localforage', () => ({
1313
}));
1414

1515
describe('ra-data-local-forage', () => {
16+
const posts = [
17+
{ id: 1, title: 'Hello world' },
18+
{ id: 2, title: 'Second post' },
19+
];
20+
1621
beforeEach(() => {
1722
jest.resetAllMocks();
1823
(localforage.keys as jest.Mock).mockResolvedValue([]);
@@ -45,4 +50,72 @@ describe('ra-data-local-forage', () => {
4550
} as any)
4651
).rejects.toThrow('Invalid resource key: __proto__');
4752
});
53+
54+
it('does not corrupt local data when update targets an unknown id', async () => {
55+
(localforage.keys as jest.Mock).mockResolvedValue([
56+
'ra-data-local-forage-posts',
57+
]);
58+
(localforage.getItem as jest.Mock).mockResolvedValue([...posts]);
59+
const dataProvider = localForageDataProvider();
60+
61+
await expect(
62+
dataProvider.update('posts', {
63+
id: 3,
64+
data: { title: 'Updated' },
65+
previousData: { id: 3 },
66+
} as any)
67+
).rejects.toThrow('No item with identifier 3');
68+
69+
expect(localforage.setItem).not.toHaveBeenCalled();
70+
});
71+
72+
it('does not partially update local data when updateMany includes an unknown id', async () => {
73+
(localforage.keys as jest.Mock).mockResolvedValue([
74+
'ra-data-local-forage-posts',
75+
]);
76+
(localforage.getItem as jest.Mock).mockResolvedValue([...posts]);
77+
const dataProvider = localForageDataProvider();
78+
79+
await expect(
80+
dataProvider.updateMany('posts', {
81+
ids: [1, 3],
82+
data: { title: 'Updated' },
83+
} as any)
84+
).rejects.toThrow('No item with identifier 3');
85+
86+
expect(localforage.setItem).not.toHaveBeenCalled();
87+
});
88+
89+
it('does not corrupt local data when delete targets an unknown id', async () => {
90+
(localforage.keys as jest.Mock).mockResolvedValue([
91+
'ra-data-local-forage-posts',
92+
]);
93+
(localforage.getItem as jest.Mock).mockResolvedValue([...posts]);
94+
const dataProvider = localForageDataProvider();
95+
96+
await expect(
97+
dataProvider.delete('posts', {
98+
id: 3,
99+
previousData: { id: 3 },
100+
} as any)
101+
).rejects.toThrow('No item with identifier 3');
102+
103+
expect(localforage.setItem).not.toHaveBeenCalled();
104+
});
105+
106+
it('does not partially delete local data when deleteMany includes an unknown id', async () => {
107+
(localforage.keys as jest.Mock).mockResolvedValue([
108+
'ra-data-local-forage-posts',
109+
]);
110+
(localforage.getItem as jest.Mock).mockResolvedValue([...posts]);
111+
const dataProvider = localForageDataProvider();
112+
113+
await expect(
114+
dataProvider.deleteMany('posts', {
115+
ids: [1, 3],
116+
} as any)
117+
).rejects.toThrow('No item with identifier 3');
118+
119+
expect(localforage.setItem).not.toHaveBeenCalled();
120+
});
48121
});

packages/ra-data-local-forage/src/index.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,26 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
175175
throw new Error('The dataProvider is not initialized.');
176176
}
177177

178+
assertRecordsExist(getResourceCollection(data, resource), [params.id]);
179+
const response = await baseDataProvider.update<RecordType>(
180+
resource,
181+
params
182+
);
178183
const resourceData = getResourceCollection(data, resource);
179184
const index = resourceData.findIndex(
180185
(record: { id: any }) => record.id === params.id
181186
);
187+
188+
if (index === -1) {
189+
return response;
190+
}
191+
182192
resourceData.splice(index, 1, {
183193
...resourceData[index],
184194
...params.data,
185195
});
186196
updateLocalForage(resource);
187-
return baseDataProvider.update<RecordType>(resource, params);
197+
return response;
188198
},
189199
updateMany: async (resource: string, params: UpdateManyParams<any>) => {
190200
checkResource(resource);
@@ -197,18 +207,25 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
197207
}
198208

199209
const resourceData = getResourceCollection(data, resource);
210+
assertRecordsExist(resourceData, params.ids);
211+
const response = await baseDataProvider.updateMany(resource, params);
200212

201213
params.ids.forEach((id: Identifier) => {
202214
const index = resourceData.findIndex(
203215
(record: { id: Identifier }) => record.id === id
204216
);
217+
218+
if (index === -1) {
219+
return;
220+
}
221+
205222
resourceData.splice(index, 1, {
206223
...resourceData[index],
207224
...params.data,
208225
});
209226
});
210227
updateLocalForage(resource);
211-
return baseDataProvider.updateMany(resource, params);
228+
return response;
212229
},
213230
create: async <RecordType extends Omit<RaRecord, 'id'> = any>(
214231
resource: string,
@@ -247,13 +264,23 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
247264
if (!data) {
248265
throw new Error('The dataProvider is not initialized.');
249266
}
267+
assertRecordsExist(getResourceCollection(data, resource), [params.id]);
268+
const response = await baseDataProvider.delete<RecordType>(
269+
resource,
270+
params
271+
);
250272
const resourceData = getResourceCollection(data, resource);
251273
const index = resourceData.findIndex(
252274
(record: { id: any }) => record.id === params.id
253275
);
276+
277+
if (index === -1) {
278+
return response;
279+
}
280+
254281
pullAt(resourceData, [index]);
255282
updateLocalForage(resource);
256-
return baseDataProvider.delete<RecordType>(resource, params);
283+
return response;
257284
},
258285
deleteMany: async (resource: string, params: DeleteManyParams<any>) => {
259286
checkResource(resource);
@@ -265,14 +292,19 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
265292
throw new Error('The dataProvider is not initialized.');
266293
}
267294
const resourceData = getResourceCollection(data, resource);
268-
const indexes = params.ids.map((id: any) => {
269-
return resourceData.findIndex(
270-
(record: any) => record.id === id
271-
);
272-
});
295+
assertRecordsExist(resourceData, params.ids);
296+
const response = await baseDataProvider.deleteMany(resource, params);
297+
const indexes = params.ids
298+
.map((id: any) => {
299+
return resourceData.findIndex(
300+
(record: any) => record.id === id
301+
);
302+
})
303+
.filter(index => index !== -1);
304+
273305
pullAt(resourceData, indexes);
274306
updateLocalForage(resource);
275-
return baseDataProvider.deleteMany(resource, params);
307+
return response;
276308
},
277309
};
278310
};
@@ -313,6 +345,18 @@ const checkResource = resource => {
313345
}
314346
};
315347

348+
const assertRecordsExist = (resourceData, ids) => {
349+
ids.forEach(id => {
350+
if (
351+
resourceData.findIndex(
352+
(record: { id: Identifier }) => record.id === id
353+
) === -1
354+
) {
355+
throw new Error(`No item with identifier ${id}`);
356+
}
357+
});
358+
};
359+
316360
export interface LocalForageDataProviderParams {
317361
defaultData?: any;
318362
prefixLocalForageKey?: string;

packages/ra-data-local-storage/src/index.spec.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import expect from 'expect';
33
import localStorageDataProvider from './index';
44

55
describe('ra-data-local-storage', () => {
6+
const posts = [
7+
{ id: 1, title: 'Hello world' },
8+
{ id: 2, title: 'Second post' },
9+
];
10+
611
beforeEach(() => {
712
localStorage.clear();
813
});
@@ -40,4 +45,104 @@ describe('ra-data-local-storage', () => {
4045
} as any)
4146
).toThrow('Invalid resource key: __proto__');
4247
});
48+
49+
it('does not corrupt local data when update targets an unknown id', async () => {
50+
localStorage.setItem(
51+
'ra-data-local-storage-test',
52+
JSON.stringify({ posts })
53+
);
54+
const dataProvider = localStorageDataProvider({
55+
localStorageKey: 'ra-data-local-storage-test',
56+
localStorageUpdateDelay: 0,
57+
});
58+
59+
await expect(
60+
dataProvider.update('posts', {
61+
id: 3,
62+
data: { title: 'Updated' },
63+
previousData: { id: 3 },
64+
} as any)
65+
).rejects.toThrow('No item with identifier 3');
66+
await new Promise(resolve => setTimeout(resolve, 0));
67+
68+
expect(
69+
JSON.parse(
70+
localStorage.getItem('ra-data-local-storage-test') || '{}'
71+
)
72+
).toEqual({ posts });
73+
});
74+
75+
it('does not partially update local data when updateMany includes an unknown id', async () => {
76+
localStorage.setItem(
77+
'ra-data-local-storage-test',
78+
JSON.stringify({ posts })
79+
);
80+
const dataProvider = localStorageDataProvider({
81+
localStorageKey: 'ra-data-local-storage-test',
82+
localStorageUpdateDelay: 0,
83+
});
84+
85+
await expect(
86+
dataProvider.updateMany('posts', {
87+
ids: [1, 3],
88+
data: { title: 'Updated' },
89+
} as any)
90+
).rejects.toThrow('No item with identifier 3');
91+
await new Promise(resolve => setTimeout(resolve, 0));
92+
93+
expect(
94+
JSON.parse(
95+
localStorage.getItem('ra-data-local-storage-test') || '{}'
96+
)
97+
).toEqual({ posts });
98+
});
99+
100+
it('does not corrupt local data when delete targets an unknown id', async () => {
101+
localStorage.setItem(
102+
'ra-data-local-storage-test',
103+
JSON.stringify({ posts })
104+
);
105+
const dataProvider = localStorageDataProvider({
106+
localStorageKey: 'ra-data-local-storage-test',
107+
localStorageUpdateDelay: 0,
108+
});
109+
110+
await expect(
111+
dataProvider.delete('posts', {
112+
id: 3,
113+
previousData: { id: 3 },
114+
} as any)
115+
).rejects.toThrow('No item with identifier 3');
116+
await new Promise(resolve => setTimeout(resolve, 0));
117+
118+
expect(
119+
JSON.parse(
120+
localStorage.getItem('ra-data-local-storage-test') || '{}'
121+
)
122+
).toEqual({ posts });
123+
});
124+
125+
it('does not partially delete local data when deleteMany includes an unknown id', async () => {
126+
localStorage.setItem(
127+
'ra-data-local-storage-test',
128+
JSON.stringify({ posts })
129+
);
130+
const dataProvider = localStorageDataProvider({
131+
localStorageKey: 'ra-data-local-storage-test',
132+
localStorageUpdateDelay: 0,
133+
});
134+
135+
await expect(
136+
dataProvider.deleteMany('posts', {
137+
ids: [1, 3],
138+
} as any)
139+
).rejects.toThrow('No item with identifier 3');
140+
await new Promise(resolve => setTimeout(resolve, 0));
141+
142+
expect(
143+
JSON.parse(
144+
localStorage.getItem('ra-data-local-storage-test') || '{}'
145+
)
146+
).toEqual({ posts });
147+
});
43148
});

0 commit comments

Comments
 (0)