@equinor/fusion-framework-react-components-people-provider 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ import { type PropsWithChildren } from 'react';
2
+ import { PersonProviderElement, type PersonResolver } from '@equinor/fusion-wc-person';
3
+ export { PersonResolver } from '@equinor/fusion-wc-person';
4
+ declare module 'react' {
5
+ namespace JSX {
6
+ interface IntrinsicElements {
7
+ 'fwc-person-provider': React.DetailedHTMLProps<React.HTMLAttributes<PersonProviderElement>, PersonProviderElement>;
8
+ }
9
+ }
10
+ }
11
+ /**
12
+ * Wraps the `fwc-person-provider` web component, wiring the given `PersonResolver` into it
13
+ * once the underlying custom element is ready.
14
+ *
15
+ * @param props - Component props
16
+ * @param props.resolver - The resolver used to fetch person data, photos, and search results
17
+ * @param props.children - Elements rendered inside the person provider, typically person-related components
18
+ * @returns The rendered `fwc-person-provider` element wrapping the given children
19
+ */
20
+ export declare const PeopleResolverComponent: (props: PropsWithChildren<{
21
+ resolver: PersonResolver;
22
+ }>) => import("react").JSX.Element;
@@ -4,5 +4,16 @@ type PeopleResolverProviderProps = PropsWithChildren<{
4
4
  readonly options?: PersonControllerOptions;
5
5
  readonly fallback?: ReactNode;
6
6
  }>;
7
- export declare const PeopleResolverProvider: (props: PeopleResolverProviderProps) => import("react/jsx-runtime").JSX.Element;
7
+ /**
8
+ * Provides person resolution to its children by wiring up a `PersonResolver` built from the
9
+ * framework's `services` module.
10
+ *
11
+ * @param props - Component props
12
+ * @param props.children - Elements that will have access to the person resolver
13
+ * @param props.options - Optional controller options, such as a fallback image for missing photos
14
+ * @param props.fallback - Optional fallback rendered while the resolver component is suspended
15
+ * @returns The rendered people resolver provider wrapping the given children
16
+ * @throws Error if the `services` module has not been registered on the framework
17
+ */
18
+ export declare const PeopleResolverProvider: (props: PeopleResolverProviderProps) => import("react").JSX.Element;
8
19
  export default PeopleResolverProvider;
