Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

(fix) - Remove special character from persistence keys and sanitize (#715)

* Refactor key serialization for persistence

- Avoid illegal character \t which is often stripped in IDB
- Move deserialization to keys.ts
- Add string sanitization

* Add changeset

* Fix idb reader in persisted-store example

authored by

Phil Plückthun and committed by
GitHub
e9617531 762a819a

+49 -31
+5
.changeset/honest-mangos-rule.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Fix persistence using special tab character in serialized keys and add sanitization to persistence key serializer.
+10 -2
exchanges/graphcache/examples/persisted-store/src/app/index.tsx
··· 12 12 db = await openDB('myApplication', 1, { 13 13 upgrade: db => db.createObjectStore('keyval'), 14 14 }); 15 - const result = (await db.getAll('keyval')) as SerializedEntries; 16 - return result; 15 + 16 + return Promise.all([ 17 + db.getAllKeys("keyval"), 18 + db.getAll("keyval"), 19 + ]).then(([keys, values]) => { 20 + return keys.reduce((acc: SerializedEntries, key, i) => { 21 + acc[key] = values[i]; 22 + return acc; 23 + }, {}); 24 + }); 17 25 }, 18 26 write: async batch => { 19 27 for (const key in batch) {
+4 -4
exchanges/graphcache/src/store/__snapshots__/store.test.ts.snap
··· 2 2 3 3 exports[`Store with storage should be able to store and rehydrate data 1`] = ` 4 4 Object { 5 - "Appointment:1 __typename": "\\"Appointment\\"", 6 - "Appointment:1 id": "\\"1\\"", 7 - "Appointment:1 info": "\\"urql meeting\\"", 8 - "Query appointment({\\"id\\":\\"1\\"})": ":\\"Appointment:1\\"", 5 + "Appointment:1.__typename": "\\"Appointment\\"", 6 + "Appointment:1.id": "\\"1\\"", 7 + "Appointment:1.info": "\\"urql meeting\\"", 8 + "Query.appointment({\\"id\\":\\"1\\"})": ":\\"Appointment:1\\"", 9 9 } 10 10 `;
+12 -18
exchanges/graphcache/src/store/data.ts
··· 9 9 } from '../types'; 10 10 11 11 import { 12 + serializeKeys, 13 + deserializeKeyInfo, 12 14 fieldInfoOfKey, 13 15 joinKeys, 14 - serializeKeys, 15 - indexOfSeparator, 16 16 } from './keys'; 17 17 18 18 import { makeDict } from '../helpers/dict'; ··· 543 543 const entries: SerializedEntries = makeDict(); 544 544 currentIgnoreOptimistic = true; 545 545 currentData!.persist.forEach(key => { 546 - const sepIndex = indexOfSeparator(key); 547 - if (sepIndex > -1) { 548 - const entityKey = key.slice(0, sepIndex); 549 - const fieldKey = key.slice(sepIndex + 1); 550 - let x: void | Link | EntityField; 551 - if ((x = readLink(entityKey, fieldKey)) !== undefined) { 552 - entries[key] = `:${stringifyVariables(x)}`; 553 - } else if ((x = readRecord(entityKey, fieldKey)) !== undefined) { 554 - entries[key] = stringifyVariables(x); 555 - } else { 556 - entries[key] = undefined; 557 - } 546 + const { entityKey, fieldKey } = deserializeKeyInfo(key); 547 + let x: void | Link | EntityField; 548 + if ((x = readLink(entityKey, fieldKey)) !== undefined) { 549 + entries[key] = `:${stringifyVariables(x)}`; 550 + } else if ((x = readRecord(entityKey, fieldKey)) !== undefined) { 551 + entries[key] = stringifyVariables(x); 552 + } else { 553 + entries[key] = undefined; 558 554 } 559 555 }); 560 556 ··· 573 569 574 570 for (const key in entries) { 575 571 const value = entries[key]; 576 - const sepIndex = indexOfSeparator(key); 577 - if (value && sepIndex > -1) { 578 - const entityKey = key.slice(0, sepIndex); 579 - const fieldKey = key.slice(sepIndex + 1); 572 + if (value !== undefined) { 573 + const { entityKey, fieldKey } = deserializeKeyInfo(key); 580 574 if (value[0] === ':') { 581 575 writeLink(entityKey, fieldKey, JSON.parse(value.slice(1))); 582 576 } else {
+12 -6
exchanges/graphcache/src/store/keys.ts
··· 1 1 import { stringifyVariables } from '@urql/core'; 2 - import { Variables, FieldInfo } from '../types'; 2 + import { Variables, FieldInfo, KeyInfo } from '../types'; 3 3 4 4 export const keyOfField = (fieldName: string, args?: null | Variables) => 5 5 args ? `${fieldName}(${stringifyVariables(args)})` : fieldName; 6 + 7 + export const joinKeys = (parentKey: string, key: string) => 8 + `${parentKey}.${key}`; 6 9 7 10 export const fieldInfoOfKey = (fieldKey: string): FieldInfo => { 8 11 const parenIndex = fieldKey.indexOf('('); ··· 21 24 } 22 25 }; 23 26 24 - export const joinKeys = (parentKey: string, key: string) => 25 - `${parentKey}.${key}`; 27 + export const serializeKeys = (entityKey: string, fieldKey: string) => 28 + `${entityKey.replace(/\./g, '\\.')}.${fieldKey}`; 26 29 27 - export const serializeKeys = (entityKey: string, fieldKey: string) => 28 - `${entityKey}\t${fieldKey}`; 29 - export const indexOfSeparator = (key: string) => key.indexOf('\t'); 30 + export const deserializeKeyInfo = (key: string): KeyInfo => { 31 + const dotIndex = key.indexOf('.'); 32 + const entityKey = key.slice(0, dotIndex).replace(/\\\./g, '.'); 33 + const fieldKey = key.slice(dotIndex + 1); 34 + return { entityKey, fieldKey }; 35 + };
+1 -1
exchanges/graphcache/src/store/store.test.ts
··· 568 568 const serialisedStore = (storage.write as any).mock.calls[0][0]; 569 569 570 570 expect(serialisedStore).toEqual({ 571 - 'Query\tbase': 'true', 571 + 'Query.base': 'true', 572 572 }); 573 573 574 574 store = new Store();
+5
exchanges/graphcache/src/types.ts
··· 45 45 arguments: Variables | null; 46 46 } 47 47 48 + export interface KeyInfo { 49 + entityKey: string; 50 + fieldKey: string; 51 + } 52 + 48 53 // This is an input operation 49 54 export interface OperationRequest { 50 55 query: DocumentNode;