diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2af0397..de492fb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased ### Added - Added Storefront API client under `Shopify.Clients.Storefront` +- Add `isActive()` method to `Session` class to check if session is active, replace `Session` with `SessionInterface` when used as a type [#153](https://github.com/Shopify/shopify-node-api/pull/153) ## [1.2.1] - 2021-03-26 ### Added diff --git a/docs/usage/customsessions.md b/docs/usage/customsessions.md index c32e0bab2..78ff0b1ce 100644 --- a/docs/usage/customsessions.md +++ b/docs/usage/customsessions.md @@ -12,8 +12,8 @@ When you're ready to deploy your app and run it in production, you'll need to se | Method | Arg type | Return type | Notes | | ------- | ------- | ------------ | -------| -| `storeCallback` | `Session` | `Promise` | Takes in the `Session` to be stored or updated, returns a `boolean` (`true` if stored successfully).
This callback is used both to save new a `Session` and to **update an existing `Session`**. | -| `loadCallback` | `string` | `Promise \| undefined> ` | Takes in the id of the `Session` to load (as a `string`) and returns either an instance of a `Session`, an object to be used to instantiate a `Session`, or `undefined` if no record is found for the specified id. | +| `storeCallback` | `SessionInterface` | `Promise` | Takes in the `Session` to be stored or updated, returns a `boolean` (`true` if stored successfully).
This callback is used both to save new a `Session` and to **update an existing `Session`**. | +| `loadCallback` | `string` | `Promise \| undefined> ` | Takes in the id of the `Session` to load (as a `string`) and returns either an instance of a `Session`, an object to be used to instantiate a `Session`, or `undefined` if no record is found for the specified id. | | `deleteCallback` | `string` | `Promise` | Takes in the id of the `Session` to load (as a `string`) and returns a `booelan` (`true` if deleted successfully). | ## Example usage diff --git a/src/auth/session/session.ts b/src/auth/session/session.ts index b3b6ce232..c4dc36f29 100644 --- a/src/auth/session/session.ts +++ b/src/auth/session/session.ts @@ -1,9 +1,12 @@ import {OnlineAccessInfo} from '../oauth/types'; +import {Context} from '../../context'; + +import {SessionInterface} from './types'; /** * Stores App information from logged in merchants so they can make authenticated requests to the Admin API. */ -class Session { +class Session implements SessionInterface { public static cloneSession(session: Session, newId: string): Session { const newSession = new Session(newId); @@ -27,6 +30,14 @@ class Session { public onlineAccessInfo?: OnlineAccessInfo; constructor(readonly id: string) {} + + public isActive(): boolean { + const scopesUnchanged = Context.SCOPES.equals(this.scope); + if (scopesUnchanged && this.accessToken && (!this.expires || this.expires >= new Date())) { + return true; + } + return false; + } } export {Session}; diff --git a/src/auth/session/session_storage.ts b/src/auth/session/session_storage.ts index 8c236c26a..c5ee2a5c4 100644 --- a/src/auth/session/session_storage.ts +++ b/src/auth/session/session_storage.ts @@ -1,4 +1,4 @@ -import {Session} from './session'; +import {SessionInterface} from './types'; /** * Defines the strategy to be used to store sessions for the Shopify App. @@ -10,14 +10,14 @@ interface SessionStorage { * * @param session Session to store */ - storeSession(session: Session): Promise; + storeSession(session: SessionInterface): Promise; /** * Loads a session from storage. * * @param id Id of the session to load */ - loadSession(id: string): Promise; + loadSession(id: string): Promise; /** * Deletes a session from storage. diff --git a/src/auth/session/storage/custom.ts b/src/auth/session/storage/custom.ts index 39c395934..44d351ccd 100644 --- a/src/auth/session/storage/custom.ts +++ b/src/auth/session/storage/custom.ts @@ -1,11 +1,12 @@ import {Session} from '../session'; +import {SessionInterface} from '../types'; import {SessionStorage} from '../session_storage'; import * as ShopifyErrors from '../../../error'; export class CustomSessionStorage implements SessionStorage { constructor( - readonly storeCallback: (session: Session) => Promise, - readonly loadCallback: (id: string) => Promise | undefined>, + readonly storeCallback: (session: SessionInterface) => Promise, + readonly loadCallback: (id: string) => Promise | undefined>, readonly deleteCallback: (id: string) => Promise, ) { this.storeCallback = storeCallback; @@ -13,7 +14,7 @@ export class CustomSessionStorage implements SessionStorage { this.deleteCallback = deleteCallback; } - public async storeSession(session: Session): Promise { + public async storeSession(session: SessionInterface): Promise { try { return await this.storeCallback(session); } catch (error) { @@ -23,8 +24,8 @@ export class CustomSessionStorage implements SessionStorage { } } - public async loadSession(id: string): Promise { - let result: Session | Record | undefined; + public async loadSession(id: string): Promise { + let result: SessionInterface | Record | undefined; try { result = await this.loadCallback(id); } catch (error) { @@ -41,7 +42,7 @@ export class CustomSessionStorage implements SessionStorage { return result; } else if (result instanceof Object && 'id' in result) { let session = new Session(result.id as string); - session = {...session, ...result}; + session = {...session, ...result as SessionInterface}; if (session.expires && typeof session.expires === 'string') { session.expires = new Date(session.expires); diff --git a/src/auth/session/storage/memory.ts b/src/auth/session/storage/memory.ts index eaef1f024..14142f45d 100644 --- a/src/auth/session/storage/memory.ts +++ b/src/auth/session/storage/memory.ts @@ -1,15 +1,15 @@ -import {Session} from '../session'; +import {SessionInterface} from '../types'; import {SessionStorage} from '../session_storage'; export class MemorySessionStorage implements SessionStorage { - private sessions: { [id: string]: Session; } = {}; + private sessions: { [id: string]: SessionInterface; } = {}; - public async storeSession(session: Session): Promise { + public async storeSession(session: SessionInterface): Promise { this.sessions[session.id] = session; return true; } - public async loadSession(id: string): Promise { + public async loadSession(id: string): Promise { return this.sessions[id] || undefined; } diff --git a/src/auth/session/test/custom.test.ts b/src/auth/session/storage/test/custom.test.ts similarity index 97% rename from src/auth/session/test/custom.test.ts rename to src/auth/session/storage/test/custom.test.ts index 79b2a4c29..b556c3133 100644 --- a/src/auth/session/test/custom.test.ts +++ b/src/auth/session/storage/test/custom.test.ts @@ -1,8 +1,8 @@ -import '../../../test/test_helper'; +import '../../../../test/test_helper'; -import {Session} from '../session'; -import {CustomSessionStorage} from '../storage/custom'; -import {SessionStorageError} from '../../../error'; +import {Session} from '../../session'; +import {CustomSessionStorage} from '../custom'; +import {SessionStorageError} from '../../../../error'; describe('custom session storage', () => { test('can perform actions', async () => { diff --git a/src/auth/session/test/memory.test.ts b/src/auth/session/storage/test/memory.test.ts similarity index 87% rename from src/auth/session/test/memory.test.ts rename to src/auth/session/storage/test/memory.test.ts index 98e06e9a1..8ac9e8930 100644 --- a/src/auth/session/test/memory.test.ts +++ b/src/auth/session/storage/test/memory.test.ts @@ -1,7 +1,7 @@ -import '../../../test/test_helper'; +import '../../../../test/test_helper'; -import {Session} from '../session'; -import {MemorySessionStorage} from '../storage/memory'; +import {Session} from '../../session'; +import {MemorySessionStorage} from '../memory'; test('can store and delete sessions in memory', async () => { const sessionId = 'test_session'; diff --git a/src/auth/session/test/session.test.ts b/src/auth/session/test/session.test.ts new file mode 100644 index 000000000..cf37463b6 --- /dev/null +++ b/src/auth/session/test/session.test.ts @@ -0,0 +1,35 @@ +import '../../../test/test_helper'; +import {Session} from '..'; + +describe('session', () => { + it('can clone a session', () => { + const session = new Session('original'); + const sessionClone = Session.cloneSession(session, 'new'); + + expect(session.id).not.toEqual(sessionClone.id); + expect(session.shop).toStrictEqual(sessionClone.shop); + expect(session.state).toStrictEqual(sessionClone.state); + expect(session.scope).toStrictEqual(sessionClone.scope); + expect(session.expires).toStrictEqual(sessionClone.expires); + expect(session.isOnline).toStrictEqual(sessionClone.isOnline); + expect(session.accessToken).toStrictEqual(sessionClone.accessToken); + expect(session.onlineAccessInfo).toStrictEqual(sessionClone.onlineAccessInfo); + }); +}); + +describe('isActive', () => { + it('returns true if session is active', () => { + const session = new Session('active'); + session.scope = 'test_scope'; + session.accessToken = 'indeed'; + session.expires = new Date(Date.now() + 86400); + expect(session.isActive()).toBeTruthy(); + }); + + it('returns false if session is not active', () => { + const session = new Session('not_active'); + session.scope = 'not_same'; + session.expires = new Date(Date.now() - 1); + expect(session.isActive()).toBeFalsy(); + }); +}); diff --git a/src/auth/session/types.ts b/src/auth/session/types.ts new file mode 100644 index 000000000..39a450c8d --- /dev/null +++ b/src/auth/session/types.ts @@ -0,0 +1,13 @@ +import {OnlineAccessInfo} from '../oauth/types'; + +export interface SessionInterface { + readonly id: string; + shop: string; + state: string; + scope: string; + expires?: Date; + isOnline?: boolean; + accessToken?: string; + onlineAccessInfo?: OnlineAccessInfo; + isActive(): boolean; +} diff --git a/src/auth/types.ts b/src/auth/types.ts new file mode 100644 index 000000000..9907ded20 --- /dev/null +++ b/src/auth/types.ts @@ -0,0 +1,2 @@ +export * from './oauth/types'; +export * from './session/types'; diff --git a/src/base_types.ts b/src/base_types.ts index 91b2a965b..d9ce7d9f1 100644 --- a/src/base_types.ts +++ b/src/base_types.ts @@ -1,5 +1,5 @@ -import {SessionStorage} from './auth/session'; import {AuthScopes} from './auth/scopes'; +import {SessionStorage} from './auth/session/session_storage'; export interface ContextParams { API_KEY: string; diff --git a/src/context.ts b/src/context.ts index 1ce57277b..c86fefe3b 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,5 +1,6 @@ import * as ShopifyErrors from './error'; -import {SessionStorage, MemorySessionStorage} from './auth/session'; +import {SessionStorage} from './auth/session/session_storage'; +import {MemorySessionStorage} from './auth/session/storage/memory'; import {ApiVersion, ContextParams} from './base_types'; import {AuthScopes} from './auth/scopes'; diff --git a/src/types.ts b/src/types.ts index 027093a90..17ffebe52 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ export * from './base_types'; -export * from './auth/oauth/types'; +export * from './auth/types'; export * from './clients/types'; export * from './utils/types'; export * from './webhooks/types';