Skip to content

Commit 6959be7

Browse files
committed
feat: add-optional-query-params-to-start
adds ability to add query parameters to the start call
1 parent 5a2313f commit 6959be7

File tree

12 files changed

+118
-17
lines changed

12 files changed

+118
-17
lines changed

.changeset/config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
}
88
],
99
"commit": false,
10-
"fixed": [],
10+
"fixed": [["@forgerock/*"]],
1111
"linked": [],
1212
"access": "public",
1313
"baseBranch": "main",
1414
"updateInternalDependencies": "patch",
1515
"ignore": [
16-
"@forgerock/*",
16+
"@forgerock/device-client",
1717
"@forgerock/davinci-app",
1818
"@forgerock/davinci-suites",
1919
"@forgerock/mock-api-v2"

.changeset/gorgeous-cats-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@forgerock/davinci-client': minor
3+
---
4+
5+
adds the ability to call start with query parameters which are appended to the /authorize call

e2e/davinci-app/main.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import './style.css';
22

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

67
import usernameComponent from './components/text.js';
78
import passwordComponent from './components/password.js';
@@ -10,9 +11,9 @@ import protect from './components/protect.js';
1011
import flowLinkComponent from './components/flow-link.js';
1112
import socialLoginButtonComponent from './components/social-login-button.js';
1213

13-
const config = {
14+
const config: DaVinciConfig = {
1415
clientId: '724ec718-c41c-4d51-98b0-84a583f450f9',
15-
redirectUri: window.location.href,
16+
redirectUri: window.location.origin + '/',
1617
scope: 'openid profile email name revoke',
1718
serverConfig: {
1819
wellknown:
@@ -178,7 +179,21 @@ const config = {
178179
console.log('Event emitted from store:', node);
179180
});
180181

181-
const node = await davinciClient.start();
182+
const qs = window.location.search;
183+
const searchParams = new URLSearchParams(qs);
184+
185+
const query: Record<string, string | string[]> = {};
186+
187+
// Get all unique keys from the searchParams
188+
const uniqueKeys = new Set(searchParams.keys());
189+
190+
// Iterate over the unique keys
191+
for (const key of uniqueKeys) {
192+
const values = searchParams.getAll(key);
193+
query[key] = values.length > 1 ? values : values[0];
194+
}
195+
console.log('query', query);
196+
const node = await davinciClient.start({ query });
182197

183198
formEl.addEventListener('submit', async (event) => {
184199
event.preventDefault();

e2e/davinci-app/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"@forgerock/davinci-client": "workspace:*",
88
"@forgerock/javascript-sdk": "4.6.0"
99
},
10-
"devDependencies": {},
1110
"version": "0.0.0",
1211
"scripts": {
1312
"build": "nx exec -- vite build --watch false",

e2e/davinci-app/tsconfig.app.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"outDir": "../../dist/out-tsc",
55
"moduleResolution": "Bundler"
66
},
7-
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
8-
"include": ["src/**/*.ts"]
7+
"exclude": ["**/*.spec.ts", "**/*.test.ts"],
8+
"include": ["./main.ts", "components/**/*.ts"]
99
}

e2e/davinci-app/tsconfig.spec.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"extends": "./tsconfig.json",
33
"compilerOptions": {
4+
"moduleResolution": "NodeNext",
45
"composite": true,
56
"outDir": "../../dist/out-tsc",
67
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]

e2e/davinci-suites/src/basic.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,45 @@ test('Test happy paths on test page', async ({ page }) => {
2626
const accessToken = await page.locator('#accessTokenValue').innerText();
2727
await expect(accessToken).toBeTruthy();
2828
});
29+
test('ensure query params passed to start are sent off in authorize call', async ({ page }) => {
30+
const { navigate } = asyncEvents(page);
31+
// Wait for the request to a URL containing '/authorize'
32+
const requestPromise = page.waitForRequest((request) => {
33+
return request
34+
.url()
35+
.includes('https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize');
36+
});
37+
await navigate('/?testParam=123');
38+
39+
// Wait for the request to be made to authorize
40+
const request = await requestPromise;
41+
42+
// Extract and verify the query parameters from authorize
43+
const url = new URL(request.url());
44+
const queryParams = Object.fromEntries(url.searchParams.entries());
45+
46+
expect(queryParams['testParam']).toBe('123');
47+
expect(queryParams['client_id']).toBe('724ec718-c41c-4d51-98b0-84a583f450f9');
48+
expect(queryParams['response_mode']).toBe('pi.flow');
49+
50+
expect(page.url()).toBe('http://localhost:5829/?testParam=123');
51+
52+
await expect(page.getByText('Username/Password Form')).toBeVisible();
53+
54+
await page.getByLabel('Username').fill('demouser');
55+
await page.getByLabel('Password').fill('U.CDmhGLK*nrQPDWEN47ZMyJh');
56+
57+
await page.getByText('Sign On').click();
58+
59+
await expect(page.getByText('Complete')).toBeVisible();
60+
61+
const sessionToken = await page.locator('#sessionToken').innerText();
62+
const authCode = await page.locator('#authCode').innerText();
63+
expect(sessionToken).toBeTruthy();
64+
expect(authCode).toBeTruthy();
65+
66+
await page.getByText('Get Tokens').click();
67+
68+
const accessToken = await page.locator('#accessTokenValue').innerText();
69+
expect(accessToken).toBeTruthy();
70+
});

packages/davinci-client/src/lib/client.store.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import { wellknownApi } from './wellknown.api.js';
1111
* Import the DaVinciRequest types
1212
*/
1313
import type { DaVinciConfig } from './config.types.js';
14-
import type { DaVinciAction, DaVinciRequest } from './davinci.types.js';
14+
import type {
15+
DaVinciAction,
16+
DaVinciRequest,
17+
OutgoingQueryParams,
18+
StartOptions,
19+
} from './davinci.types.js';
1520
import type { SingleValueCollectors } from './collector.types.js';
1621
import type { InitFlow, Updater } from './client.types.js';
1722

@@ -91,8 +96,10 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
9196
* @method start - Method for initiating a DaVinci flow
9297
* @returns {Promise} - a promise that initiates a DaVinci flow and returns a node
9398
*/
94-
start: async () => {
95-
await store.dispatch(davinciApi.endpoints.start.initiate());
99+
start: async <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(
100+
options?: StartOptions<QueryParams> | undefined,
101+
) => {
102+
await store.dispatch(davinciApi.endpoints.start.initiate(options));
96103
return store.getState().node;
97104
},
98105

packages/davinci-client/src/lib/davinci.api.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import { handleResponse, transformActionRequest, transformSubmitRequest } from '
1414
* Import the DaVinci types
1515
*/
1616
import type { RootStateWithNode } from './client.store.utils.js';
17-
import type { DaVinciCacheEntry, ThrownQueryError } from './davinci.types';
17+
import type {
18+
DaVinciCacheEntry,
19+
OutgoingQueryParams,
20+
StartOptions,
21+
ThrownQueryError,
22+
} from './davinci.types';
1823
import type { ContinueNode } from './node.types.js';
1924
import type { StartNode } from '../types.js';
2025

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

196201
if (!state) {
@@ -209,16 +214,31 @@ export const davinciApi = createApi({
209214
}
210215

211216
try {
217+
console.log('redirect uri', state.config.redirectUri);
212218
const authorizeUrl = await createAuthorizeUrl(authorizeEndpoint, {
213219
clientId: state?.config?.clientId,
214220
login: 'redirect', // TODO: improve this in SDK to be more semantic
215221
redirectUri: state?.config?.redirectUri,
216222
responseType: state?.config?.responseType,
217223
scope: state?.config?.scope,
218224
});
225+
const url = new URL(authorizeUrl);
226+
const existingParams = url.searchParams;
227+
228+
if (options?.query) {
229+
Object.entries(options.query).forEach(([key, value]) => {
230+
/**
231+
* We use set here because if we have existing params, we want
232+
* to make sure we override them and not add duplicates
233+
*/
234+
existingParams.set(key, String(value));
235+
});
236+
237+
url.search = existingParams.toString();
238+
}
219239

220240
const response = await baseQuery({
221-
url: authorizeUrl,
241+
url: url.toString(),
222242
credentials: 'include',
223243
method: 'GET',
224244
headers: {
@@ -268,7 +288,6 @@ export const davinciApi = createApi({
268288
}
269289

270290
const cacheEntry: DaVinciCacheEntry = api.getCacheEntry();
271-
272291
handleResponse(cacheEntry, api.dispatch, response?.status || 0);
273292
},
274293
}),

packages/davinci-client/src/lib/davinci.types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,11 @@ export interface ThrownQueryError {
221221
isHandledError: boolean;
222222
meta: FetchBaseQueryMeta;
223223
}
224+
225+
export interface StartOptions<Query extends OutgoingQueryParams = OutgoingQueryParams> {
226+
query: Query;
227+
}
228+
// Outgoing query parameters (sent in the request)
229+
export interface OutgoingQueryParams {
230+
[key: string]: string | string[];
231+
}

packages/davinci-client/src/lib/davinci.utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
DaVinciNextResponse,
1313
DaVinciRequest,
1414
DaVinciSuccessResponse,
15+
StartOptions,
1516
} from './davinci.types';
1617
import type { ContinueNode } from './node.types';
1718

packages/davinci-client/src/lib/node.slice.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ export const nodeSlice = createSlice({
165165
*/
166166
next(
167167
state,
168-
action: PayloadAction<{ data: DaVinciNextResponse; requestId: string; httpStatus: number }>,
168+
action: PayloadAction<{
169+
data: DaVinciNextResponse;
170+
requestId: string;
171+
httpStatus: number;
172+
}>,
169173
) {
170174
const newState = state as Draft<ContinueNode>;
171175

0 commit comments

Comments
 (0)