Skip to content

Commit f587f74

Browse files
committed
Add relative path prefix support to routeFor utilities
Introduce a routePrefix store that allows consuming projects to inject a path prefix (e.g., /projects/{projectId}) into all UI route functions. The prefix is inserted between the SvelteKit base path and the route path via a withPrefix helper that wraps the 12 root resolve() calls. Leaf functions inherit the prefix automatically through the existing string concatenation chain. Auth routes are excluded to avoid breaking SSO flows.
1 parent 3e6416d commit f587f74

4 files changed

Lines changed: 300 additions & 28 deletions

File tree

src/lib/stores/route-prefix.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { get, writable } from 'svelte/store';
2+
3+
export const routePrefix = writable<string>('');
4+
5+
export const getRoutePrefix = (): string => get(routePrefix);

src/lib/utilities/route-for-base-path.test.ts

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { afterEach, describe, expect, it } from 'vitest';
22

33
import { base } from '$app/paths';
44

5+
import { routePrefix } from '$lib/stores/route-prefix';
6+
57
import * as routeForModule from './route-for';
68
import {
79
routeForArchivalEventHistory,
@@ -217,3 +219,173 @@ describe('routeFor functions should resolve the base path exactly once', () => {
217219
}
218220
});
219221
});
222+
223+
describe('routeFor functions with prefix should resolve base + prefix correctly', () => {
224+
const prefix = '/projects/my-project';
225+
226+
const namespaceParams = { namespace: 'default' };
227+
const workflowParams = {
228+
namespace: 'default',
229+
workflow: 'wf-id',
230+
run: 'run-id',
231+
};
232+
const scheduleParams = { namespace: 'default', scheduleId: 'sched-1' };
233+
const activityParams = {
234+
namespace: 'default',
235+
activityId: 'act-1',
236+
runId: 'run-1',
237+
};
238+
239+
afterEach(() => {
240+
routePrefix.set('');
241+
});
242+
243+
const prefixedCases: [string, () => string | undefined][] = [
244+
['routeForNamespaces', () => routeForNamespaces()],
245+
['routeForNexus', () => routeForNexus()],
246+
['routeForNexusEndpoint', () => routeForNexusEndpoint('ep-1')],
247+
['routeForNexusEndpointEdit', () => routeForNexusEndpointEdit('ep-1')],
248+
['routeForNexusEndpointCreate', () => routeForNexusEndpointCreate()],
249+
['routeForNamespace', () => routeForNamespace(namespaceParams)],
250+
['routeForNamespaceSelector', () => routeForNamespaceSelector()],
251+
['routeForWorkflows', () => routeForWorkflows(namespaceParams)],
252+
[
253+
'routeForArchivalWorkflows',
254+
() => routeForArchivalWorkflows(namespaceParams),
255+
],
256+
['routeForWorkflow', () => routeForWorkflow(workflowParams)],
257+
['routeForSchedules', () => routeForSchedules(namespaceParams)],
258+
['routeForScheduleCreate', () => routeForScheduleCreate(namespaceParams)],
259+
['routeForSchedule', () => routeForSchedule(scheduleParams)],
260+
['routeForScheduleEdit', () => routeForScheduleEdit(scheduleParams)],
261+
[
262+
'routeForArchivalEventHistory',
263+
() => routeForArchivalEventHistory(workflowParams),
264+
],
265+
[
266+
'routeForEventHistoryEvent',
267+
() => routeForEventHistoryEvent({ ...workflowParams, eventId: '1' }),
268+
],
269+
['routeForEventHistory', () => routeForEventHistory(workflowParams)],
270+
['routeForTimeline', () => routeForTimeline(workflowParams)],
271+
['routeForWorkers', () => routeForWorkers(workflowParams)],
272+
[
273+
'routeForWorkerDeployments',
274+
() => routeForWorkerDeployments(namespaceParams),
275+
],
276+
[
277+
'routeForWorkerDeployment',
278+
() =>
279+
routeForWorkerDeployment({
280+
namespace: 'default',
281+
deployment: 'dep-1',
282+
}),
283+
],
284+
[
285+
'routeForWorkerDeploymentVersion',
286+
() =>
287+
routeForWorkerDeploymentVersion({
288+
namespace: 'default',
289+
deployment: 'dep-1',
290+
version: 'v1',
291+
}),
292+
],
293+
['routeForRelationships', () => routeForRelationships(workflowParams)],
294+
[
295+
'routeForTaskQueue',
296+
() => routeForTaskQueue({ namespace: 'default', queue: 'q-1' }),
297+
],
298+
['routeForCallStack', () => routeForCallStack(workflowParams)],
299+
['routeForWorkflowQuery', () => routeForWorkflowQuery(workflowParams)],
300+
['routeForUserMetadata', () => routeForUserMetadata(workflowParams)],
301+
[
302+
'routeForWorkflowSearchAttributes',
303+
() => routeForWorkflowSearchAttributes(workflowParams),
304+
],
305+
['routeForWorkflowMemo', () => routeForWorkflowMemo(workflowParams)],
306+
['routeForWorkflowUpdate', () => routeForWorkflowUpdate(workflowParams)],
307+
[
308+
'routeForPendingActivities',
309+
() => routeForPendingActivities(workflowParams),
310+
],
311+
['routeForNexusLinks', () => routeForNexusLinks(workflowParams)],
312+
['routeForEventHistoryImport', () => routeForEventHistoryImport()],
313+
['routeForBatchOperations', () => routeForBatchOperations(namespaceParams)],
314+
[
315+
'routeForBatchOperation',
316+
() => routeForBatchOperation({ namespace: 'default', jobId: 'job-1' }),
317+
],
318+
[
319+
'routeForStandaloneActivities',
320+
() => routeForStandaloneActivities(namespaceParams),
321+
],
322+
[
323+
'routeForStandaloneActivitiesWithQuery',
324+
() =>
325+
routeForStandaloneActivitiesWithQuery(namespaceParams, 'test-query'),
326+
],
327+
[
328+
'routeForStartStandaloneActivity',
329+
() => routeForStartStandaloneActivity(namespaceParams),
330+
],
331+
[
332+
'routeForStandaloneActivityDetails',
333+
() => routeForStandaloneActivityDetails(activityParams),
334+
],
335+
[
336+
'routeForStandaloneActivityWorkers',
337+
() => routeForStandaloneActivityWorkers(activityParams),
338+
],
339+
[
340+
'routeForStandaloneActivitySearchAttributes',
341+
() => routeForStandaloneActivitySearchAttributes(activityParams),
342+
],
343+
[
344+
'routeForStandaloneActivityMetadata',
345+
() => routeForStandaloneActivityMetadata(activityParams),
346+
],
347+
[
348+
'routeForWorkflowStart',
349+
() => routeForWorkflowStart({ namespace: 'default' }),
350+
],
351+
[
352+
'routeForWorkflowsWithQuery',
353+
() => routeForWorkflowsWithQuery({ namespace: 'default', query: 'test' }),
354+
],
355+
];
356+
357+
const authCases: [string, () => string | undefined][] = [
358+
[
359+
'routeForAuthentication',
360+
() =>
361+
routeForAuthentication({
362+
settings: { auth: {}, baseUrl: 'https://example.com' },
363+
searchParams: new URLSearchParams(),
364+
}),
365+
],
366+
['routeForLoginPage', () => routeForLoginPage('', false)],
367+
];
368+
369+
it.each(prefixedCases)(
370+
'%s should include base + prefix when prefix is set',
371+
(_name, fn) => {
372+
routePrefix.set(prefix);
373+
const result = fn();
374+
expect(typeof result).toBe('string');
375+
expect(result).toMatch(new RegExp(`^${base}${prefix}`));
376+
expect(result).not.toMatch(
377+
new RegExp(`${base}${prefix}${base}${prefix}`),
378+
);
379+
},
380+
);
381+
382+
it.each(authCases)(
383+
'%s should NOT include prefix (auth routes excluded)',
384+
(_name, fn) => {
385+
routePrefix.set(prefix);
386+
const result = fn();
387+
expect(typeof result).toBe('string');
388+
expect(result).not.toContain(prefix);
389+
},
390+
);
391+
});

