UNPKG

trusted

Version:

⚖️ Trustworthy localStorage. Validate against a schema, set default values.

289 lines (264 loc) 8.24 kB
export interface TrustedOptions { namespace?: string; } export interface TrustedAccessorOptions<T> { key: string; defaultValue?: T; yupSchema?: any; validate?: (value: T) => boolean; skipRegistration?: boolean; unmarshal?: (localValue: string) => T; marshal?: (value: T) => string; } export type TrustedTypeAccessorOptions<T> = Omit< TrustedAccessorOptions<T>, 'marshal' | 'unmarshal' >; export interface TrustedAccessor<T> { get: () => T; set: (value: T) => void; remove: () => void; unregister: () => void; getKey: () => string; getDefaultValue: () => T; } export class Trusted { private namespace: string | undefined; private registeredKeys: Set<string>; constructor(options: TrustedOptions = {}) { const { namespace } = options; this.namespace = namespace; this.registeredKeys = new Set(); this.accessor.bind(this); } registerKey(key: string) { if (this.registeredKeys.has(key)) { throw new Error(`Key has already been registered: ${key}`); } this.registeredKeys.add(key); } unregisterKey(key: string) { this.registeredKeys.delete(key); } accessor<T>( options: TrustedAccessorOptions<T> & { defaultValue: T } ): TrustedAccessor<T>; accessor<T>( options: TrustedAccessorOptions<T> ): TrustedAccessor<T | undefined>; accessor<T>( options: TrustedAccessorOptions<T> ): TrustedAccessor<T | undefined> { const key = `${this.namespace || ''}${options.key}`; const { yupSchema, skipRegistration, validate = yupSchema ? (value: T) => yupSchema.isValidSync(value) : undefined, unmarshal, marshal, defaultValue, } = options; if ( defaultValue !== undefined && defaultValue !== null && typeof validate === 'function' && !validate(defaultValue) ) { throw new Error( `Invalid default value provided at key: ${key}. Please check your yupSchema.` ); } if ( defaultValue !== undefined && defaultValue !== null && typeof defaultValue !== 'string' && typeof marshal !== 'function' ) { throw new Error( `Invalid configuration provided at key: ${key}. Non-string default values must be accompanied by a marshal function.` ); } if (!skipRegistration) { this.registerKey(key); } return { get: () => { let item; try { item = localStorage.getItem(key); if (item === undefined || item === null) { throw new Error(`Item not found at key: ${key}.`); } item = typeof unmarshal === 'function' ? unmarshal(item) : item; if (typeof validate === 'function' && !validate(item as T)) { throw new Error(`Item is invalid at key: ${key}.`); } return item as T; } catch (e) { if ( defaultValue !== undefined && defaultValue !== null && typeof marshal === 'function' ) { localStorage.setItem(key, marshal(defaultValue)); } else if (typeof defaultValue === 'string') { localStorage.setItem(key, defaultValue); } else if (defaultValue === undefined) { localStorage.removeItem(key); } return defaultValue; } }, set: value => { if ( validate && value !== undefined && value !== null && !validate(value) ) { console.error(`Invalid value provided at key: ${key}.`, value); return; } if ( value !== undefined && value !== null && typeof marshal === 'function' ) { localStorage.setItem(key, marshal(value)); } else if (typeof value === 'string') { localStorage.setItem(key, value); } else { console.warn( `You have attempted to set a non-string value in local storage without marshaling. This operation has been prevented.\nkey: ${key}\nvalue: ${value}` ); } }, remove: () => { localStorage.removeItem(key); }, unregister: () => { this.unregisterKey(key); }, getDefaultValue: () => { return defaultValue; }, getKey: () => { return key; }, }; } string<T extends string = string>( accessorOptions: TrustedTypeAccessorOptions<T> & { defaultValue: T } ): TrustedAccessor<T>; string<T extends string = string>( accessorOptions: TrustedTypeAccessorOptions<T> ): TrustedAccessor<T | undefined>; string<T extends string = string>( accessorOptions: TrustedTypeAccessorOptions<T> ) { return this.accessor<T>(accessorOptions); } boolean<T extends boolean = boolean>( accessorOptions: TrustedTypeAccessorOptions<T> & { defaultValue: T } ): TrustedAccessor<T>; boolean<T extends boolean = boolean>( accessorOptions: TrustedTypeAccessorOptions<T> ): TrustedAccessor<T | undefined>; boolean<T extends boolean = boolean>( accessorOptions: TrustedTypeAccessorOptions<T> ) { return this.accessor<T>({ ...accessorOptions, marshal: JSON.stringify, unmarshal: JSON.parse, }); } object<T extends Record<any, any>>( accessorOptions: TrustedTypeAccessorOptions<T> & { defaultValue: T } ): TrustedAccessor<T>; object<T extends Record<any, any>>( accessorOptions: TrustedTypeAccessorOptions<T> ): TrustedAccessor<T | undefined>; object<T extends Record<any, any>>( accessorOptions: TrustedTypeAccessorOptions<T> ) { return this.accessor<T>({ ...accessorOptions, marshal: JSON.stringify, unmarshal: JSON.parse, }); } number<T extends number = number>( accessorOptions: TrustedTypeAccessorOptions<T> & { defaultValue: T } ): TrustedAccessor<T>; number<T extends number = number>( accessorOptions: TrustedTypeAccessorOptions<T> ): TrustedAccessor<T | undefined>; number<T extends number = number>( accessorOptions: TrustedTypeAccessorOptions<T> ) { return this.accessor<T>({ ...accessorOptions, marshal: JSON.stringify, unmarshal: JSON.parse, }); } array<T, A extends T[] = T[]>( accessorOptions: TrustedTypeAccessorOptions<A> & { defaultValue: A; } ): TrustedAccessor<A>; array<T, A extends T[] = T[]>( accessorOptions: TrustedTypeAccessorOptions<A> ): TrustedAccessor<A | undefined>; array<T, A extends T[] = T[]>( accessorOptions: TrustedTypeAccessorOptions<A> ) { return this.accessor<A>({ ...accessorOptions, marshal: JSON.stringify, unmarshal: JSON.parse, }); } map<K extends string | number | symbol, T, M extends Map<K, T> = Map<K, T>>( accessorOptions: TrustedTypeAccessorOptions<M> & { defaultValue: M } ): TrustedAccessor<M>; map<K extends string | number | symbol, T, M extends Map<K, T> = Map<K, T>>( accessorOptions: TrustedTypeAccessorOptions<M> ): TrustedAccessor<M | undefined>; map<K extends string | number | symbol, T, M extends Map<K, T> = Map<K, T>>( accessorOptions: TrustedTypeAccessorOptions<M> ) { return this.accessor<M>({ ...accessorOptions, marshal: (map: M) => JSON.stringify(Array.from(map)), unmarshal: (localString: string) => new Map<K, T>(JSON.parse(localString)) as M, }); } set<T, S extends Set<T> = Set<T>>( accessorOptions: TrustedAccessorOptions<S> ) { return this.accessor<S>({ ...accessorOptions, marshal: (set: S) => JSON.stringify(Array.from(set)), unmarshal: (localString: string) => new Set<T>(JSON.parse(localString)) as S, }); } date<T extends Date>( accessorOptions: TrustedAccessorOptions<T> & { defaultValue: T } ): TrustedAccessor<T>; date<T extends Date>( accessorOptions: TrustedAccessorOptions<T> ): TrustedAccessor<T | undefined>; date<T extends Date>(accessorOptions: TrustedAccessorOptions<T>) { return this.accessor<T>({ ...accessorOptions, marshal: value => value.toISOString(), unmarshal: (localString: string) => new Date(localString) as T, }); } }