From ae42107abdb320fb51891638c7d66ba96ce5de86 Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Tue, 19 Mar 2024 14:42:17 -0500 Subject: [PATCH 1/4] Update the Session to return all user information When a session is passed in with all of the user information include that in the transformation of the property array This will allow session adapters to start storing the user information Update docs Update docs Update docs fix comments --- .../docs/guides/session-storage.md | 110 +++++++++++++++++- packages/shopify-api/lib/auth/oauth/types.ts | 70 +++++------ .../lib/session/__tests__/session.test.ts | 57 +++++++-- packages/shopify-api/lib/session/session.ts | 92 ++++++++++++++- 4 files changed, 273 insertions(+), 56 deletions(-) diff --git a/packages/shopify-api/docs/guides/session-storage.md b/packages/shopify-api/docs/guides/session-storage.md index 6482c78f5..a7d3e9ea7 100644 --- a/packages/shopify-api/docs/guides/session-storage.md +++ b/packages/shopify-api/docs/guides/session-storage.md @@ -100,16 +100,64 @@ const {session, headers} = shopify.auth.callback({ const sessionProperties = session.toPropertyArray(); /* ... then sessionProperties will have the following data... - [ + [ ['id', 'online_session_id'], ['shop', 'online-session-shop'], ['state', 'online-session-state'], ['isOnline', true], ['scope', 'online-session-scope'], ['accessToken', 'online-session-token'], - ['expires', 1641013200000], // number, milliseconds since Jan 1, 1970 - ['onlineAccessInfo', 1], // only the `id` property of the `associated_user` property is stored - ] + ['expires', 1641013200000], // example = January 1, 2022, as number of milliseconds since Jan 1, 1970 + ['userId', 1], + ['first_name', 'online-session-first-name'], + ['last_name', 'online-session-last-name'], + ['email', 'online-session-email'], + ['locale', 'online-session-locale'], + ['email_verified', false] + ['account_owner', true,] + ['collaborator', false], + ], + */ +``` + +```ts +const {session, headers} = shopify.auth.callback({ + rawRequest: req, + rawResponse: res, +}); +/* + If session has the following data content... + { + id: 'online_session_id', + shop: 'online-session-shop', + state: 'online-session-state', + isOnline: true, + scope: 'online-session-scope', + accessToken: 'online-session-token', + expires: 2022-01-01T05:00:00.000Z, // Date object + onlineAccessInfo: { + expires_in: 1, + associated_user_scope: 'online-session-user-scope', + associated_user: { + id: 1, + } + } + } + */ + +const sessionProperties = session.toPropertyArray(); +/* + ... then sessionProperties will have the following data... + [ + ['id', 'online_session_id'], + ['shop', 'online-session-shop'], + ['state', 'online-session-state'], + ['isOnline', true], + ['scope', 'online-session-scope'], + ['accessToken', 'online-session-token'], + ['expires', 1641013200000], // example = January 1, 2022, as number of milliseconds since Jan 1, 1970 + ['onlineAccessInfo', 1], // The userID is returned under onlineAccessInfo + ], */ ``` @@ -155,8 +203,58 @@ const sessionProperties = session.toPropertyArray(); ['scope', 'online-session-scope'], ['accessToken', 'online-session-token'], ['expires', 1641013200000], // example = January 1, 2022, as number of milliseconds since Jan 1, 1970 - ['onlineAccessInfo', 1], // only the `id` property of the `associated_user` property is stored - ] + ['userId', 1], + ['first_name', 'online-session-first-name'], + ['last_name', 'online-session-last-name'], + ['email', 'online-session-email'], + ['locale', 'online-session-locale'], + ['email_verified', false] + ['account_owner', true,] + ['collaborator', false], + ], + */ + +const session = Session.fromPropertyArray(sessionProperties); +/* + ... then session will have the following data... + { + id: 'online_session_id', + shop: 'online-session-shop', + state: 'online-session-state', + isOnline: true, + scope: 'online-session-scope', + accessToken: 'online-session-token', + expires: 2022-01-01T05:00:00.000Z, // Date object + onlineAccessInfo: { + associated_user: { + id: 1, + first_name: 'online-session-first-name' + last_name: 'online-session-last-name', + email: 'online-session-email', + locale: 'online-session-locale', + email_verified: false, + account_owner: true, + collaborator: false, + }, + } + } + */ +``` + +```ts +const sessionProperties = session.toPropertyArray(); +/* + if sessionProperties has the following data, without the user data + [ + ['id', 'online_session_id'], + ['shop', 'online-session-shop'], + ['state', 'online-session-state'], + ['isOnline', true], + ['scope', 'online-session-scope'], + ['accessToken', 'online-session-token'], + ['expires', 1641013200000], // example = January 1, 2022, as number of milliseconds since Jan 1, 1970 + ['onlineAccessInfo', 1], + ], */ const session = Session.fromPropertyArray(sessionProperties); diff --git a/packages/shopify-api/lib/auth/oauth/types.ts b/packages/shopify-api/lib/auth/oauth/types.ts index b29e0833c..15d743f54 100644 --- a/packages/shopify-api/lib/auth/oauth/types.ts +++ b/packages/shopify-api/lib/auth/oauth/types.ts @@ -34,40 +34,42 @@ export interface OnlineAccessInfo { /** * The user associated with the access token. */ - associated_user: { - /** - * The user's ID. - */ - id: number; - /** - * The user's first name. - */ - first_name: string; - /** - * The user's last name. - */ - last_name: string; - /** - * The user's email address. - */ - email: string; - /** - * Whether the user has verified their email address. - */ - email_verified: boolean; - /** - * Whether the user is the account owner. - */ - account_owner: boolean; - /** - * The user's locale. - */ - locale: string; - /** - * Whether the user is a collaborator. - */ - collaborator: boolean; - }; + associated_user: OnlineAccessUser; +} + +export interface OnlineAccessUser { + /** + * The user's ID. + */ + id: number; + /** + * The user's first name. + */ + first_name?: string; + /** + * The user's last name. + */ + last_name?: string; + /** + * The user's email address. + */ + email?: string; + /** + * Whether the user has verified their email address. + */ + email_verified?: boolean; + /** + * Whether the user is the account owner. + */ + account_owner?: boolean; + /** + * The user's locale. + */ + locale?: string; + /** + * Whether the user is a collaborator. + */ + collaborator?: boolean; } export interface OnlineAccessResponse diff --git a/packages/shopify-api/lib/session/__tests__/session.test.ts b/packages/shopify-api/lib/session/__tests__/session.test.ts index f214f9636..c8cb7b696 100644 --- a/packages/shopify-api/lib/session/__tests__/session.test.ts +++ b/packages/shopify-api/lib/session/__tests__/session.test.ts @@ -1,6 +1,6 @@ import {Session} from '../session'; import {testConfig} from '../../__tests__/test-config'; -import {shopifyApi} from '../..'; +import {SessionParams, shopifyApi} from '../..'; describe('session', () => { it('can create a session from another session', () => { @@ -174,7 +174,13 @@ describe('isScopeChanged', () => { const expiresDate = new Date(Date.now() + 86400); const expiresNumber = expiresDate.getTime(); -const testSessions = [ +interface SessionTestData { + session: SessionParams; + propertyArray: [string, string | number | boolean][]; +} +type SessionTestDataArray = SessionTestData[]; + +const testSessions: SessionTestDataArray = [ { session: { id: 'offline_session_id', @@ -258,6 +264,7 @@ const testSessions = [ ], }, { + // Represents an online session fetched from the DB stored in the old format session: { id: 'online_session_id', shop: 'online-session-shop', @@ -271,13 +278,6 @@ const testSessions = [ associated_user_scope: 'online-session-user-scope', associated_user: { id: 1, - first_name: 'online-session-first-name', - last_name: 'online-session-last-name', - email: 'online-session-email', - locale: 'online-session-locale', - email_verified: true, - account_owner: true, - collaborator: false, }, }, }, @@ -298,6 +298,32 @@ const testSessions = [ shop: 'online-session-shop', state: 'online-session-state', isOnline: true, + onlineAccessInfo: { + expires_in: 1, + associated_user_scope: 'online-session-user-scope', + associated_user: { + id: 1, + }, + }, + }, + propertyArray: [ + ['id', 'online_session_id'], + ['shop', 'online-session-shop'], + ['state', 'online-session-state'], + ['isOnline', true], + ['onlineAccessInfo', 1], + ], + }, + { + // Represents a session stored in new format + session: { + id: 'online_session_id', + shop: 'online-session-shop', + state: 'online-session-state', + isOnline: true, + scope: 'online-session-scope', + accessToken: 'online-session-token', + expires: expiresDate, onlineAccessInfo: { expires_in: 1, associated_user_scope: 'online-session-user-scope', @@ -318,11 +344,20 @@ const testSessions = [ ['shop', 'online-session-shop'], ['state', 'online-session-state'], ['isOnline', true], - ['onlineAccessInfo', 1], + ['scope', 'online-session-scope'], + ['accessToken', 'online-session-token'], + ['expires', expiresNumber], + ['userId', 1], + ['firstName', 'online-session-first-name'], + ['lastName', 'online-session-last-name'], + ['email', 'online-session-email'], + ['locale', 'online-session-locale'], + ['emailVerified', true], + ['accountOwner', true], + ['collaborator', false], ], }, ]; - describe('toObject', () => { testSessions.forEach((test) => { const onlineOrOffline = test.session.isOnline ? 'online' : 'offline'; diff --git a/packages/shopify-api/lib/session/session.ts b/packages/shopify-api/lib/session/session.ts index 568e8fe95..6c6c879c1 100644 --- a/packages/shopify-api/lib/session/session.ts +++ b/packages/shopify-api/lib/session/session.ts @@ -1,5 +1,5 @@ import {InvalidSession} from '../error'; -import {OnlineAccessInfo} from '../auth/oauth/types'; +import {OnlineAccessInfo, OnlineAccessUser} from '../auth/oauth/types'; import {AuthScopes} from '../auth/scopes'; import {SessionParams} from './types'; @@ -14,6 +14,10 @@ const propertiesToSave = [ 'expires', 'onlineAccessInfo', ]; + +interface AssociatedUserObject { + associated_user: Partial; +} /** * Stores App information from logged in merchants so they can make authenticated requests to the Admin API. */ @@ -27,6 +31,9 @@ export class Session { ); } + const associatedUserObj: AssociatedUserObject = { + associated_user: {}, + }; const obj = Object.fromEntries( entries .filter(([_key, value]) => value !== null && value !== undefined) @@ -39,10 +46,21 @@ export class Session { return ['accessToken', value]; case 'onlineaccessinfo': return ['onlineAccessInfo', value]; + case 'firstname': + return ['firstName', value]; + case 'lastname': + return ['lastName', value]; + case 'accountowner': + return ['accountOwner', value]; + case 'emailverified': + return ['emailVerified', value]; + case 'userid': + return ['userId', value]; default: return [key.toLowerCase(), value]; } }) + // Sanitize values .map(([key, value]) => { switch (key) { @@ -66,11 +84,58 @@ export class Session { }, }, ]; + case 'userId': + return [ + key, + (associatedUserObj.associated_user.id = Number(value)), + ]; + case 'firstName': + return [ + key, + (associatedUserObj.associated_user.first_name = String(value)), + ]; + case 'lastName': + return [ + key, + (associatedUserObj.associated_user.last_name = String(value)), + ]; + case 'email': + return [ + key, + (associatedUserObj.associated_user.email = String(value)), + ]; + case 'accountOwner': + return [ + key, + (associatedUserObj.associated_user.account_owner = + Boolean(value)), + ]; + case 'locale': + return [ + key, + (associatedUserObj.associated_user.locale = String(value)), + ]; + case 'collaborator': + return [ + key, + (associatedUserObj.associated_user.collaborator = + Boolean(value)), + ]; + case 'emailVerified': + return [ + key, + (associatedUserObj.associated_user.email_verified = + Boolean(value)), + ]; default: return [key, value]; } }), ) as any; + // If the onlineAccessInfo is not present, we are using the new session info and add it to the object + if (!obj.onlineAccessInfo) { + obj.onlineAccessInfo = associatedUserObj; + } Object.setPrototypeOf(obj, Session.prototype); return obj; } @@ -205,14 +270,31 @@ export class Session { value !== null, ) // Prepare values for db storage - .map(([key, value]) => { + .flatMap(([key, value]) => { switch (key) { case 'expires': - return [key, value ? value.getTime() : undefined]; + return [[key, value ? value.getTime() : undefined]]; case 'onlineAccessInfo': - return [key, value?.associated_user?.id]; + if ( + value?.associated_user && + Object.keys(value.associated_user).length === 1 && + value.associated_user.id !== undefined + ) { + return [[key, value.associated_user.id]]; + } else { + return [ + ['userId', value?.associated_user?.id], + ['firstName', value?.associated_user?.first_name], + ['lastName', value?.associated_user?.last_name], + ['email', value?.associated_user?.email], + ['locale', value?.associated_user?.locale], + ['emailVerified', value?.associated_user?.email_verified], + ['accountOwner', value?.associated_user?.account_owner], + ['collaborator', value?.associated_user?.collaborator], + ]; + } default: - return [key, value]; + return [[key, value]]; } }) ); From b912ecd375430f7ba11f806926d641a9990abf69 Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Wed, 20 Mar 2024 13:33:32 -0500 Subject: [PATCH 2/4] Changeset --- .changeset/warm-hotels-allow.md | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .changeset/warm-hotels-allow.md diff --git a/.changeset/warm-hotels-allow.md b/.changeset/warm-hotels-allow.md new file mode 100644 index 000000000..cb20380b9 --- /dev/null +++ b/.changeset/warm-hotels-allow.md @@ -0,0 +1,59 @@ +--- +"@shopify/shopify-api": minor +--- + +Update session fromPropertyArray to handle all user info fields + +```tsconst {session, headers} = shopify.auth.callback({ + rawRequest: req, + rawResponse: res, +}); +/* + If session has the following data content... + { + id: 'online_session_id', + shop: 'online-session-shop', + state: 'online-session-state', + isOnline: true, + scope: 'online-session-scope', + accessToken: 'online-session-token', + expires: 2022-01-01T05:00:00.000Z, // Date object + onlineAccessInfo: { + expires_in: 1, + associated_user_scope: 'online-session-user-scope', + associated_user: { + id: 1, + first_name: 'online-session-first-name', + last_name: 'online-session-last-name', + email: 'online-session-email', + locale: 'online-session-locale', + email_verified: true, + account_owner: true, + collaborator: false, + }, + } + } + */ + +const sessionProperties = session.toPropertyArray(); +/* + ... then sessionProperties will have the following data... + [ + ['id', 'online_session_id'], + ['shop', 'online-session-shop'], + ['state', 'online-session-state'], + ['isOnline', true], + ['scope', 'online-session-scope'], + ['accessToken', 'online-session-token'], + ['expires', 1641013200000], // example = January 1, 2022, as number of milliseconds since Jan 1, 1970 + ['userId', 1], + ['first_name', 'online-session-first-name'], + ['last_name', 'online-session-last-name'], + ['email', 'online-session-email'], + ['locale', 'online-session-locale'], + ['email_verified', false] + ['account_owner', true,] + ['collaborator', false], + ], + */ +``` From 6d4a244f51b687ffdd54f193862638f2c6d58b38 Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Wed, 20 Mar 2024 15:12:58 -0500 Subject: [PATCH 3/4] Add new stored session user type --- packages/shopify-api/lib/auth/oauth/types.ts | 14 +++++++------- .../lib/session/__tests__/session.test.ts | 3 +-- packages/shopify-api/lib/session/session.ts | 2 +- packages/shopify-api/lib/session/types.ts | 8 ++++++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/shopify-api/lib/auth/oauth/types.ts b/packages/shopify-api/lib/auth/oauth/types.ts index 15d743f54..96cf9efba 100644 --- a/packages/shopify-api/lib/auth/oauth/types.ts +++ b/packages/shopify-api/lib/auth/oauth/types.ts @@ -45,31 +45,31 @@ export interface OnlineAccessUser { /** * The user's first name. */ - first_name?: string; + first_name: string; /** * The user's last name. */ - last_name?: string; + last_name: string; /** * The user's email address. */ - email?: string; + email: string; /** * Whether the user has verified their email address. */ - email_verified?: boolean; + email_verified: boolean; /** * Whether the user is the account owner. */ - account_owner?: boolean; + account_owner: boolean; /** * The user's locale. */ - locale?: string; + locale: string; /** * Whether the user is a collaborator. */ - collaborator?: boolean; + collaborator: boolean; } export interface OnlineAccessResponse diff --git a/packages/shopify-api/lib/session/__tests__/session.test.ts b/packages/shopify-api/lib/session/__tests__/session.test.ts index c8cb7b696..570a98423 100644 --- a/packages/shopify-api/lib/session/__tests__/session.test.ts +++ b/packages/shopify-api/lib/session/__tests__/session.test.ts @@ -178,9 +178,8 @@ interface SessionTestData { session: SessionParams; propertyArray: [string, string | number | boolean][]; } -type SessionTestDataArray = SessionTestData[]; -const testSessions: SessionTestDataArray = [ +const testSessions: SessionTestData[] = [ { session: { id: 'offline_session_id', diff --git a/packages/shopify-api/lib/session/session.ts b/packages/shopify-api/lib/session/session.ts index 6c6c879c1..0419248ef 100644 --- a/packages/shopify-api/lib/session/session.ts +++ b/packages/shopify-api/lib/session/session.ts @@ -133,7 +133,7 @@ export class Session { }), ) as any; // If the onlineAccessInfo is not present, we are using the new session info and add it to the object - if (!obj.onlineAccessInfo) { + if (!obj.onlineAccessInfo && Object.keys(associatedUserObj).length === 0) { obj.onlineAccessInfo = associatedUserObj; } Object.setPrototypeOf(obj, Session.prototype); diff --git a/packages/shopify-api/lib/session/types.ts b/packages/shopify-api/lib/session/types.ts index be3347dcd..c7e36623d 100644 --- a/packages/shopify-api/lib/session/types.ts +++ b/packages/shopify-api/lib/session/types.ts @@ -1,5 +1,5 @@ import {AdapterArgs} from '../../runtime/http'; -import {OnlineAccessInfo} from '../auth/oauth/types'; +import {OnlineAccessInfo, OnlineAccessUser} from '../auth/oauth/types'; export interface SessionParams { /** @@ -33,9 +33,13 @@ export interface SessionParams { /** * Information on the user for the session. Only present for online sessions. */ - onlineAccessInfo?: OnlineAccessInfo; + onlineAccessInfo?: OnlineAccessInfo | StoredOnlineAccessInfo; } +type StoredOnlineAccessInfo = Omit & { + associated_user: Partial; +}; + export interface JwtPayload { /** * The shop's admin domain. From 84664fe71e94487ee76b3dad6e31fbcadb6c5706 Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Thu, 21 Mar 2024 14:30:59 -0500 Subject: [PATCH 4/4] Add flag to toPropertyArray to turn on returning user data Update documenation to include new returnUserData argument Add details element to changelog remove console.log --- .changeset/warm-hotels-allow.md | 68 +++++++- .../docs/guides/session-storage.md | 19 ++- .../lib/session/__tests__/session.test.ts | 152 ++++++++++++++++-- packages/shopify-api/lib/session/session.ts | 25 +-- 4 files changed, 231 insertions(+), 33 deletions(-) diff --git a/.changeset/warm-hotels-allow.md b/.changeset/warm-hotels-allow.md index cb20380b9..a0375448c 100644 --- a/.changeset/warm-hotels-allow.md +++ b/.changeset/warm-hotels-allow.md @@ -1,13 +1,74 @@ --- "@shopify/shopify-api": minor --- +Updates the Session class to handle the associated user information on the session object. -Update session fromPropertyArray to handle all user info fields +Updates the Session `fromPropertyArray` to handle all user info fields. -```tsconst {session, headers} = shopify.auth.callback({ +
+ +```ts +const sessionProperties = session.toPropertyArray(true); +/* + if sessionProperties has the following data... + [ + ['id', 'online_session_id'], + ['shop', 'online-session-shop'], + ['state', 'online-session-state'], + ['isOnline', true], + ['scope', 'online-session-scope'], + ['accessToken', 'online-session-token'], + ['expires', 1641013200000], // example = January 1, 2022, as number of milliseconds since Jan 1, 1970 + ['userId', 1], + ['first_name', 'online-session-first-name'], + ['last_name', 'online-session-last-name'], + ['email', 'online-session-email'], + ['locale', 'online-session-locale'], + ['email_verified', false] + ['account_owner', true,] + ['collaborator', false], + ], + */ + +const session = Session.fromPropertyArray(sessionProperties, true); +/* + ... then session will have the following data... + { + id: 'online_session_id', + shop: 'online-session-shop', + state: 'online-session-state', + isOnline: true, + scope: 'online-session-scope', + accessToken: 'online-session-token', + expires: 2022-01-01T05:00:00.000Z, // Date object + onlineAccessInfo: { + associated_user: { + id: 1, + first_name: 'online-session-first-name' + last_name: 'online-session-last-name', + email: 'online-session-email', + locale: 'online-session-locale', + email_verified: false, + account_owner: true, + collaborator: false, + }, + } + } + */ +``` + +
+ +Updates the Session `toPropertyArray` to handle all user info fields. New optional argument `returnUserData`, (defaulted to `false`), will return the user data as part of property array object. This will be defaulted to `true` in an upcoming version. + +
+ +```ts +const {session, headers} = shopify.auth.callback({ rawRequest: req, rawResponse: res, }); + /* If session has the following data content... { @@ -46,7 +107,7 @@ const sessionProperties = session.toPropertyArray(); ['scope', 'online-session-scope'], ['accessToken', 'online-session-token'], ['expires', 1641013200000], // example = January 1, 2022, as number of milliseconds since Jan 1, 1970 - ['userId', 1], + ['userId', 1], // New returns the user id under the userId key instead of onlineAccessInfo ['first_name', 'online-session-first-name'], ['last_name', 'online-session-last-name'], ['email', 'online-session-email'], @@ -57,3 +118,4 @@ const sessionProperties = session.toPropertyArray(); ], */ ``` +
diff --git a/packages/shopify-api/docs/guides/session-storage.md b/packages/shopify-api/docs/guides/session-storage.md index a7d3e9ea7..2ec295b17 100644 --- a/packages/shopify-api/docs/guides/session-storage.md +++ b/packages/shopify-api/docs/guides/session-storage.md @@ -65,6 +65,8 @@ Now that the app has a JavaScript object containing the data of a `Session`, it The `Session` class also includes an instance method called `.toPropertyArray` that returns an array of key-value pairs, e.g., +`toPropertyArray` has an optional parameter `returnUserData`, defaulted to false, when set to true it will return the associated user data as part of the property array object. + ```ts const {session, headers} = shopify.auth.callback({ rawRequest: req, @@ -97,7 +99,7 @@ const {session, headers} = shopify.auth.callback({ } */ -const sessionProperties = session.toPropertyArray(); +const sessionProperties = session.toPropertyArray(true); /* ... then sessionProperties will have the following data... [ @@ -126,7 +128,7 @@ const {session, headers} = shopify.auth.callback({ rawResponse: res, }); /* - If session has the following data content... + If session has the following data content... { id: 'online_session_id', shop: 'online-session-shop', @@ -140,12 +142,19 @@ const {session, headers} = shopify.auth.callback({ associated_user_scope: 'online-session-user-scope', associated_user: { id: 1, - } + first_name: 'online-session-first-name', + last_name: 'online-session-last-name', + email: 'online-session-email', + locale: 'online-session-locale', + email_verified: true, + account_owner: true, + collaborator: false, + }, } } */ -const sessionProperties = session.toPropertyArray(); +const sessionProperties = session.toPropertyArray(false); /* ... then sessionProperties will have the following data... [ @@ -192,7 +201,7 @@ Once the `Session` is found, the app must ensure that it converts it from the st If the `.toPropertyArray` method was used to obtain the session data, the `Session` class has a `.fromPropertyArray` static method that can be used to convert the array data back into a session. ```ts -const sessionProperties = session.toPropertyArray(); +const sessionProperties = session.toPropertyArray(true); /* if sessionProperties has the following data... [ diff --git a/packages/shopify-api/lib/session/__tests__/session.test.ts b/packages/shopify-api/lib/session/__tests__/session.test.ts index 570a98423..59c224f75 100644 --- a/packages/shopify-api/lib/session/__tests__/session.test.ts +++ b/packages/shopify-api/lib/session/__tests__/session.test.ts @@ -1,6 +1,6 @@ import {Session} from '../session'; import {testConfig} from '../../__tests__/test-config'; -import {SessionParams, shopifyApi} from '../..'; +import {shopifyApi} from '../..'; describe('session', () => { it('can create a session from another session', () => { @@ -174,12 +174,7 @@ describe('isScopeChanged', () => { const expiresDate = new Date(Date.now() + 86400); const expiresNumber = expiresDate.getTime(); -interface SessionTestData { - session: SessionParams; - propertyArray: [string, string | number | boolean][]; -} - -const testSessions: SessionTestData[] = [ +const testSessions = [ { session: { id: 'offline_session_id', @@ -199,6 +194,7 @@ const testSessions: SessionTestData[] = [ ['accessToken', 'offline-session-token'], ['expires', expiresNumber], ], + returnUserData: false, }, { session: { @@ -213,6 +209,7 @@ const testSessions: SessionTestData[] = [ ['state', 'offline-session-state'], ['isOnline', false], ], + returnUserData: false, }, { session: { @@ -229,6 +226,7 @@ const testSessions: SessionTestData[] = [ ['isOnline', false], ['scope', 'offline-session-scope'], ], + returnUserData: false, }, { session: { @@ -245,6 +243,7 @@ const testSessions: SessionTestData[] = [ ['isOnline', false], ['accessToken', 'offline-session-token'], ], + returnUserData: false, }, { session: { @@ -261,9 +260,9 @@ const testSessions: SessionTestData[] = [ ['isOnline', false], ['expires', expiresNumber], ], + returnUserData: false, }, { - // Represents an online session fetched from the DB stored in the old format session: { id: 'online_session_id', shop: 'online-session-shop', @@ -277,6 +276,13 @@ const testSessions: SessionTestData[] = [ associated_user_scope: 'online-session-user-scope', associated_user: { id: 1, + first_name: 'online-session-first-name', + last_name: 'online-session-last-name', + email: 'online-session-email', + locale: 'online-session-locale', + email_verified: true, + account_owner: true, + collaborator: false, }, }, }, @@ -290,6 +296,7 @@ const testSessions: SessionTestData[] = [ ['expires', expiresNumber], ['onlineAccessInfo', 1], ], + returnUserData: false, }, { session: { @@ -302,6 +309,13 @@ const testSessions: SessionTestData[] = [ associated_user_scope: 'online-session-user-scope', associated_user: { id: 1, + first_name: 'online-session-first-name', + last_name: 'online-session-last-name', + email: 'online-session-email', + locale: 'online-session-locale', + email_verified: true, + account_owner: true, + collaborator: false, }, }, }, @@ -312,9 +326,30 @@ const testSessions: SessionTestData[] = [ ['isOnline', true], ['onlineAccessInfo', 1], ], + returnUserData: false, + }, + { + session: { + id: 'offline_session_id', + shop: 'offline-session-shop', + state: 'offline-session-state', + isOnline: false, + scope: 'offline-session-scope', + accessToken: 'offline-session-token', + expires: expiresDate, + }, + propertyArray: [ + ['id', 'offline_session_id'], + ['shop', 'offline-session-shop'], + ['state', 'offline-session-state'], + ['isOnline', false], + ['scope', 'offline-session-scope'], + ['accessToken', 'offline-session-token'], + ['expires', expiresNumber], + ], + returnUserData: true, }, { - // Represents a session stored in new format session: { id: 'online_session_id', shop: 'online-session-shop', @@ -355,8 +390,39 @@ const testSessions: SessionTestData[] = [ ['accountOwner', true], ['collaborator', false], ], + returnUserData: true, + }, + { + session: { + id: 'online_session_id', + shop: 'online-session-shop', + state: 'online-session-state', + isOnline: true, + scope: 'online-session-scope', + accessToken: 'online-session-token', + expires: expiresDate, + onlineAccessInfo: { + expires_in: 1, + associated_user_scope: 'online-session-user-scope', + associated_user: { + id: 1, + }, + }, + }, + propertyArray: [ + ['id', 'online_session_id'], + ['shop', 'online-session-shop'], + ['state', 'online-session-state'], + ['isOnline', true], + ['scope', 'online-session-scope'], + ['accessToken', 'online-session-token'], + ['expires', expiresNumber], + ['userId', 1], + ], + returnUserData: true, }, ]; + describe('toObject', () => { testSessions.forEach((test) => { const onlineOrOffline = test.session.isOnline ? 'online' : 'offline'; @@ -376,14 +442,19 @@ describe('toObject', () => { describe('toPropertyArray and fromPropertyArray', () => { testSessions.forEach((test) => { const onlineOrOffline = test.session.isOnline ? 'online' : 'offline'; - it(`returns a property array of an ${onlineOrOffline} session`, () => { + const userData = test.returnUserData ? 'with' : 'without'; + it(`returns a property array of an ${onlineOrOffline} session ${userData} user data`, () => { const session = new Session(test.session); - expect(session.toPropertyArray()).toStrictEqual(test.propertyArray); + expect(session.toPropertyArray(test.returnUserData)).toStrictEqual( + test.propertyArray, + ); }); - it(`recreates a Session from a property array of an ${onlineOrOffline} session`, () => { + it(`recreates a Session from a property array of an ${onlineOrOffline} session ${userData} user data`, () => { const session = new Session(test.session); - const sessionCopy = Session.fromPropertyArray(session.toPropertyArray()); + const sessionCopy = Session.fromPropertyArray( + session.toPropertyArray(test.returnUserData), + ); expect(session.id).toStrictEqual(sessionCopy.id); expect(session.shop).toStrictEqual(sessionCopy.shop); expect(session.state).toStrictEqual(sessionCopy.state); @@ -394,6 +465,61 @@ describe('toPropertyArray and fromPropertyArray', () => { expect(session.onlineAccessInfo?.associated_user.id).toStrictEqual( sessionCopy.onlineAccessInfo?.associated_user.id, ); + if (test.returnUserData) { + expect( + session.onlineAccessInfo?.associated_user.first_name, + ).toStrictEqual( + sessionCopy.onlineAccessInfo?.associated_user.first_name, + ); + expect( + session.onlineAccessInfo?.associated_user.last_name, + ).toStrictEqual( + sessionCopy.onlineAccessInfo?.associated_user.last_name, + ); + expect(session.onlineAccessInfo?.associated_user.email).toStrictEqual( + sessionCopy.onlineAccessInfo?.associated_user.email, + ); + expect(session.onlineAccessInfo?.associated_user.locale).toStrictEqual( + sessionCopy.onlineAccessInfo?.associated_user.locale, + ); + expect( + session.onlineAccessInfo?.associated_user.email_verified, + ).toStrictEqual( + sessionCopy.onlineAccessInfo?.associated_user.email_verified, + ); + expect( + session.onlineAccessInfo?.associated_user.account_owner, + ).toStrictEqual( + sessionCopy.onlineAccessInfo?.associated_user.account_owner, + ); + expect( + session.onlineAccessInfo?.associated_user.collaborator, + ).toStrictEqual( + sessionCopy.onlineAccessInfo?.associated_user.collaborator, + ); + } else { + expect( + sessionCopy.onlineAccessInfo?.associated_user.first_name, + ).toBeUndefined(); + expect( + sessionCopy.onlineAccessInfo?.associated_user.last_name, + ).toBeUndefined(); + expect( + sessionCopy.onlineAccessInfo?.associated_user.email, + ).toBeUndefined(); + expect( + sessionCopy.onlineAccessInfo?.associated_user.locale, + ).toBeUndefined(); + expect( + sessionCopy.onlineAccessInfo?.associated_user.email_verified, + ).toBeUndefined(); + expect( + sessionCopy.onlineAccessInfo?.associated_user.account_owner, + ).toBeUndefined(); + expect( + sessionCopy.onlineAccessInfo?.associated_user.collaborator, + ).toBeUndefined(); + } }); }); }); diff --git a/packages/shopify-api/lib/session/session.ts b/packages/shopify-api/lib/session/session.ts index 0419248ef..5ceaa8d92 100644 --- a/packages/shopify-api/lib/session/session.ts +++ b/packages/shopify-api/lib/session/session.ts @@ -46,6 +46,8 @@ export class Session { return ['accessToken', value]; case 'onlineaccessinfo': return ['onlineAccessInfo', value]; + case 'userid': + return ['userId', value]; case 'firstname': return ['firstName', value]; case 'lastname': @@ -54,8 +56,6 @@ export class Session { return ['accountOwner', value]; case 'emailverified': return ['emailVerified', value]; - case 'userid': - return ['userId', value]; default: return [key.toLowerCase(), value]; } @@ -133,7 +133,7 @@ export class Session { }), ) as any; // If the onlineAccessInfo is not present, we are using the new session info and add it to the object - if (!obj.onlineAccessInfo && Object.keys(associatedUserObj).length === 0) { + if (!obj.onlineAccessInfo && Object.keys(associatedUserObj).length !== 0) { obj.onlineAccessInfo = associatedUserObj; } Object.setPrototypeOf(obj, Session.prototype); @@ -248,10 +248,10 @@ export class Session { if (!mandatoryPropsMatch) return false; - const copyA = this.toPropertyArray(); + const copyA = this.toPropertyArray(true); copyA.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); - const copyB = other.toPropertyArray(); + const copyB = other.toPropertyArray(true); copyB.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); return JSON.stringify(copyA) === JSON.stringify(copyB); @@ -260,7 +260,9 @@ export class Session { /** * Converts the session into an array of key-value pairs. */ - public toPropertyArray(): [string, string | number | boolean][] { + public toPropertyArray( + returnUserData = false, + ): [string, string | number | boolean][] { return ( Object.entries(this) .filter( @@ -270,16 +272,13 @@ export class Session { value !== null, ) // Prepare values for db storage - .flatMap(([key, value]) => { + .flatMap(([key, value]): [string, string | number | boolean][] => { switch (key) { case 'expires': return [[key, value ? value.getTime() : undefined]]; case 'onlineAccessInfo': - if ( - value?.associated_user && - Object.keys(value.associated_user).length === 1 && - value.associated_user.id !== undefined - ) { + // eslint-disable-next-line no-negated-condition + if (!returnUserData) { return [[key, value.associated_user.id]]; } else { return [ @@ -297,6 +296,8 @@ export class Session { return [[key, value]]; } }) + // Filter out tuples with undefined values + .filter(([_key, value]) => value !== undefined) ); } }