Skip to content

Commit bdaa055

Browse files
committed
Simplify local data provider resource guard
1 parent 2dcb9b3 commit bdaa055

4 files changed

Lines changed: 83 additions & 93 deletions

File tree

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ describe('ra-data-local-forage', () => {
5151
).rejects.toThrow('Invalid resource key: __proto__');
5252
});
5353

54+
it('supports resource keys inherited from Object.prototype', async () => {
55+
const dataProvider = localForageDataProvider();
56+
57+
const response = await dataProvider.create('constructor', {
58+
data: { title: 'Hello world' },
59+
} as any);
60+
61+
expect(response.data.title).toEqual('Hello world');
62+
expect(localforage.setItem).toHaveBeenCalledWith(
63+
'ra-data-local-forage-constructor',
64+
[expect.objectContaining({ title: 'Hello world' })]
65+
);
66+
});
67+
5468
it('does not corrupt local data when update targets an unknown id', async () => {
5569
(localforage.keys as jest.Mock).mockResolvedValue([
5670
'ra-data-local-forage-posts',

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

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

178-
assertRecordsExist(getResourceCollection(data, resource), [
179-
params.id,
180-
]);
178+
assertRecordsExist(getResourceCollection(data, resource), [params.id]);
181179
const response = await baseDataProvider.update<RecordType>(
182180
resource,
183181
params
@@ -210,10 +208,7 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
210208

211209
const resourceData = getResourceCollection(data, resource);
212210
assertRecordsExist(resourceData, params.ids);
213-
const response = await baseDataProvider.updateMany(
214-
resource,
215-
params
216-
);
211+
const response = await baseDataProvider.updateMany(resource, params);
217212

218213
params.ids.forEach((id: Identifier) => {
219214
const index = resourceData.findIndex(
@@ -269,9 +264,7 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
269264
if (!data) {
270265
throw new Error('The dataProvider is not initialized.');
271266
}
272-
assertRecordsExist(getResourceCollection(data, resource), [
273-
params.id,
274-
]);
267+
assertRecordsExist(getResourceCollection(data, resource), [params.id]);
275268
const response = await baseDataProvider.delete<RecordType>(
276269
resource,
277270
params
@@ -300,10 +293,7 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
300293
}
301294
const resourceData = getResourceCollection(data, resource);
302295
assertRecordsExist(resourceData, params.ids);
303-
const response = await baseDataProvider.deleteMany(
304-
resource,
305-
params
306-
);
296+
const response = await baseDataProvider.deleteMany(resource, params);
307297
const indexes = params.ids
308298
.map((id: any) => {
309299
return resourceData.findIndex(
@@ -320,37 +310,28 @@ export default (params?: LocalForageDataProviderParams): DataProvider => {
320310
};
321311

322312
const getResourceCollection = (data: Record<string, any>, resource: string) => {
323-
const resourceData = data[resource];
324-
325-
if (!resourceData) {
313+
if (!Object.prototype.hasOwnProperty.call(data, resource)) {
326314
throw new Error(`Unknown resource key: ${resource}`);
327315
}
328316

329-
return resourceData;
317+
return data[resource];
330318
};
331319

332320
const getOrCreateResourceCollection = (
333321
data: Record<string, any>,
334322
resource: string
335323
) => {
336-
const resourceData = data[resource];
337-
if (resourceData) {
338-
return resourceData;
324+
if (!Object.prototype.hasOwnProperty.call(data, resource)) {
325+
data[resource] = [];
339326
}
340327

341-
Object.defineProperty(data, resource, {
342-
value: [],
343-
writable: true,
344-
enumerable: true,
345-
configurable: true,
346-
});
347-
348328
return data[resource];
349329
};
350330

351331
const checkResource = resource => {
352-
if (['__proto__', 'constructor', 'prototype'].includes(resource)) {
353-
// protection against prototype pollution
332+
// Reject "__proto__" so dynamic writes like data[resource] = value don't
333+
// mutate Object.prototype instead of creating a normal resource collection.
334+
if (resource === '__proto__') {
354335
throw new Error(`Invalid resource key: ${resource}`);
355336
}
356337
};

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,28 @@ describe('ra-data-local-storage', () => {
4646
).toThrow('Invalid resource key: __proto__');
4747
});
4848

49+
it('supports resource keys inherited from Object.prototype', async () => {
50+
const dataProvider = localStorageDataProvider({
51+
localStorageKey: 'ra-data-local-storage-test',
52+
localStorageUpdateDelay: 0,
53+
});
54+
55+
const response = await dataProvider.create('constructor', {
56+
data: { title: 'Hello world' },
57+
} as any);
58+
59+
await new Promise(resolve => setTimeout(resolve, 0));
60+
61+
expect(response.data.title).toEqual('Hello world');
62+
expect(
63+
JSON.parse(
64+
localStorage.getItem('ra-data-local-storage-test') || '{}'
65+
)
66+
).toMatchObject({
67+
constructor: [expect.objectContaining({ title: 'Hello world' })],
68+
});
69+
});
70+
4971
it('does not corrupt local data when update targets an unknown id', async () => {
5072
localStorage.setItem(
5173
'ra-data-local-storage-test',

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

Lines changed: 36 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,7 @@ export default (params?: LocalStorageDataProviderParams): DataProvider => {
110110
.update<RecordType>(resource, params)
111111
.then(response => {
112112
updateLocalStorage(() => {
113-
const resourceData = getResourceCollection(
114-
data,
115-
resource
116-
);
113+
const resourceData = getResourceCollection(data, resource);
117114
const index = resourceData.findIndex(
118115
record => record.id == params.id
119116
);
@@ -142,32 +139,27 @@ export default (params?: LocalStorageDataProviderParams): DataProvider => {
142139
return Promise.reject(error);
143140
}
144141

145-
return baseDataProvider
146-
.updateMany(resource, params)
147-
.then(response => {
148-
updateLocalStorage(() => {
149-
const resourceData = getResourceCollection(
150-
data,
151-
resource
142+
return baseDataProvider.updateMany(resource, params).then(response => {
143+
updateLocalStorage(() => {
144+
const resourceData = getResourceCollection(data, resource);
145+
params.ids.forEach(id => {
146+
const index = resourceData.findIndex(
147+
record => record.id == id
152148
);
153-
params.ids.forEach(id => {
154-
const index = resourceData.findIndex(
155-
record => record.id == id
156-
);
157149

158-
if (index === -1) {
159-
return;
160-
}
150+
if (index === -1) {
151+
return;
152+
}
161153

162-
resourceData.splice(index, 1, {
163-
...resourceData[index],
164-
...params.data,
165-
});
154+
resourceData.splice(index, 1, {
155+
...resourceData[index],
156+
...params.data,
166157
});
167158
});
168-
169-
return response;
170159
});
160+
161+
return response;
162+
});
171163
},
172164
create: <RecordType extends Omit<RaRecord, 'id'> = any>(
173165
resource,
@@ -201,10 +193,7 @@ export default (params?: LocalStorageDataProviderParams): DataProvider => {
201193
.delete<RecordType>(resource, params)
202194
.then(response => {
203195
updateLocalStorage(() => {
204-
const resourceData = getResourceCollection(
205-
data,
206-
resource
207-
);
196+
const resourceData = getResourceCollection(data, resource);
208197
const index = resourceData.findIndex(
209198
record => record.id == params.id
210199
);
@@ -230,60 +219,44 @@ export default (params?: LocalStorageDataProviderParams): DataProvider => {
230219
return Promise.reject(error);
231220
}
232221

233-
return baseDataProvider
234-
.deleteMany(resource, params)
235-
.then(response => {
236-
updateLocalStorage(() => {
237-
const resourceData = getResourceCollection(
238-
data,
239-
resource
240-
);
241-
const indexes = params.ids
242-
.map(id =>
243-
resourceData.findIndex(
244-
record => record.id == id
245-
)
246-
)
247-
.filter(index => index !== -1);
222+
return baseDataProvider.deleteMany(resource, params).then(response => {
223+
updateLocalStorage(() => {
224+
const resourceData = getResourceCollection(data, resource);
225+
const indexes = params.ids
226+
.map(id =>
227+
resourceData.findIndex(record => record.id == id)
228+
)
229+
.filter(index => index !== -1);
248230

249-
pullAt(resourceData, indexes);
250-
});
251-
252-
return response;
231+
pullAt(resourceData, indexes);
253232
});
233+
234+
return response;
235+
});
254236
},
255237
};
256238
};
257239

258240
const getResourceCollection = (data, resource) => {
259-
const resourceData = data[resource];
260-
261-
if (!resourceData) {
241+
if (!Object.prototype.hasOwnProperty.call(data, resource)) {
262242
throw new Error(`Unknown resource key: ${resource}`);
263243
}
264244

265-
return resourceData;
245+
return data[resource];
266246
};
267247

268248
const getOrCreateResourceCollection = (data, resource) => {
269-
const resourceData = data[resource];
270-
if (resourceData) {
271-
return resourceData;
249+
if (!Object.prototype.hasOwnProperty.call(data, resource)) {
250+
data[resource] = [];
272251
}
273252

274-
Object.defineProperty(data, resource, {
275-
value: [],
276-
writable: true,
277-
enumerable: true,
278-
configurable: true,
279-
});
280-
281253
return data[resource];
282254
};
283255

284256
const checkResource = resource => {
285-
if (['__proto__', 'constructor', 'prototype'].includes(resource)) {
286-
// protection against prototype pollution
257+
// Reject "__proto__" so dynamic writes like data[resource] = value don't
258+
// mutate Object.prototype instead of creating a normal resource collection.
259+
if (resource === '__proto__') {
287260
throw new Error(`Invalid resource key: ${resource}`);
288261
}
289262
};

0 commit comments

Comments
 (0)