Skip to content

Commit

Permalink
feat: add-optional-query-params-to-start
Browse files Browse the repository at this point in the history
adds ability to add query parameters to the start call
  • Loading branch information
ryanbas21 committed Jan 30, 2025
1 parent ee8c7aa commit 6ce29c4
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-cats-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

adds the ability to call start with query parameters which are appended to the /authorize call
21 changes: 18 additions & 3 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './style.css';

import { Config, FRUser, TokenManager } from '@forgerock/javascript-sdk';
import { davinci } from '@forgerock/davinci-client';
import type { DaVinciConfig } from '@forgerock/davinci-client/types';

import usernameComponent from './components/text.js';
import passwordComponent from './components/password.js';
Expand All @@ -10,9 +11,9 @@ import protect from './components/protect.js';
import flowLinkComponent from './components/flow-link.js';
import socialLoginButtonComponent from './components/social-login-button.js';

const config = {
const config: DaVinciConfig = {
clientId: '724ec718-c41c-4d51-98b0-84a583f450f9',
redirectUri: window.location.href,
redirectUri: window.location.origin + '/',
scope: 'openid profile email name revoke',
serverConfig: {
wellknown:
Expand Down Expand Up @@ -178,7 +179,21 @@ const config = {
console.log('Event emitted from store:', node);
});

const node = await davinciClient.start();
const qs = window.location.search;
const searchParams = new URLSearchParams(qs);

const query: Record<string, string | string[]> = {};

// Get all unique keys from the searchParams
const uniqueKeys = new Set(searchParams.keys());

// Iterate over the unique keys
for (const key of uniqueKeys) {
const values = searchParams.getAll(key);
query[key] = values.length > 1 ? values : values[0];
}
console.log('query', query);
const node = await davinciClient.start({ query });

formEl.addEventListener('submit', async (event) => {
event.preventDefault();
Expand Down
1 change: 1 addition & 0 deletions e2e/davinci-app/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"moduleResolution": "NodeNext",
"composite": true,
"outDir": "../../dist/out-tsc",
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]
Expand Down
42 changes: 42 additions & 0 deletions e2e/davinci-suites/src/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,45 @@ test('Test happy paths on test page', async ({ page }) => {
const accessToken = await page.locator('#accessTokenValue').innerText();
await expect(accessToken).toBeTruthy();
});
test('ensure query params passed to start are sent off in authorize call', async ({ page }) => {
const { navigate } = asyncEvents(page);
// Wait for the request to a URL containing '/authorize'
const requestPromise = page.waitForRequest((request) => {
return request
.url()
.includes('https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize');
});
await navigate('/?testParam=123');

// Wait for the request to be made to authorize
const request = await requestPromise;

// Extract and verify the query parameters from authorize
const url = new URL(request.url());
const queryParams = Object.fromEntries(url.searchParams.entries());

expect(queryParams['testParam']).toBe('123');
expect(queryParams['client_id']).toBe('724ec718-c41c-4d51-98b0-84a583f450f9');
expect(queryParams['response_mode']).toBe('pi.flow');

expect(page.url()).toBe('http://localhost:5829/?testParam=123');

await expect(page.getByText('Username/Password Form')).toBeVisible();

await page.getByLabel('Username').fill('demouser');
await page.getByLabel('Password').fill('U.CDmhGLK*nrQPDWEN47ZMyJh');

await page.getByText('Sign On').click();

await expect(page.getByText('Complete')).toBeVisible();

const sessionToken = await page.locator('#sessionToken').innerText();
const authCode = await page.locator('#authCode').innerText();
expect(sessionToken).toBeTruthy();
expect(authCode).toBeTruthy();

await page.getByText('Get Tokens').click();

const accessToken = await page.locator('#accessTokenValue').innerText();
expect(accessToken).toBeTruthy();
});
13 changes: 10 additions & 3 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { wellknownApi } from './wellknown.api.js';
* Import the DaVinciRequest types
*/
import type { DaVinciConfig } from './config.types.js';
import type { DaVinciAction, DaVinciRequest } from './davinci.types.js';
import type {
DaVinciAction,
DaVinciRequest,
OutgoingQueryParams,
StartOptions,
} from './davinci.types.js';
import type { SingleValueCollectors } from './collector.types.js';
import type { InitFlow, Updater } from './client.types.js';

Expand Down Expand Up @@ -94,8 +99,10 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
* @method start - Method for initiating a DaVinci flow
* @returns {Promise} - a promise that initiates a DaVinci flow and returns a node
*/
start: async () => {
await store.dispatch(davinciApi.endpoints.start.initiate());
start: async <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(
options?: StartOptions<QueryParams> | undefined,
) => {
await store.dispatch(davinciApi.endpoints.start.initiate(options));
return store.getState().node;
},

Expand Down
28 changes: 23 additions & 5 deletions packages/davinci-client/src/lib/davinci.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { handleResponse, transformActionRequest, transformSubmitRequest } from '
* Import the DaVinci types
*/
import type { RootStateWithNode } from './client.store.utils.js';
import type { DaVinciCacheEntry, ThrownQueryError } from './davinci.types.js';
import type {
DaVinciCacheEntry,
OutgoingQueryParams,
StartOptions,
ThrownQueryError,
} from './davinci.types';
import type { ContinueNode } from './node.types.js';
import type { StartNode } from '../types.js';

Expand Down Expand Up @@ -186,11 +191,11 @@ export const davinciApi = createApi({
* @method start - method for initiating a DaVinci flow
* @param - needs no arguments, but need to declare types to make it explicit
*/
start: builder.mutation<unknown, void>({
start: builder.mutation<unknown, StartOptions<OutgoingQueryParams> | undefined>({
/**
* @method queryFn - This is just a wrapper around the fetch call
*/
async queryFn(_, api, __, baseQuery) {
async queryFn(options, api, __, baseQuery) {
const state = api.getState() as RootStateWithNode<StartNode>;

if (!state) {
Expand All @@ -216,9 +221,23 @@ export const davinciApi = createApi({
responseType: state?.config?.responseType,
scope: state?.config?.scope,
});
const url = new URL(authorizeUrl);
const existingParams = url.searchParams;

if (options?.query) {
Object.entries(options.query).forEach(([key, value]) => {
/**
* We use set here because if we have existing params, we want
* to make sure we override them and not add duplicates
*/
existingParams.set(key, String(value));
});

url.search = existingParams.toString();
}

const response = await baseQuery({
url: authorizeUrl,
url: url.toString(),
credentials: 'include',
method: 'GET',
headers: {
Expand Down Expand Up @@ -268,7 +287,6 @@ export const davinciApi = createApi({
}

const cacheEntry: DaVinciCacheEntry = api.getCacheEntry();

handleResponse(cacheEntry, api.dispatch, response?.status || 0);
},
}),
Expand Down
8 changes: 8 additions & 0 deletions packages/davinci-client/src/lib/davinci.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,11 @@ export interface ThrownQueryError {
isHandledError: boolean;
meta: FetchBaseQueryMeta;
}

export interface StartOptions<Query extends OutgoingQueryParams = OutgoingQueryParams> {
query: Query;
}
// Outgoing query parameters (sent in the request)
export interface OutgoingQueryParams {
[key: string]: string | string[];
}
1 change: 1 addition & 0 deletions packages/davinci-client/src/lib/davinci.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
DaVinciNextResponse,
DaVinciRequest,
DaVinciSuccessResponse,
StartOptions,
} from './davinci.types';
import type { ContinueNode } from './node.types';

Expand Down
6 changes: 5 additions & 1 deletion packages/davinci-client/src/lib/node.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ export const nodeSlice = createSlice({
*/
next(
state,
action: PayloadAction<{ data: DaVinciNextResponse; requestId: string; httpStatus: number }>,
action: PayloadAction<{
data: DaVinciNextResponse;
requestId: string;
httpStatus: number;
}>,
) {
const newState = state as Draft<ContinueNode>;

Expand Down

0 comments on commit 6ce29c4

Please sign in to comment.