src/lib/utilities/route-for.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
EventSortOrder,
99
WorkflowViewPreference,
1010
} from '$lib/stores/event-view';
11+
import { routePrefix } from '$lib/stores/route-prefix';
1112

1213
import {
1314
baseRouteForWorkflow,
@@ -24,6 +25,7 @@ import {
2425
routeForLoginPage,
2526
routeForNamespace,
2627
routeForNamespaces,
28+
routeForNexus,
2729
routeForPendingActivities,
2830
routeForSchedule,
2931
routeForScheduleCreate,
@@ -530,3 +532,79 @@ describe('routeForWorkflow', () => {
530532
expect(path).not.toContain('sort=descending');
531533
});
532534
});
535+
536+
describe('routeFor with prefix', () => {
537+
const prefix = '/projects/my-project';
538+
539+
afterEach(() => {
540+
routePrefix.set('');
541+
});
542+
543+
it('should prepend prefix to root route', () => {
544+
routePrefix.set(prefix);
545+
expect(routeForNamespaces()).toBe(`${base}${prefix}/namespaces`);
546+
});
547+
548+
it('should prepend prefix to namespace route', () => {
549+
routePrefix.set(prefix);
550+
expect(routeForNamespace({ namespace: 'default' })).toBe(
551+
`${base}${prefix}/namespaces/default`,
552+
);
553+
});
554+
555+
it('should propagate prefix through leaf functions', () => {
556+
routePrefix.set(prefix);
557+
expect(routeForWorkflows({ namespace: 'default' })).toBe(
558+
`${base}${prefix}/namespaces/default/workflows`,
559+
);
560+
});
561+
562+
it('should propagate prefix through deep leaf functions', () => {
563+
routePrefix.set(prefix);
564+
expect(
565+
routeForCallStack({
566+
namespace: 'default',
567+
workflow: 'abc',
568+
run: 'def',
569+
}),
570+
).toBe(`${base}${prefix}/namespaces/default/workflows/abc/def/call-stack`);
571+
});
572+
573+
it('should propagate prefix to nexus routes', () => {
574+
routePrefix.set(prefix);
575+
expect(routeForNexus()).toBe(`${base}${prefix}/nexus`);
576+
});
577+
578+
it('should propagate prefix to schedule routes', () => {
579+
routePrefix.set(prefix);
580+
expect(routeForSchedules({ namespace: 'default' })).toBe(
581+
`${base}${prefix}/namespaces/default/schedules`,
582+
);
583+
});
584+
585+
it('should not apply prefix when store is empty', () => {
586+
routePrefix.set('');
587+
expect(routeForNamespaces()).toBe(`${base}/namespaces`);
588+
});
589+
590+
it('should not apply prefix to auth routes', () => {
591+
routePrefix.set(prefix);
592+
const settings = { auth: {}, baseUrl: 'https://localhost' };
593+
const searchParams = new URLSearchParams();
594+
const sso = routeForAuthentication({ settings, searchParams });
595+
expect(sso).not.toContain(prefix);
596+
});
597+
598+
it('should not apply prefix to login page', () => {
599+
routePrefix.set(prefix);
600+
const login = routeForLoginPage('', false);
601+
expect(login).not.toContain(prefix);
602+
});
603+
604+
it('should revert to default behavior when prefix is cleared', () => {
605+
routePrefix.set(prefix);
606+
expect(routeForNamespaces()).toBe(`${base}${prefix}/namespaces`);
607+
routePrefix.set('');
608+
expect(routeForNamespaces()).toBe(`${base}/namespaces`);
609+
});
610+
});

0 commit comments

Comments
 (0)