@@ -41,13 +41,25 @@ export interface IPersonController {
41
41
  export type PersonControllerOptions = {
42
42
  fallbackImage?: Blob;
43
43
  };
44
+ /**
45
+ * Default implementation of {@link IPersonController}, backed by a {@link PeopleApiClient} and
46
+ * a set of {@link Query} caches for people, searches, photos, suggestions, and resolves.
47
+ */
44
48
  export declare class PersonController implements IPersonController {
45
49
  #private;
50
+ /**
51
+ * @param client - The people API client used to fetch person data, photos, and search results
52
+ * @param options - Optional controller options, such as a fallback image for missing photos
53
+ * @throws Error if the photo request fails for a reason other than a fallback-eligible 404
54
+ */
46
55
  constructor(client: PeopleApiClient, options?: PersonControllerOptions);
47
56
  /**
48
57
  * Suggest persons matching the given search string.
49
58
  * Search string can be a part of display name, mail, upn or the full azureId.
50
59
  * If systemAccounts is true, it will also include system accounts in the result.
60
+ *
61
+ * @param args - The search string, whether to include system accounts, and an optional abort signal
62
+ * @returns An observable emitting the matching suggestions
51
63
  */
52
64
  suggest(args: ResolverArgs<{
53
65
  search: string;
@@ -55,25 +67,110 @@ export declare class PersonController implements IPersonController {
55
67
  }>): Observable<SuggestPersonApiResponse>;
56
68
  /**
57
69
  * Resolve person details for given identifiers, which can be a mix of azureIds and upns.
70
+ *
71
+ * @param args - The identifiers to resolve and an optional abort signal
72
+ * @returns An observable emitting the resolved person details
58
73
  */
59
74
  resolve(args: ResolverArgs<{
60
75
  resolveIds: string[];
61
76
  }>): Observable<ResolvePersonApiResponse>;
77
+ /**
78
+ * Search for persons matching the given search string.
79
+ *
80
+ * @param args - The search string and an optional abort signal
81
+ * @returns An observable emitting the search results
82
+ */
62
83
  search(args: {
63
84
  search: string;
64
85
  signal?: AbortSignal;
65
86
  }): Observable<PersonSearchResult>;
66
- /** TODO why does this need to have data?!? */
87
+ /**
88
+ * Fetch the photo of a person, resolving by azureId when available, falling back to upn.
89
+ *
90
+ * TODO(#5088): why does this need to have data?!?
91
+ *
92
+ * @param args - A matcher (azureId and/or upn) plus an optional abort signal
93
+ * @returns An observable emitting the object URL of the person's photo
94
+ * @throws Error if neither azureId nor upn is provided
95
+ */
67
96
  getPhoto(args: ResolverArgs<MatcherArgs>): Observable<string>;
97
+ /**
98
+ * Fetch full person details, resolving by azureId when available, falling back to upn.
99
+ *
100
+ * @param args - A matcher (azureId and/or upn) plus an optional abort signal
101
+ * @returns An observable emitting the person details
102
+ * @throws Error if neither azureId nor upn is provided
103
+ */
68
104
  getPerson(args: ResolverArgs<MatcherArgs>): Observable<GetPersonResult>;
105
+ /**
106
+ * Fetch v2 person info, resolving by azureId when available, falling back to upn.
107
+ *
108
+ * @param args - A matcher (azureId and/or upn) plus an optional abort signal
109
+ * @returns An observable emitting the v2 person info
110
+ * @throws Error if neither azureId nor upn is provided
111
+ */
69
112
  getPersonInfo(args: ResolverArgs<MatcherArgs>): Observable<ApiPerson<'v2'>>;
113
+ /**
114
+ * Resolve a full v4 person by upn, via cache and live lookup.
115
+ *
116
+ * @param upn - The person's upn
117
+ * @param signal - Optional abort signal
118
+ * @returns An observable emitting the resolved v4 person
119
+ */
70
120
  protected _getPersonByUpn(upn: string, signal?: AbortSignal): Observable<GetPersonResult>;
121
+ /**
122
+ * Fetch a full v4 person by azureId.
123
+ *
124
+ * @param azureId - The person's azureId
125
+ * @param signal - Optional abort signal
126
+ * @returns An observable emitting the fetched v4 person
127
+ */
71
128
  _getPersonByAzureId(azureId: string, signal?: AbortSignal): Observable<GetPersonResult>;
129
+ /**
130
+ * Resolve v2 person info by azureId, via cache and live lookup.
131
+ *
132
+ * @param azureId - The person's azureId
133
+ * @param signal - Optional abort signal
134
+ * @returns An observable emitting the resolved v2 person info
135
+ */
72
136
  protected _getPersonInfoById(azureId: string, signal?: AbortSignal): Observable<ApiPerson<'v2'>>;
137
+ /**
138
+ * Resolve v2 person info by upn, via cache and search-based lookup.
139
+ *
140
+ * @param upn - The person's upn
141
+ * @param signal - Optional abort signal
142
+ * @returns An observable emitting the resolved v2 person info
143
+ */
73
144
  protected _getPersonInfoByUpn(upn: string, signal?: AbortSignal): Observable<ApiPerson<'v2'>>;
145
+ /**
146
+ * Fetch a person's photo by azureId as an object URL.
147
+ *
148
+ * @param azureId - The person's azureId
149
+ * @param signal - Optional abort signal
150
+ * @returns An observable emitting the photo's object URL
151
+ */
74
152
  protected _getPersonPhotoByAzureId(azureId: string, signal?: AbortSignal): Observable<string>;
153
+ /**
154
+ * Fetch a person's photo by upn, resolving their azureId first.
155
+ *
156
+ * @param upn - The person's upn
157
+ * @param signal - Optional abort signal
158
+ * @returns An observable emitting the photo's object URL
159
+ */
75
160
  protected _getPersonPhotoByUpn(upn: string, signal?: AbortSignal): Observable<string>;
161
+ /**
162
+ * Search the person-query cache for a matching v4 person.
163
+ *
164
+ * @param args - A matcher (azureId and/or upn) used to find the cached entry
165
+ * @returns An observable emitting the matching cached v4 person, if any
166
+ */
76
167
  protected _personCache$(args: MatcherArgs): Observable<GetPersonResult>;
168
+ /**
169
+ * Search the search-query cache for a matching v2 person.
170
+ *
171
+ * @param args - A matcher (azureId and/or upn) used to find the cached entry
172
+ * @returns An observable emitting the matching cached v2 person, if any
173
+ */
77
174
  protected _queryCache$(args: MatcherArgs): Observable<ApiPerson<'v2'>>;
78
175
  }
79
176
  export {};
@@ -2,4 +2,4 @@ import { type PersonControllerOptions } from './PersonController';
2
2
  import type { IApiProvider } from '@equinor/fusion-framework-module-services';
3
3
  export declare const makeResolver: (services: IApiProvider, options?: PersonControllerOptions) => import("react").LazyExoticComponent<({ children }: {
4
4
  readonly children?: React.ReactNode;
5
- }) => import("react/jsx-runtime").JSX.Element>;
5
+ }) => import("react").JSX.Element>;
@@ -1 +1 @@
1
- export declare const version = "2.0.2";
1
+ export declare const version = "2.0.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-react-components-people-provider",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "",
5
5
  "main": "dist/esm/index.js",
