Skip to content

Commit 3e5d66d

Browse files
committed
feat(davinci-client): add RichContent types to ReadOnlyField
Support RichContent link types by creating a NoValueCollector for it
1 parent 037b012 commit 3e5d66d

10 files changed

Lines changed: 1630 additions & 10 deletions

File tree

.changeset/rich-content-links.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@forgerock/davinci-client': minor
3+
---
4+
5+
**Breaking change**: `ReadOnlyCollector.output.content` now returns a plain `string` (the label text) instead of `ContentPart[]`.
6+
7+
A new `ReadOnlyCollector.output.richContent` property is always present and contains the structured link data when a LABEL field includes `richContent`. Its shape is `CollectorRichContent` — a template string with `{{key}}` placeholders (`content`) and a validated `replacements` array (`ValidatedReplacement[]`). When no `richContent` is present, `replacements` is an empty array.
8+
9+
**Removed type exports**: `ContentPart`, `TextContentPart`, `LinkContentPart`
10+
11+
**New type exports**: `RichContentLink`, `ValidatedReplacement`, `CollectorRichContent`
12+
13+
Includes href protocol validation that rejects unsafe URI schemes (e.g. `javascript:`, `data:`).
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import { describe, expectTypeOf, it } from 'vitest';
8+
import type {
9+
ReadOnlyCollectorBase,
10+
ReadOnlyCollector,
11+
RichContentLink,
12+
ValidatedReplacement,
13+
CollectorRichContent,
14+
ValidateReplacementsResult,
15+
NoValueCollector,
16+
} from './collector.types.js';
17+
18+
describe('Rich Content Types', () => {
19+
describe('RichContentLink', () => {
20+
it('should require key, type, value, and href', () => {
21+
expectTypeOf<RichContentLink>().toHaveProperty('key').toBeString();
22+
expectTypeOf<RichContentLink>().toHaveProperty('type').toEqualTypeOf<'link'>();
23+
expectTypeOf<RichContentLink>().toHaveProperty('value').toBeString();
24+
expectTypeOf<RichContentLink>().toHaveProperty('href').toBeString();
25+
});
26+
27+
it('should have optional target constrained to _self or _blank', () => {
28+
expectTypeOf<RichContentLink>()
29+
.toHaveProperty('target')
30+
.toEqualTypeOf<'_self' | '_blank' | undefined>();
31+
});
32+
});
33+
34+
describe('ValidatedReplacement', () => {
35+
it('should be assignable from RichContentLink', () => {
36+
expectTypeOf<RichContentLink>().toMatchTypeOf<ValidatedReplacement>();
37+
});
38+
39+
it('should be assignable to RichContentLink', () => {
40+
expectTypeOf<ValidatedReplacement>().toMatchTypeOf<RichContentLink>();
41+
});
42+
});
43+
44+
describe('CollectorRichContent', () => {
45+
it('should have required content string and replacements array', () => {
46+
expectTypeOf<CollectorRichContent>().toHaveProperty('content').toBeString();
47+
expectTypeOf<CollectorRichContent>()
48+
.toHaveProperty('replacements')
49+
.toEqualTypeOf<ValidatedReplacement[]>();
50+
});
51+
});
52+
53+
describe('ValidateReplacementsResult', () => {
54+
it('should narrow to replacements on ok: true', () => {
55+
const result = {} as ValidateReplacementsResult;
56+
if (result.ok) {
57+
expectTypeOf(result.replacements).toEqualTypeOf<ValidatedReplacement[]>();
58+
}
59+
});
60+
61+
it('should narrow to error on ok: false', () => {
62+
const result = {} as ValidateReplacementsResult;
63+
if (!result.ok) {
64+
expectTypeOf(result.error).toBeString();
65+
}
66+
});
67+
});
68+
69+
describe('ReadOnlyCollectorBase', () => {
70+
it('should have content as string, not array', () => {
71+
expectTypeOf<ReadOnlyCollectorBase['output']['content']>().toBeString();
72+
});
73+
74+
it('should have required richContent with CollectorRichContent shape', () => {
75+
expectTypeOf<ReadOnlyCollectorBase['output']['richContent']>().toEqualTypeOf<CollectorRichContent>();
76+
});
77+
78+
it('should have standard collector fields', () => {
79+
expectTypeOf<ReadOnlyCollectorBase>().toHaveProperty('category').toEqualTypeOf<'NoValueCollector'>();
80+
expectTypeOf<ReadOnlyCollectorBase>().toHaveProperty('type').toEqualTypeOf<'ReadOnlyCollector'>();
81+
expectTypeOf<ReadOnlyCollectorBase>().toHaveProperty('error').toEqualTypeOf<string | null>();
82+
});
83+
});
84+
85+
describe('NoValueCollector<ReadOnlyCollector>', () => {
86+
it('should resolve to ReadOnlyCollectorBase', () => {
87+
expectTypeOf<NoValueCollector<'ReadOnlyCollector'>>().toEqualTypeOf<ReadOnlyCollectorBase>();
88+
});
89+
90+
it('should have content and richContent on output', () => {
91+
type Resolved = NoValueCollector<'ReadOnlyCollector'>;
92+
expectTypeOf<Resolved['output']['content']>().toBeString();
93+
expectTypeOf<Resolved['output']['richContent']>().toEqualTypeOf<CollectorRichContent>();
94+
});
95+
});
96+
97+
describe('ReadOnlyCollector alias', () => {
98+
it('should equal ReadOnlyCollectorBase', () => {
99+
expectTypeOf<ReadOnlyCollector>().toEqualTypeOf<ReadOnlyCollectorBase>();
100+
});
101+
});
102+
});

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,25 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
497497
};
498498
}
499499

