UNPKG

@shopify/shopify-app-session-storage-redis

Version:
193 lines (161 loc) 5.59 kB
import {RedisClientOptions, createClient} from 'redis'; import {Session} from '@shopify/shopify-api'; import { SessionStorage, SessionStorageMigratorOptions, SessionStorageMigrator, } from '@shopify/shopify-app-session-storage'; import {migrationList} from './migrations'; import {RedisConnection} from './redis-connection'; import {RedisSessionStorageMigrator} from './redis-migrator'; type RedisClient = ReturnType<typeof createClient>; /* eslint-disable @shopify/typescript-prefer-pascal-case-enums */ enum ShopifyStorageOption { sessionKeyPrefix = 'sessionKeyPrefix', migratorOptions = 'migratorOptions', onError = 'onError', } /* eslint-enable @shopify/typescript-prefer-pascal-case-enums */ export interface RedisSessionStorageOptions extends RedisClientOptions { [ShopifyStorageOption.sessionKeyPrefix]: string; [ShopifyStorageOption.migratorOptions]?: SessionStorageMigratorOptions; [ShopifyStorageOption.onError]?: (...args: any[]) => void; } const defaultRedisSessionStorageOptions: RedisSessionStorageOptions = { sessionKeyPrefix: 'shopify_sessions', migratorOptions: { migrationDBIdentifier: 'migrations', }, }; export class RedisSessionStorage implements SessionStorage { static withCredentials( host: string, db: number, username: string, password: string, opts: Partial<RedisSessionStorageOptions>, ) { return new RedisSessionStorage( new URL( `redis://${encodeURIComponent(username)}:${encodeURIComponent( password, )}@${host}/${db}`, ), opts, ); } public readonly ready: Promise<void>; private internalInit: Promise<void>; private options: RedisSessionStorageOptions; private client: RedisConnection; private migrator: SessionStorageMigrator; constructor( urlOrClient: URL | string | RedisClient, opts: Partial<RedisSessionStorageOptions> = {}, ) { const allowedClientKeys = Object.keys(ShopifyStorageOption); const disallowedClientKeys = Object.keys(opts).filter( (key) => !allowedClientKeys.includes(key), ); if ( typeof urlOrClient !== 'string' && !(urlOrClient instanceof URL) && disallowedClientKeys.length > 0 ) { throw new Error( 'Passing a RedisClient instance is not supported with options. Set the options when creating the client ' + 'instead.', ); } this.options = {...defaultRedisSessionStorageOptions, ...opts}; this.internalInit = this.init(urlOrClient); this.migrator = new RedisSessionStorageMigrator( this.client, this.options.migratorOptions, migrationList, ); this.ready = this.migrator.applyMigrations(this.internalInit); } public async storeSession(session: Session): Promise<boolean> { await this.ready; await this.client.set( session.id, JSON.stringify(session.toPropertyArray(true)), ); await this.addKeyToShopList(session); return true; } public async loadSession(id: string): Promise<Session | undefined> { await this.ready; let rawResult: any = await this.client.get(id); if (!rawResult) return undefined; rawResult = JSON.parse(rawResult); return Session.fromPropertyArray(rawResult, true); } public async deleteSession(id: string): Promise<boolean> { await this.ready; const session = await this.loadSession(id); if (session) { await this.removeKeyFromShopList(session.shop, id); await this.client.del(id); } return true; } public async deleteSessions(ids: string[]): Promise<boolean> { await this.ready; await Promise.all(ids.map((id) => this.deleteSession(id))); return true; } public async findSessionsByShop(shop: string): Promise<Session[]> { await this.ready; const idKeysArrayString = await this.client.get(shop); if (!idKeysArrayString) return []; const idKeysArray = JSON.parse(idKeysArrayString); const results: Session[] = []; for (const idKey of idKeysArray) { const rawResult = await this.client.get(idKey, false); if (!rawResult) continue; const session = Session.fromPropertyArray(JSON.parse(rawResult), true); results.push(session); } return results; } public async disconnect(): Promise<void> { await this.client.disconnect(); } private async addKeyToShopList(session: Session) { const shopKey = session.shop; const idKey = this.client.generateFullKey(session.id); const idKeysArrayString = await this.client.get(shopKey); if (idKeysArrayString) { const idKeysArray = JSON.parse(idKeysArrayString); if (!idKeysArray.includes(idKey)) { idKeysArray.push(idKey); await this.client.set(shopKey, JSON.stringify(idKeysArray)); } } else { await this.client.set(shopKey, JSON.stringify([idKey])); } } private async removeKeyFromShopList(shop: string, id: string) { const shopKey = shop; const idKey = this.client.generateFullKey(id); const idKeysArrayString = await this.client.get(shopKey); if (idKeysArrayString) { const idKeysArray = JSON.parse(idKeysArrayString); const index = idKeysArray.indexOf(idKey); if (index > -1) { idKeysArray.splice(index, 1); await this.client.set(shopKey, JSON.stringify(idKeysArray)); } } } private async init(urlOrClient: URL | string | RedisClient) { this.client = new RedisConnection( urlOrClient, this.options, this.options.sessionKeyPrefix, ); await this.client.connect(); } }