6
6
  "exports": {
@@ -23,19 +23,19 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "rxjs": "^7.8.1",
26
- "@equinor/fusion-framework-module-services": "^8.0.1",
27
- "@equinor/fusion-framework-react": "^8.0.0",
28
- "@equinor/fusion-query": "^7.0.1",
29
- "@equinor/fusion-framework-react-module": "^4.0.0"
26
+ "@equinor/fusion-framework-module-services": "^8.0.2",
27
+ "@equinor/fusion-framework-react-module": "^4.0.2",
28
+ "@equinor/fusion-query": "^7.0.2",
29
+ "@equinor/fusion-framework-react": "^8.0.1"
30
30
  },
31
31
  "devDependencies": {
32
- "@equinor/fusion-wc-person": "^3.5.3",
32
+ "@equinor/fusion-wc-person": "^3.5.5",
33
33
  "@types/react": "^19.2.7",
34
34
  "react": "^19.2.1",
35
- "typescript": "^6.0.3",
36
- "@equinor/fusion-framework-module-app": "^8.0.2",
37
- "@equinor/fusion-framework-module-bookmark": "^4.0.2",
38
- "@equinor/fusion-framework-module-event": "^6.0.0"
35
+ "typescript": "^7.0.2",
36
+ "@equinor/fusion-framework-module-app": "^8.0.3",
37
+ "@equinor/fusion-framework-module-bookmark": "^4.0.3",
38
+ "@equinor/fusion-framework-module-event": "^6.0.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@types/react": "^18.0.0 || ^19.0.0",
@@ -4,10 +4,11 @@ import {
4
4
  PersonProviderElement,
5
5
  PersonAvatarElement,
6
6
  PersonCardElement,
7
- PersonResolver,
7
+ type PersonResolver,
8
8
  PersonListItemElement,
9
9
  PersonSelectElement,
10
10
  } from '@equinor/fusion-wc-person';
11
+ export { PersonResolver } from '@equinor/fusion-wc-person';
11
12
 
12
13
  declare module 'react' {
13
14
  namespace JSX {
@@ -26,14 +27,22 @@ PersonCardElement;
26
27
  PersonListItemElement;
27
28
  PersonSelectElement;
28
29
 
29
- export { PersonResolver };
30
-
30
+ /**
31
+ * Wraps the `fwc-person-provider` web component, wiring the given `PersonResolver` into it
32
+ * once the underlying custom element is ready.
33
+ *
34
+ * @param props - Component props
35
+ * @param props.resolver - The resolver used to fetch person data, photos, and search results
36
+ * @param props.children - Elements rendered inside the person provider, typically person-related components
37
+ * @returns The rendered `fwc-person-provider` element wrapping the given children
38
+ */
31
39
  export const PeopleResolverComponent = (props: PropsWithChildren<{ resolver: PersonResolver }>) => {
32
40
  const { resolver, children } = props;
33
41
  const ref = useRef<PersonProviderElement | null>(null);
34
42
 
35
43
  // when the element is ready, set the resolver
36
44
  useEffect(() => {
45
+ // Only assign the resolver once the custom element ref has mounted
37
46
  if (ref.current && resolver) {
38
47
  ref.current.resolver = resolver;
39
48
  }
@@ -1,7 +1,7 @@
1
1
  import { type PropsWithChildren, type ReactNode, Suspense, useMemo } from 'react';
2
2
  import type { ServicesModule } from '@equinor/fusion-framework-module-services';
3
3
  import { useModule } from '@equinor/fusion-framework-react-module';
4
- import { makeResolver } from './makeResolver';
4
+ import { makeResolver } from './make-resolver';
5
5
  import type { PersonControllerOptions } from './PersonController';
6
6
 
7
7
  type PeopleResolverProviderProps = PropsWithChildren<{
@@ -9,9 +9,21 @@ type PeopleResolverProviderProps = PropsWithChildren<{
9
9
  readonly fallback?: ReactNode;
10
10
  }>;
11
11
 
12
+ /**
13
+ * Provides person resolution to its children by wiring up a `PersonResolver` built from the
14
+ * framework's `services` module.
15
+ *
16
+ * @param props - Component props
17
+ * @param props.children - Elements that will have access to the person resolver
18
+ * @param props.options - Optional controller options, such as a fallback image for missing photos
19
+ * @param props.fallback - Optional fallback rendered while the resolver component is suspended
20
+ * @returns The rendered people resolver provider wrapping the given children
21
+ * @throws Error if the `services` module has not been registered on the framework
22
+ */
12
23
  export const PeopleResolverProvider = (props: PeopleResolverProviderProps) => {
13
24
  const { children, options, fallback } = props;
14
25
  const services = useModule<ServicesModule>('services');
26
+ // Fail fast when the services module has not been registered on the framework
15
27
  if (!services) {
16
28
  throw Error('missing service module');
17
29
  }
@@ -29,6 +29,7 @@ const personMatcher =
29
29
  (args: MatcherArgs) =>
30
30
  <T extends { azureUniqueId?: string; upn?: string }>(value: T): value is T => {
31
31
  const { azureId, upn } = args;
32
+ // Both identifiers must match when both are supplied to avoid a false-positive match
32
33
  if (azureId && upn) {
33
34
  return (
34
35
  value.upn?.toLocaleLowerCase() === upn.toLocaleLowerCase() &&
@@ -57,6 +58,10 @@ export type PersonControllerOptions = {
57
58
  fallbackImage?: Blob;
58
59
  };
59
60
 
61
+ /**
62
+ * Default implementation of {@link IPersonController}, backed by a {@link PeopleApiClient} and
63
+ * a set of {@link Query} caches for people, searches, photos, suggestions, and resolves.
64
+ */
60
65
  export class PersonController implements IPersonController {
61
66
  #personQuery: Query<GetPersonResult, ResolverArgs<{ azureId: string }>>;
62
67
  #personSearchQuery: Query<PersonSearchResult, ResolverArgs<{ search: string }>>;
@@ -67,6 +72,11 @@ export class PersonController implements IPersonController {
67
72
  >;
68
73
  #personResolveQuery: Query<ResolvePersonApiResponse, ResolverArgs<{ resolveIds: string[] }>>;
69
74
 
75
+ /**
76
+ * @param client - The people API client used to fetch person data, photos, and search results
77
+ * @param options - Optional controller options, such as a fallback image for missing photos
78
+ * @throws Error if the photo request fails for a reason other than a fallback-eligible 404
79
+ */
70
80
  constructor(client: PeopleApiClient, options?: PersonControllerOptions) {
71
81
  const expire = 3 * 60 * 1000;
72
82
  this.#personQuery = new Query({
@@ -75,14 +85,17 @@ export class PersonController implements IPersonController {
75
85
  key: ({ azureId }) => azureId,
76
86
  client: {
77
87
  fn: ({ azureId }, signal): Observable<GetPersonResult> => {
88
+ // Filter out expired positions from the fetched person's result before returning it
78
89
  return client
79
90
  .get('v4', 'json$', { azureId, expand: ['manager', 'positions'] }, { signal })
80
91
  .pipe(
81
92
  map((result) => {
82
93
  const { positions = [] } = result;
94
+ // Drop positions that have already expired so stale data isn't shown
95
+ const activePositions = positions.filter((x) => new Date(x.appliesTo) > new Date());
83
96
  return {
84
97
  ...result,
85
- positions: positions.filter((x) => new Date(x.appliesTo) > new Date()),
98
+ positions: activePositions,
86
99
  };
87
100
  }),
88
101
  );
@@ -105,11 +118,13 @@ export class PersonController implements IPersonController {
105
118
  key: ({ azureId }) => azureId,
106
119
  client: {
107
120
  fn: ({ azureId }, signal): Observable<Blob> => {
121
+ // Extract the blob from the response, falling back to a placeholder image on 404
108
122
  return client.photo('v2', 'blob$', { azureId }, { signal }).pipe(
109
123
  map((result) => {
110
124
  return result.blob;
111
125
  }),
112
126
  catchError((err) => {
127
+ // Fall back to a placeholder image when the person genuinely has no photo
113
128
  if (
114
129
  (err as Error).name === 'ApiProviderError' &&
115
130
  (err as ApiProviderError).response?.status === 404 &&
@@ -130,6 +145,7 @@ export class PersonController implements IPersonController {
130
145
  client: {
131
146
  fn: ({ search, systemAccounts }, signal) => {
132
147
  const types = ['Person'];
148
+ // System accounts are opt-in since they're excluded from suggestions by default
133
149
  if (systemAccounts) {
134
150
  types.push('SystemAccount');
135
151
  }
@@ -161,33 +177,57 @@ export class PersonController implements IPersonController {
161
177
  * Suggest persons matching the given search string.
162
178
  * Search string can be a part of display name, mail, upn or the full azureId.
163
179
  * If systemAccounts is true, it will also include system accounts in the result.
180
+ *
181
+ * @param args - The search string, whether to include system accounts, and an optional abort signal
182
+ * @returns An observable emitting the matching suggestions
164
183
  */
165
184
  public suggest(
166
185
  args: ResolverArgs<{ search: string; systemAccounts: boolean }>,
167
186
  ): Observable<SuggestPersonApiResponse> {
168
187
  const { search, systemAccounts, signal } = args;
188
+ // Unwrap the query result to just its value
169
189
  return this.#personSuggestQuery.query({ search, systemAccounts }, { signal }).pipe(queryValue);
170
190
  }
171
191
 
172
192
  /**
173
193
  * Resolve person details for given identifiers, which can be a mix of azureIds and upns.
194
+ *
195
+ * @param args - The identifiers to resolve and an optional abort signal
196
+ * @returns An observable emitting the resolved person details
174
197
  */
175
198
  public resolve(
176
199
  args: ResolverArgs<{ resolveIds: string[] }>,
177
200
  ): Observable<ResolvePersonApiResponse> {
178
201
  const { resolveIds, signal } = args;
202
+ // Unwrap the query result to just its value
179
203
  return this.#personResolveQuery.query({ resolveIds }, { signal }).pipe(queryValue);
180
204
  }
181
205
 
206
+ /**
207
+ * Search for persons matching the given search string.
208
+ *
209
+ * @param args - The search string and an optional abort signal
210
+ * @returns An observable emitting the search results
211
+ */
182
212
  public search(args: { search: string; signal?: AbortSignal }): Observable<PersonSearchResult> {
183
213
  const { search, signal } = args;
214
+ // Unwrap the query result to just its value
184
215
  return this.#personSearchQuery.query({ search }, { signal }).pipe(queryValue);
185
216
  }
186
217
 
187
- /** TODO why does this need to have data?!? */
218
+ /**
219
+ * Fetch the photo of a person, resolving by azureId when available, falling back to upn.
220
+ *
221
+ * TODO(#5088): why does this need to have data?!?
222
+ *
223
+ * @param args - A matcher (azureId and/or upn) plus an optional abort signal
224
+ * @returns An observable emitting the object URL of the person's photo
225
+ * @throws Error if neither azureId nor upn is provided
226
+ */
188
227
  public getPhoto(args: ResolverArgs<MatcherArgs>): Observable<string> {
189
228
  const { azureId, upn, signal } = args;
190
229
 
230
+ // Prefer resolving by azureId when available, falling back to upn
191
231
  if (azureId) {
192
232
  return this._getPersonPhotoByAzureId(azureId, signal);
193
233
  } else if (upn) {
@@ -196,8 +236,16 @@ export class PersonController implements IPersonController {
196
236
  throw Error('invalid args provided');
197
237
  }
198
238
 
239
+ /**
240
+ * Fetch full person details, resolving by azureId when available, falling back to upn.
241
+ *
242
+ * @param args - A matcher (azureId and/or upn) plus an optional abort signal
243
+ * @returns An observable emitting the person details
244
+ * @throws Error if neither azureId nor upn is provided
245
+ */
199
246
  public getPerson(args: ResolverArgs<MatcherArgs>): Observable<GetPersonResult> {
200
247
  const { azureId, upn, signal } = args;
248
+ // Prefer resolving by azureId when available, falling back to upn
201
249
  if (azureId) {
202
250
  return this._getPersonByAzureId(azureId, signal);
203
251
  } else if (upn) {
@@ -206,8 +254,16 @@ export class PersonController implements IPersonController {
206
254
  throw Error('invalid args provided');
207
255
  }
208
256
 
257
+ /**
258
+ * Fetch v2 person info, resolving by azureId when available, falling back to upn.
259
+ *
260
+ * @param args - A matcher (azureId and/or upn) plus an optional abort signal
261
+ * @returns An observable emitting the v2 person info
262
+ * @throws Error if neither azureId nor upn is provided
263
+ */
209
264
  public getPersonInfo(args: ResolverArgs<MatcherArgs>): Observable<ApiPerson<'v2'>> {
210
265
  const { azureId, upn, signal } = args;
266
+ // Prefer resolving by azureId when available, falling back to upn
211
267
  if (azureId) {
212
268
  return this._getPersonInfoById(azureId, signal);
213
269
  } else if (upn) {
@@ -216,95 +272,155 @@ export class PersonController implements IPersonController {
216
272
  throw Error('invalid args provided');
217
273
  }
218
274
 
275
+ /**
276
+ * Resolve a full v4 person by upn, via cache and live lookup.
277
+ *
278
+ * @param upn - The person's upn
279
+ * @param signal - Optional abort signal
280
+ * @returns An observable emitting the resolved v4 person
281
+ */
219
282
  protected _getPersonByUpn(upn: string, signal?: AbortSignal): Observable<GetPersonResult> {
220
283
  const abort$ = signal ? fromEvent(signal, 'abort') : EMPTY;
221
- return concat(
222
- this._personCache$({ upn }),
223
- this._getPersonInfoByUpn(upn, signal).pipe(
224
- filter(isApiPerson('v2')),
225
- switchMap(({ azureUniqueId: azureId }) => {
226
- return this._getPersonByAzureId(azureId, signal);
227
- }),
228
- ),
229
- ).pipe(
230
- /** */
284
+ // Resolve the v2 person info by upn, then use its azureId to fetch the full v4 person
285
+ const personByAzureId$ = this._getPersonInfoByUpn(upn, signal).pipe(
286
+ filter(isApiPerson('v2')),
287
+ switchMap(({ azureUniqueId: azureId }) => {
288
+ return this._getPersonByAzureId(azureId, signal);
289
+ }),
290
+ );
291
+ // Emit from cache first, then fall back to the live lookup, stopping once a v4 person arrives
292
+ return concat(this._personCache$({ upn }), personByAzureId$).pipe(
231
293
  filter(isApiPerson('v4')),
232
294
  takeUntil(abort$),
233
295
  );
234
296
  }
235
297
 
298
+ /**
299
+ * Fetch a full v4 person by azureId.
300
+ *
301
+ * @param azureId - The person's azureId
302
+ * @param signal - Optional abort signal
303
+ * @returns An observable emitting the fetched v4 person
304
+ */
236
305
  public _getPersonByAzureId(azureId: string, signal?: AbortSignal): Observable<GetPersonResult> {
306
+ // Unwrap the query result to just its value
237
307
  return this.#personQuery.query({ azureId }, { signal }).pipe(queryValue);
238
308
  }
239
309
 
310
+ /**
311
+ * Resolve v2 person info by azureId, via cache and live lookup.
312
+ *
313
+ * @param azureId - The person's azureId
314
+ * @param signal - Optional abort signal
315
+ * @returns An observable emitting the resolved v2 person info
316
+ */
240
317
  protected _getPersonInfoById(azureId: string, signal?: AbortSignal): Observable<ApiPerson<'v2'>> {
241
318
  const abort$ = signal ? fromEvent(signal, 'abort') : EMPTY;
319
+ // Emit from caches first, then fall back to a live lookup, keeping only v2 persons
242
320
  return concat(
243
- /** */
244
321
  this._personCache$({ azureId }),
245
322
  this._queryCache$({ azureId }),
246
323
  this._getPersonByAzureId(azureId, signal),
247
- ).pipe(
248
- /** */
249
- filter(isApiPerson('v2')),
250
- takeUntil(abort$),
251
- );
324
+ ).pipe(filter(isApiPerson('v2')), takeUntil(abort$));
252
325
  }
253
326
 
327
+ /**
328
+ * Resolve v2 person info by upn, via cache and search-based lookup.
329
+ *
330
+ * @param upn - The person's upn
331
+ * @param signal - Optional abort signal
332
+ * @returns An observable emitting the resolved v2 person info
333
+ */
254
334
  protected _getPersonInfoByUpn(upn: string, signal?: AbortSignal): Observable<ApiPerson<'v2'>> {
255
335
  const matcher = personMatcher({ upn });
256
336
  const abort$ = signal ? fromEvent(signal, 'abort') : EMPTY;
257
- return concat(
258
- this._personCache$({ upn }),
259
- this._queryCache$({ upn }),
260
- this.#personSearchQuery.query({ search: upn }, { signal }).pipe(
261
- /** extract first match, should only be 0 or 1 */
262
- map((x) => x.value.find(matcher)),
263
- /** type cast and end stream */
264
- find(isApiPerson('v2')),
265
- ),
266
- ).pipe(find(isApiPerson('v2')), filter(isApiPerson('v2')), takeUntil(abort$));
337
+ // Search by upn, then narrow the results down to the single matching entry, if any
338
+ const searchMatch$ = this.#personSearchQuery.query({ search: upn }, { signal }).pipe(
339
+ map((x) => {
340
+ // Narrow the search results down to the one entry matching this upn/azureId
341
+ const match = x.value.find(matcher);
342
+ return match;
343
+ }),
344
+ find(isApiPerson('v2')),
345
+ );
346
+ // Emit from caches first, then fall back to the search-based lookup, keeping only v2 persons
347
+ return concat(this._personCache$({ upn }), this._queryCache$({ upn }), searchMatch$).pipe(
348
+ find(isApiPerson('v2')),
349
+ filter(isApiPerson('v2')),
350
+ takeUntil(abort$),
351
+ );
267
352
  }
268
353
 
354
+ /**
355
+ * Fetch a person's photo by azureId as an object URL.
356
+ *
357
+ * @param azureId - The person's azureId
358
+ * @param signal - Optional abort signal
359
+ * @returns An observable emitting the photo's object URL
360
+ */
269
361
  protected _getPersonPhotoByAzureId(azureId: string, signal?: AbortSignal): Observable<string> {
362
+ // Take just the first emission and convert the blob result to an object URL
270
363
  return this.#personPhotoQuery.query({ azureId }, { signal }).pipe(
271
- /** make subscription cold */
272
364
  take(1),
273
365
  map((result) => URL.createObjectURL(result.value)),
274
366
  );
275
367
  }
276
368
 
369
+ /**
370
+ * Fetch a person's photo by upn, resolving their azureId first.
371
+ *
372
+ * @param upn - The person's upn
373
+ * @param signal - Optional abort signal
374
+ * @returns An observable emitting the photo's object URL
375
+ */
277
376
  protected _getPersonPhotoByUpn(upn: string, signal?: AbortSignal) {
377
+ // Take just the first emission, then fetch the photo using its resolved azureId
278
378
  return this._getPersonInfoByUpn(upn, signal).pipe(
279
- /** make subscription cold */
280
379
  take(1),
281
380
  switchMap((x) => this._getPersonPhotoByAzureId(x.azureUniqueId, signal)),
282
381
  );
283
382
  }
284
383
 
384
+ /**
385
+ * Search the person-query cache for a matching v4 person.
386
+ *
387
+ * @param args - A matcher (azureId and/or upn) used to find the cached entry
388
+ * @returns An observable emitting the matching cached v4 person, if any
389
+ */
285
390
  protected _personCache$(args: MatcherArgs): Observable<GetPersonResult> {
286
391
  const mather = personMatcher(args);
392
+ // Search the person-query cache for the first matching v4 entry
287
393
  return this.#personQuery.cache.state$.pipe(
288
- /** make subscription cold */
289
394
  take(1),
290
- /** map out cached ApiPerson_v2 which matches upn */
291
- map((x) => Object.values(x).find((x) => mather(x.value))?.value),
395
+ map((x) => {
396
+ // Search each cached query result entry for the one matching this person
397
+ const match = Object.values(x).find((x) => mather(x.value));
398
+ return match?.value;
399
+ }),
292
400
  find(isApiPerson('v4')),
293
401
  filter(isApiPerson('v4')),
294
402
  );
295
403
  }
296
404
 
405
+ /**
406
+ * Search the search-query cache for a matching v2 person.
407
+ *
408
+ * @param args - A matcher (azureId and/or upn) used to find the cached entry
409
+ * @returns An observable emitting the matching cached v2 person, if any
410
+ */
297
411
  protected _queryCache$(args: MatcherArgs): Observable<ApiPerson<'v2'>> {
298
412
  const mather = personMatcher(args);
413
+ // Search the search-query cache for the first matching v2 entry
299
414
  return this.#personSearchQuery.cache.state$.pipe(
300
- /** make subscription cold */
301
415
  take(1),
302
416
  switchMap((entry) => {
303
- /** expand cache records */
417
+ // Expand the cache entry's records and find the one matching this person
304
418
  return from(Object.values(entry)).pipe(
305
- /** find matching cache record item */
306
- map((x) => x.value.find((x) => mather(x))),
307
- /** type cast and end stream */
419
+ map((x) => {
420
+ // Search each cached entry's results for the one matching this person
421
+ const match = x.value.find((x) => mather(x));
422
+ return match;
423
+ }),
308
424
  find(isApiPerson('v2')),
309
425
  );
310
426
  }),