500+
export interface RichContentLink {
501+
key: string;
502+
type: 'link';
503+
value: string;
504+
href: string;
505+
target?: '_self' | '_blank';
506+
}
507+
508+
export type ValidatedReplacement = RichContentLink;
509+
510+
export interface CollectorRichContent {
511+
content: string;
512+
replacements: ValidatedReplacement[];
513+
}
514+
515+
export type ValidateReplacementsResult =
516+
| { ok: true; replacements: ValidatedReplacement[] }
517+
| { ok: false; error: string };
518+
500519
export interface QrCodeCollectorBase {
501520
category: 'NoValueCollector';
502521
error: string | null;
@@ -511,6 +530,21 @@ export interface QrCodeCollectorBase {
511530
};
512531
}
513532

533+
export interface ReadOnlyCollectorBase {
534+
category: 'NoValueCollector';
535+
error: string | null;
536+
type: 'ReadOnlyCollector';
537+
id: string;
538+
name: string;
539+
output: {
540+
key: string;
541+
label: string;
542+
type: string;
543+
content: string;
544+
richContent: CollectorRichContent;
545+
};
546+
}
547+
514548
/**
515549
* Type to help infer the collector based on the collector type
516550
* Used specifically in the returnNoValueCollector wrapper function.
@@ -520,19 +554,19 @@ export interface QrCodeCollectorBase {
520554
*/
521555
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> =
522556
T extends 'ReadOnlyCollector'
523-
? NoValueCollectorBase<'ReadOnlyCollector'>
557+
? ReadOnlyCollectorBase
524558
: T extends 'QrCodeCollector'
525559
? QrCodeCollectorBase
526560
: NoValueCollectorBase<'NoValueCollector'>;
527561

528562
export type NoValueCollectors =
529563
| NoValueCollectorBase<'NoValueCollector'>
530-
| NoValueCollectorBase<'ReadOnlyCollector'>
564+
| ReadOnlyCollectorBase
531565
| QrCodeCollectorBase;
532566

533-
export type NoValueCollector<T extends NoValueCollectorTypes> = NoValueCollectorBase<T>;
567+
export type NoValueCollector<T extends NoValueCollectorTypes> = InferNoValueCollectorType<T>;
534568

535-
export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>;
569+
export type ReadOnlyCollector = ReadOnlyCollectorBase;
536570

537571
export type QrCodeCollector = QrCodeCollectorBase;
538572

0 commit comments

Comments
 (0)