@equinor/fusion-framework-module-context 8.0.0 → 8.0.2
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.
- package/CHANGELOG.md +32 -0
- package/README.md +70 -0
- package/dist/esm/ContextConfigBuilder.js +17 -6
- package/dist/esm/ContextConfigBuilder.js.map +1 -1
- package/dist/esm/ContextProvider.js +71 -15
- package/dist/esm/ContextProvider.js.map +1 -1
- package/dist/esm/client/ContextClient.js +13 -3
- package/dist/esm/client/ContextClient.js.map +1 -1
- package/dist/esm/configurator.js +10 -11
- package/dist/esm/configurator.js.map +1 -1
- package/dist/esm/{errors.js → errors/FusionContextSearchError.js} +3 -1
- package/dist/esm/errors/FusionContextSearchError.js.map +1 -0
- package/dist/esm/errors/index.js +2 -0
- package/dist/esm/errors/index.js.map +1 -0
- package/dist/esm/get-context-selector.js +14 -0
- package/dist/esm/get-context-selector.js.map +1 -0
- package/dist/esm/parse-context-item.js +31 -0
- package/dist/esm/parse-context-item.js.map +1 -0
- package/dist/esm/query-context-selector.js +12 -0
- package/dist/esm/query-context-selector.js.map +1 -0
- package/dist/esm/related-context-selector.js +12 -0
- package/dist/esm/related-context-selector.js.map +1 -0
- package/dist/esm/utils/extract-context-id-from-path.js +35 -0
- package/dist/esm/utils/extract-context-id-from-path.js.map +1 -0
- package/dist/esm/utils/resolve-context-from-path.js +11 -22
- package/dist/esm/utils/resolve-context-from-path.js.map +1 -1
- package/dist/esm/utils/resolve-initial-context.js +11 -15
- package/dist/esm/utils/resolve-initial-context.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ContextConfigBuilder.d.ts +16 -1
- package/dist/types/ContextProvider.d.ts +45 -3
- package/dist/types/client/ContextClient.d.ts +8 -1
- package/dist/types/{errors.d.ts → errors/FusionContextSearchError.d.ts} +2 -0
- package/dist/types/errors/index.d.ts +1 -0
- package/dist/types/get-context-selector.d.ts +9 -0
- package/dist/types/parse-context-item.d.ts +8 -0
- package/dist/types/query-context-selector.d.ts +7 -0
- package/dist/types/related-context-selector.d.ts +7 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/extract-context-id-from-path.d.ts +22 -0
- package/dist/types/utils/resolve-context-from-path.d.ts +9 -16
- package/dist/types/utils/resolve-initial-context.d.ts +3 -6
- package/dist/types/version.d.ts +1 -1
- package/package.json +15 -11
- package/src/ContextConfigBuilder.ts +32 -7
- package/src/ContextProvider.ts +86 -21
- package/src/client/ContextClient.ts +13 -3
- package/src/configurator.ts +16 -12
- package/src/{errors.ts → errors/FusionContextSearchError.ts} +2 -0
- package/src/errors/index.ts +1 -0
- package/src/get-context-selector.ts +18 -0
- package/src/parse-context-item.ts +39 -0
- package/src/query-context-selector.ts +15 -0
- package/src/related-context-selector.ts +15 -0
- package/src/types.ts +1 -1
- package/src/utils/extract-context-id-from-path.ts +39 -0
- package/src/utils/resolve-context-from-path.ts +21 -28
- package/src/utils/resolve-initial-context.ts +11 -23
- package/src/version.ts +1 -1
- package/dist/esm/errors.js.map +0 -1
- package/dist/esm/selectors.js +0 -58
- package/dist/esm/selectors.js.map +0 -1
- package/dist/types/selectors.d.ts +0 -19
- package/src/selectors.ts +0 -70
package/src/ContextProvider.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
FrameworkEventInit,
|
|
25
25
|
} from '@equinor/fusion-framework-module-event';
|
|
26
26
|
import Query from '@equinor/fusion-query';
|
|
27
|
+
import type { SemVer } from 'semver';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Interface representing a provider for managing and interacting with context items within an application.
|
|
@@ -296,6 +297,13 @@ export interface IContextProvider {
|
|
|
296
297
|
* @returns path for the context item
|
|
297
298
|
*/
|
|
298
299
|
generatePathFromContext?: (context: ContextItem, path: string) => string | undefined;
|
|
300
|
+
/**
|
|
301
|
+
* The version of the context provider.
|
|
302
|
+
*
|
|
303
|
+
* @remarks
|
|
304
|
+
* This property represents the version of the context provider, which can be a string or a SemVer object.
|
|
305
|
+
*/
|
|
306
|
+
readonly version: string | SemVer;
|
|
299
307
|
}
|
|
300
308
|
|
|
301
309
|
/**
|
|
@@ -346,35 +354,64 @@ export class ContextProvider
|
|
|
346
354
|
|
|
347
355
|
#contextQueue = new Subject<Observable<ContextItem<Record<string, unknown>>>>();
|
|
348
356
|
|
|
357
|
+
/**
|
|
358
|
+
* The underlying context client used to resolve and hold the current context item.
|
|
359
|
+
* @returns The internal {@link ContextClient} instance.
|
|
360
|
+
*/
|
|
349
361
|
public get contextClient() {
|
|
350
362
|
return this.#contextClient;
|
|
351
363
|
}
|
|
352
364
|
|
|
365
|
+
/**
|
|
366
|
+
* The query client used to search for context items.
|
|
367
|
+
* @returns The internal `Query` instance used by {@link queryContext}.
|
|
368
|
+
*/
|
|
353
369
|
public get queryClient() {
|
|
354
370
|
return this.#contextQuery;
|
|
355
371
|
}
|
|
356
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Observable stream emitting the current context item.
|
|
375
|
+
* @returns Observable that emits the current `ContextItem`, `null`, or `undefined`.
|
|
376
|
+
*/
|
|
357
377
|
get currentContext$(): Observable<ContextItem | null | undefined> {
|
|
358
378
|
return this.#contextClient.currentContext$;
|
|
359
379
|
}
|
|
360
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Snapshot of the current context item.
|
|
383
|
+
* @returns The current `ContextItem`, or `null`/`undefined` if not set.
|
|
384
|
+
*/
|
|
361
385
|
get currentContext(): ContextItem | undefined | null {
|
|
362
386
|
return this.#contextClient.currentContext;
|
|
363
387
|
}
|
|
364
388
|
|
|
365
|
-
/**
|
|
389
|
+
/**
|
|
390
|
+
* Sets the current context item.
|
|
391
|
+
* @deprecated do not use, will be removed
|
|
392
|
+
* @param context - The context item to set as current. Must not be `undefined`.
|
|
393
|
+
* @throws Error if `context` is `undefined`.
|
|
394
|
+
*/
|
|
366
395
|
set currentContext(context: ContextItem | null | undefined) {
|
|
367
396
|
console.warn(
|
|
368
397
|
'@deprecated',
|
|
369
398
|
'ContextProvider.currentContext',
|
|
370
399
|
'use setCurrentContextById|setCurrentContext|clearCurrentContext',
|
|
371
400
|
);
|
|
401
|
+
// undefined is reserved to mean "not yet initialized", so it cannot be set explicitly
|
|
372
402
|
if (context === undefined) {
|
|
373
403
|
throw Error('not allowed to set current context as undefined undefined!');
|
|
374
404
|
}
|
|
375
405
|
this.setCurrentContextAsync(context);
|
|
376
406
|
}
|
|
377
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Creates a new instance of `ContextProvider`.
|
|
410
|
+
* @param args - Constructor arguments.
|
|
411
|
+
* @param args.config - The context module configuration.
|
|
412
|
+
* @param args.event - Optional event module instance for dispatching context change events.
|
|
413
|
+
* @param args.parentContext - Optional parent context provider. Deprecated, use {@link connectParentContext}.
|
|
414
|
+
*/
|
|
378
415
|
constructor(args: {
|
|
379
416
|
config: ContextModuleConfig;
|
|
380
417
|
event?: ModuleType<EventModule>;
|
|
@@ -385,6 +422,7 @@ export class ContextProvider
|
|
|
385
422
|
|
|
386
423
|
super({ version, config });
|
|
387
424
|
|
|
425
|
+
// warn about deprecated parentContext constructor arg
|
|
388
426
|
if (args.parentContext) {
|
|
389
427
|
console.warn(
|
|
390
428
|
'@deprecated',
|
|
@@ -398,16 +436,19 @@ export class ContextProvider
|
|
|
398
436
|
if (config.resolveContext) {
|
|
399
437
|
this.resolveContext = config.resolveContext?.bind(this);
|
|
400
438
|
}
|
|
439
|
+
// override validateContext if configured
|
|
401
440
|
if (config.validateContext) {
|
|
402
441
|
this.validateContext = config.validateContext?.bind(this);
|
|
403
442
|
}
|
|
404
443
|
|
|
444
|
+
// override extractContextIdFromPath if configured
|
|
405
445
|
if (config.extractContextIdFromPath) {
|
|
406
|
-
// @ts-
|
|
446
|
+
// @ts-expect-error - this is to avoid breaking change, the signature will be updated in future major release
|
|
407
447
|
this.extractContextIdFromPath = config.extractContextIdFromPath;
|
|
408
448
|
}
|
|
449
|
+
// override generatePathFromContext if configured
|
|
409
450
|
if (config.generatePathFromContext) {
|
|
410
|
-
// @ts-
|
|
451
|
+
// @ts-expect-error - this is to avoid breaking change, the signature will be updated in future major release
|
|
411
452
|
this.generatePathFromContext = config.generatePathFromContext;
|
|
412
453
|
}
|
|
413
454
|
|
|
@@ -418,6 +459,7 @@ export class ContextProvider
|
|
|
418
459
|
this.#contextClient = new ContextClient(config.client.get);
|
|
419
460
|
this.#contextQuery = new Query(config.client.query);
|
|
420
461
|
|
|
462
|
+
// only create the related-context query when the config provides one
|
|
421
463
|
if (config.client.related) {
|
|
422
464
|
this.#contextRelated = new Query(config.client.related);
|
|
423
465
|
}
|
|
@@ -436,10 +478,8 @@ export class ContextProvider
|
|
|
436
478
|
this.#subscriptions.add(
|
|
437
479
|
// observe current context changes
|
|
438
480
|
this.currentContext$
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
pairwise(),
|
|
442
|
-
)
|
|
481
|
+
// emit previous and next context together for change comparisons
|
|
482
|
+
.pipe(pairwise())
|
|
443
483
|
.subscribe(([previous, next]) => {
|
|
444
484
|
this.#event?.dispatchEvent('onCurrentContextChanged', {
|
|
445
485
|
source: this,
|
|
@@ -494,12 +534,13 @@ export class ContextProvider
|
|
|
494
534
|
provider: IContextProvider,
|
|
495
535
|
opt?: { skipFirst: boolean },
|
|
496
536
|
): Subscription {
|
|
537
|
+
// build a stream of validated context changes from the parent provider
|
|
497
538
|
const parentContext$ = provider.currentContext$.pipe(
|
|
498
539
|
// do not set context if parent has not initialized
|
|
499
540
|
filter((x): x is ContextItem | null => x !== undefined),
|
|
500
541
|
filter((next, index) => {
|
|
501
542
|
// skip first item if opt.skipFirst is true
|
|
502
|
-
// TODO: this is a bit hacky, should be handled in a better way
|
|
543
|
+
// TODO(#5121): this is a bit hacky, should be handled in a better way
|
|
503
544
|
if (opt?.skipFirst && index <= 1) {
|
|
504
545
|
console.debug('ContextProvider::connectParentContext', 'skipping first item', next);
|
|
505
546
|
return false;
|
|
@@ -508,8 +549,8 @@ export class ContextProvider
|
|
|
508
549
|
return this.currentContext?.id !== next?.id;
|
|
509
550
|
}),
|
|
510
551
|
switchMap(async (next) => {
|
|
552
|
+
// if parent context is null, just return
|
|
511
553
|
if (!next) {
|
|
512
|
-
// if parent context is null, just return
|
|
513
554
|
return { next };
|
|
514
555
|
}
|
|
515
556
|
// notify event observers that parent context is about to change and await for cancelation
|
|
@@ -524,15 +565,19 @@ export class ContextProvider
|
|
|
524
565
|
filter((x) => !x.canceled),
|
|
525
566
|
switchMap(({ next }) => {
|
|
526
567
|
// set current context with validation and resolution
|
|
527
|
-
return
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
568
|
+
return (
|
|
569
|
+
this.setCurrentContext(next, {
|
|
570
|
+
validate: true,
|
|
571
|
+
resolve: true,
|
|
572
|
+
})
|
|
573
|
+
// swallow errors so a failed context change doesn't break the parent subscription
|
|
574
|
+
.pipe(
|
|
575
|
+
catchError((err) => {
|
|
576
|
+
console.warn('ContextProvider::onParentContextChanged', 'setCurrentContext', err);
|
|
577
|
+
// do not emit any value if an error occurs
|
|
578
|
+
return EMPTY;
|
|
579
|
+
}),
|
|
580
|
+
)
|
|
536
581
|
);
|
|
537
582
|
}),
|
|
538
583
|
catchError((err) => {
|
|
@@ -568,6 +613,7 @@ export class ContextProvider
|
|
|
568
613
|
this.#contextClient
|
|
569
614
|
// resolve context item by id
|
|
570
615
|
.resolveContext(id)
|
|
616
|
+
// filter out invalid items and set as current context
|
|
571
617
|
.pipe(
|
|
572
618
|
// filter out invalid context items
|
|
573
619
|
filter((item): item is ContextItem => !!item),
|
|
@@ -609,6 +655,11 @@ export class ContextProvider
|
|
|
609
655
|
* If the observable is subscribe, unsubscribing __WILL__ abort the task and remove it from queue
|
|
610
656
|
*
|
|
611
657
|
* @param context context item which would be queue to set as current
|
|
658
|
+
* @param opt Optional settings.
|
|
659
|
+
* @param opt.validate Whether to validate the context item before setting it.
|
|
660
|
+
* @param opt.resolve Whether to attempt to resolve the context item if validation fails.
|
|
661
|
+
* @template T The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
|
|
662
|
+
* @returns An observable that emits the context item once the queued task completes.
|
|
612
663
|
*/
|
|
613
664
|
public setCurrentContext<T extends ContextItem<Record<string, unknown>> | null>(
|
|
614
665
|
context: T,
|
|
@@ -620,6 +671,7 @@ export class ContextProvider
|
|
|
620
671
|
// wrapper for returning an observable to the caller
|
|
621
672
|
const subject$ = new Subject<T>();
|
|
622
673
|
|
|
674
|
+
// run the actual context-setting logic and relay results/errors to the caller's subject
|
|
623
675
|
const task$ = this._setCurrentContext(context, opt).pipe(
|
|
624
676
|
// send context item which was set to the caller
|
|
625
677
|
tap((x) => subject$.next(x)),
|
|
@@ -639,6 +691,7 @@ export class ContextProvider
|
|
|
639
691
|
// add task to internal queue
|
|
640
692
|
this.#contextQueue.next(task$ as Observable<ContextItem<Record<string, unknown>>>);
|
|
641
693
|
|
|
694
|
+
// tear down the queued task if the caller unsubscribes
|
|
642
695
|
return subject$.pipe(
|
|
643
696
|
// if caller subscribes, unsubscribe should abort queue entry
|
|
644
697
|
finalize(() => abort$.next(true)),
|
|
@@ -674,7 +727,7 @@ export class ContextProvider
|
|
|
674
727
|
* - Emit the context and complete the observable.
|
|
675
728
|
*
|
|
676
729
|
* @protected
|
|
677
|
-
* @
|
|
730
|
+
* @template T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
|
|
678
731
|
* @param context - The new context to set.
|
|
679
732
|
* @param opt - Optional settings:
|
|
680
733
|
* - `validate`: Whether to validate the context before setting.
|
|
@@ -708,7 +761,10 @@ export class ContextProvider
|
|
|
708
761
|
// emit error and complete
|
|
709
762
|
return subscriber.error(Error('failed to validate provided context'));
|
|
710
763
|
}
|
|
764
|
+
// if resolve is enabled, attempt to resolve the invalid context before setting it
|
|
711
765
|
if (opt.resolve) {
|
|
766
|
+
// the recursive `_setCurrentContext` call below is re-entered with the already
|
|
767
|
+
// validated/resolved context, so the generic `T` cast is safe here.
|
|
712
768
|
return of(context)
|
|
713
769
|
.pipe(
|
|
714
770
|
// notify event observers that context is about to get resolved
|
|
@@ -727,6 +783,7 @@ export class ContextProvider
|
|
|
727
783
|
}),
|
|
728
784
|
// resolve context
|
|
729
785
|
switchMap((context) =>
|
|
786
|
+
// Pair the resolved context alongside the original for downstream consumers.
|
|
730
787
|
this.resolveContext(context).pipe(
|
|
731
788
|
map((resolved) => ({
|
|
732
789
|
context,
|
|
@@ -789,7 +846,7 @@ export class ContextProvider
|
|
|
789
846
|
*
|
|
790
847
|
* @see {@link setCurrentContext} for more details.
|
|
791
848
|
*
|
|
792
|
-
* @
|
|
849
|
+
* @template T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
|
|
793
850
|
* @param context - The context item to set as the current context, or `null` to clear it.
|
|
794
851
|
* @param opt - Optional settings for context handling.
|
|
795
852
|
* @param opt.validate - If `true`, validates the context before setting it.
|
|
@@ -813,6 +870,7 @@ export class ContextProvider
|
|
|
813
870
|
*
|
|
814
871
|
* @param search - The search string to filter context items.
|
|
815
872
|
* @returns An Observable that emits an array of `ContextItem` objects matching the search criteria.
|
|
873
|
+
* @throws Re-throws the underlying cause of a `QueryClientError`, otherwise re-throws the original error.
|
|
816
874
|
*/
|
|
817
875
|
public queryContext(search: string): Observable<Array<ContextItem>> {
|
|
818
876
|
const query$ = this.queryClient
|
|
@@ -823,6 +881,7 @@ export class ContextProvider
|
|
|
823
881
|
type: this.#contextType,
|
|
824
882
|
}) as QueryContextParameters,
|
|
825
883
|
)
|
|
884
|
+
// unwrap query-client errors and expose the underlying cause to subscribers
|
|
826
885
|
.pipe(
|
|
827
886
|
catchError((err) => {
|
|
828
887
|
// if query client throws a QueryClientError, extract the cause and throw it
|
|
@@ -857,7 +916,9 @@ export class ContextProvider
|
|
|
857
916
|
* @returns `true` if the context type is not set or if the item's type ID matches one of the allowed types (case-insensitive); otherwise, `false`.
|
|
858
917
|
*/
|
|
859
918
|
public validateContext(item: ContextItem<Record<string, unknown>>): boolean {
|
|
919
|
+
// no context type configured means every context item is considered valid
|
|
860
920
|
if (!this.#contextType) return true;
|
|
921
|
+
// normalize allowed types for a case-insensitive comparison
|
|
861
922
|
return this.#contextType.map((x) => x.toLowerCase()).includes(item.type.id.toLowerCase());
|
|
862
923
|
}
|
|
863
924
|
|
|
@@ -881,7 +942,10 @@ export class ContextProvider
|
|
|
881
942
|
// request related context items for the given context item with the same context type which the provider is configured with
|
|
882
943
|
return this.relatedContexts({ item, filter: { type: this.#contextType } }).pipe(
|
|
883
944
|
// filter out invalid context items
|
|
884
|
-
map((x) =>
|
|
945
|
+
map((x) =>
|
|
946
|
+
// keep only context items that validate against the provider's context type
|
|
947
|
+
x.filter((item) => this.validateContext(item)),
|
|
948
|
+
),
|
|
885
949
|
map((values) => {
|
|
886
950
|
// related context should be resolved to a single context item
|
|
887
951
|
const value = values.shift();
|
|
@@ -941,6 +1005,7 @@ export class ContextProvider
|
|
|
941
1005
|
return this.#contextRelated.query(args).pipe(
|
|
942
1006
|
map(({ value }) => value),
|
|
943
1007
|
catchError((err) => {
|
|
1008
|
+
// unwrap the underlying cause so callers see the original error
|
|
944
1009
|
if (err.cause) {
|
|
945
1010
|
throw err.cause;
|
|
946
1011
|
}
|
|
@@ -25,7 +25,7 @@ export type GetContextParameters = { id: string };
|
|
|
25
25
|
* @template ContextItem The type of the context item managed by the client.
|
|
26
26
|
* @extends Observable<ContextItem | null | undefined>
|
|
27
27
|
*
|
|
28
|
-
* @todo - should this have `undefined` as a valid value?
|
|
28
|
+
* @todo(#5116) - should this have `undefined` as a valid value?
|
|
29
29
|
*
|
|
30
30
|
* @example
|
|
31
31
|
* ```typescript
|
|
@@ -70,6 +70,10 @@ export class ContextClient extends Observable<ContextItem | null | undefined> {
|
|
|
70
70
|
return this.#client;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Creates a new `ContextClient`.
|
|
75
|
+
* @param options - Query constructor options used to fetch a `ContextItem` by its ID.
|
|
76
|
+
*/
|
|
73
77
|
constructor(options: QueryCtorOptions<ContextItem, GetContextParameters>) {
|
|
74
78
|
super((observer) => this.#currentContext$.subscribe(observer));
|
|
75
79
|
this.#client = new Query(options);
|
|
@@ -85,10 +89,11 @@ export class ContextClient extends Observable<ContextItem | null | undefined> {
|
|
|
85
89
|
* @param idOrItem - The context identifier (string), a `ContextItem`, or `null`. If omitted, the current context may be cleared.
|
|
86
90
|
*/
|
|
87
91
|
public setCurrentContext(idOrItem?: string | ContextItem | null): void {
|
|
92
|
+
// resolve string identifiers to a context item before setting
|
|
88
93
|
if (typeof idOrItem === 'string') {
|
|
89
|
-
// TODO - compare context
|
|
94
|
+
// TODO(#5117) - compare context
|
|
95
|
+
// TODO(#5117) should this catch error?
|
|
90
96
|
this.resolveContext(idOrItem)
|
|
91
|
-
// TODO should this catch error?
|
|
92
97
|
.pipe(catchError(() => EMPTY))
|
|
93
98
|
.subscribe((value) => this.setCurrentContext(value));
|
|
94
99
|
/** only add context if not match */
|
|
@@ -105,10 +110,12 @@ export class ContextClient extends Observable<ContextItem | null | undefined> {
|
|
|
105
110
|
* @throws Rethrows the underlying error cause if present, otherwise throws the original error.
|
|
106
111
|
*/
|
|
107
112
|
public resolveContext(id: string): Observable<ContextItem> {
|
|
113
|
+
// unwrap the query result into the resolved context item
|
|
108
114
|
return this.#client.query({ id }).pipe(
|
|
109
115
|
map((x) => x.value),
|
|
110
116
|
// unwrap error
|
|
111
117
|
catchError((err) => {
|
|
118
|
+
// unwrap the underlying cause so callers see the original error
|
|
112
119
|
if (err.cause) {
|
|
113
120
|
throw err.cause;
|
|
114
121
|
}
|
|
@@ -131,6 +138,9 @@ export class ContextClient extends Observable<ContextItem | null | undefined> {
|
|
|
131
138
|
return fn(this.resolveContext(id));
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Disposes of the client, completing the internal current-context subject.
|
|
143
|
+
*/
|
|
134
144
|
public dispose(): void {
|
|
135
145
|
this.#currentContext$.complete();
|
|
136
146
|
}
|
package/src/configurator.ts
CHANGED
|
@@ -8,7 +8,11 @@ import type {
|
|
|
8
8
|
} from '@equinor/fusion-framework-module';
|
|
9
9
|
import type { ServicesModule, IApiProvider } from '@equinor/fusion-framework-module-services';
|
|
10
10
|
import type { NavigationModule } from '@equinor/fusion-framework-module-navigation';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getContextSelector,
|
|
13
|
+
queryContextSelector,
|
|
14
|
+
relatedContextSelector,
|
|
15
|
+
} from './get-context-selector';
|
|
12
16
|
import type { QueryCtorOptions } from '@equinor/fusion-query';
|
|
13
17
|
import type {
|
|
14
18
|
ContextFilterFn,
|
|
@@ -149,9 +153,10 @@ export interface ContextModuleConfig {
|
|
|
149
153
|
* @returns An observable input emitting the initial context item, or void.
|
|
150
154
|
*/
|
|
151
155
|
resolveInitialContext?: (args: {
|
|
152
|
-
//
|
|
156
|
+
// biome-ignore lint/suspicious/noExplicitAny: `AnyModuleInstance | any` intentionally widens to accept any module instance shape for `ref`
|
|
153
157
|
ref?: AnyModuleInstance | any;
|
|
154
158
|
modules: ModuleInstance;
|
|
159
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: `void` here relies on TypeScript's special-cased "void-returning callback accepts any return value" behavior — `undefined` would break assignability of resolver functions that only conditionally emit a `ContextItem`
|
|
155
160
|
}) => ObservableInput<ContextItem | void>;
|
|
156
161
|
}
|
|
157
162
|
|
|
@@ -206,10 +211,12 @@ export class ContextModuleConfigurator implements IContextModuleConfigurator {
|
|
|
206
211
|
protected async _getServiceProvider(
|
|
207
212
|
init: ModuleInitializerArgs<IContextModuleConfigurator, [ServicesModule]>,
|
|
208
213
|
): Promise<IApiProvider> {
|
|
214
|
+
// prefer the local services module instance if available
|
|
209
215
|
if (init.hasModule('services')) {
|
|
210
216
|
return init.requireInstance('services');
|
|
211
217
|
}
|
|
212
218
|
const parentServiceModule = (init.ref as ModulesInstanceType<[ServicesModule]>)?.services;
|
|
219
|
+
// fall back to the parent module's services instance
|
|
213
220
|
if (!parentServiceModule) {
|
|
214
221
|
throw Error('no service services provider configures [ServicesModule]');
|
|
215
222
|
}
|
|
@@ -230,24 +237,21 @@ export class ContextModuleConfigurator implements IContextModuleConfigurator {
|
|
|
230
237
|
public async createConfig(
|
|
231
238
|
init: ModuleInitializerArgs<IContextModuleConfigurator, [ServicesModule, NavigationModule]>,
|
|
232
239
|
): Promise<ContextModuleConfig> {
|
|
240
|
+
// run each registered config builder in sequence, merging results into the accumulated config
|
|
233
241
|
const config = await this.#configBuilders.reduce(
|
|
234
242
|
async (cur, cb) => {
|
|
235
|
-
//
|
|
243
|
+
// biome-ignore lint/suspicious/noExplicitAny: builder generics are erased here — `unknown` breaks assignability of the concrete `ContextConfigBuilder` instance passed to consumer callbacks
|
|
236
244
|
const builder = new ContextConfigBuilder<any, any>(init, await cur);
|
|
237
245
|
await Promise.resolve(cb(builder));
|
|
246
|
+
// Merge this builder's config into the accumulator for the next iteration.
|
|
238
247
|
return Object.assign(cur, builder.config);
|
|
239
248
|
},
|
|
240
249
|
Promise.resolve({} as Partial<ContextModuleConfig>),
|
|
241
250
|
);
|
|
242
251
|
|
|
243
|
-
config.resolveInitialContext ??= resolveInitialContext(
|
|
244
|
-
path: {
|
|
245
|
-
extract: config.extractContextIdFromPath,
|
|
246
|
-
validate: config.extractContextIdFromPath ? () => true : undefined,
|
|
247
|
-
},
|
|
248
|
-
});
|
|
252
|
+
config.resolveInitialContext ??= resolveInitialContext();
|
|
249
253
|
|
|
250
|
-
// TODO - make less lazy
|
|
254
|
+
// TODO(#5119) - make less lazy
|
|
251
255
|
config.client ??= await (async (): Promise<ContextModuleConfig['client']> => {
|
|
252
256
|
const apiProvider = await this._getServiceProvider(init);
|
|
253
257
|
const contextClient = await apiProvider.createContextClient('json$');
|
|
@@ -263,7 +267,7 @@ export class ContextModuleConfigurator implements IContextModuleConfigurator {
|
|
|
263
267
|
client: {
|
|
264
268
|
fn: (query) => contextClient.query('v1', { query }, { selector: queryContextSelector }),
|
|
265
269
|
},
|
|
266
|
-
// TODO - might cast to checksum
|
|
270
|
+
// TODO(#5118) - might cast to checksum
|
|
267
271
|
key: (args) => JSON.stringify(args),
|
|
268
272
|
expire: this.defaultExpireTime,
|
|
269
273
|
},
|
|
@@ -277,7 +281,7 @@ export class ContextModuleConfigurator implements IContextModuleConfigurator {
|
|
|
277
281
|
);
|
|
278
282
|
},
|
|
279
283
|
},
|
|
280
|
-
// TODO - might cast to checksum
|
|
284
|
+
// TODO(#5118) - might cast to checksum
|
|
281
285
|
key: (args) => JSON.stringify(args),
|
|
282
286
|
expire: this.defaultExpireTime,
|
|
283
287
|
},
|
|
@@ -17,6 +17,7 @@ export class FusionContextSearchError extends Error {
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* The title of the error.
|
|
20
|
+
* @returns The error title.
|
|
20
21
|
*/
|
|
21
22
|
get title(): string {
|
|
22
23
|
return this.#details.title;
|
|
@@ -24,6 +25,7 @@ export class FusionContextSearchError extends Error {
|
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* The description of the error, if available.
|
|
28
|
+
* @returns The error description, or `undefined` if none was provided.
|
|
27
29
|
*/
|
|
28
30
|
get description(): string | undefined {
|
|
29
31
|
return this.#details.description;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FusionContextSearchError';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GetContextResponse } from '@equinor/fusion-framework-module-services/context/get';
|
|
2
|
+
|
|
3
|
+
import { parseContextItem } from './parse-context-item';
|
|
4
|
+
import type { ContextItem } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse the response from the GetContext API into a context item.
|
|
8
|
+
* @param response The response object containing the context item.
|
|
9
|
+
* @returns A promise that resolves to the context item.
|
|
10
|
+
*/
|
|
11
|
+
export const getContextSelector = async (response: Response): Promise<ContextItem> => {
|
|
12
|
+
const result = (await response.json()) as GetContextResponse<'v1'>;
|
|
13
|
+
return parseContextItem(result);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Deliberately re-exported from here to preserve the package's existing selectors entry point
|
|
17
|
+
export { queryContextSelector } from './query-context-selector';
|
|
18
|
+
export { relatedContextSelector } from './related-context-selector';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiVersion,
|
|
3
|
+
ApiContextEntity,
|
|
4
|
+
} from '@equinor/fusion-framework-module-services/context';
|
|
5
|
+
import type { GetContextResponse } from '@equinor/fusion-framework-module-services/context/get';
|
|
6
|
+
|
|
7
|
+
import type { ContextItem, ContextItemType } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses the context type from the response of the GetContext API.
|
|
11
|
+
*
|
|
12
|
+
* @param type The type property from the GetContext response.
|
|
13
|
+
* @returns The parsed context item type.
|
|
14
|
+
*/
|
|
15
|
+
const parseContextType = (type: GetContextResponse<'v1'>['type']): ContextItemType => ({
|
|
16
|
+
id: type.id,
|
|
17
|
+
isChildType: type.isChildType,
|
|
18
|
+
parentTypeIds: type.parentTypeIds ?? [],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parses an ApiContextEntity object into a ContextItem object.
|
|
23
|
+
* @param item The ApiContextEntity object to parse.
|
|
24
|
+
* @returns The parsed ContextItem object.
|
|
25
|
+
*/
|
|
26
|
+
export const parseContextItem = (item: ApiContextEntity<ApiVersion.v1>): ContextItem => {
|
|
27
|
+
return {
|
|
28
|
+
id: item.id,
|
|
29
|
+
externalId: item.externalId ?? undefined,
|
|
30
|
+
isActive: item.isActive,
|
|
31
|
+
isDeleted: item.isDeleted,
|
|
32
|
+
created: new Date(item.created),
|
|
33
|
+
source: item.source ?? undefined,
|
|
34
|
+
title: item.title ?? undefined,
|
|
35
|
+
type: parseContextType(item.type),
|
|
36
|
+
// TODO(#5115): parse and map the raw `value` payload into a typed context item value
|
|
37
|
+
value: item.value ?? {},
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { QueryContextResponse } from '@equinor/fusion-framework-module-services/context/query';
|
|
2
|
+
|
|
3
|
+
import { parseContextItem } from './parse-context-item';
|
|
4
|
+
import type { ContextItem } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse the response from the QueryContext API into an array of context items.
|
|
8
|
+
* @param response The response object.
|
|
9
|
+
* @returns A promise that resolves to an array of context items.
|
|
10
|
+
*/
|
|
11
|
+
export const queryContextSelector = async (response: Response): Promise<ContextItem[]> => {
|
|
12
|
+
const result = (await response.json()) as QueryContextResponse<'v1'>;
|
|
13
|
+
// parse each raw API entry into a ContextItem
|
|
14
|
+
return result.map(parseContextItem);
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RelatedContextResponse } from '@equinor/fusion-framework-module-services/context/related';
|
|
2
|
+
|
|
3
|
+
import { parseContextItem } from './parse-context-item';
|
|
4
|
+
import type { ContextItem } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse the response from the RelatedContext API into an array of context items.
|
|
8
|
+
* @param response The response object containing the related context items.
|
|
9
|
+
* @returns A promise that resolves to an array of ContextItem objects.
|
|
10
|
+
*/
|
|
11
|
+
export const relatedContextSelector = async (response: Response): Promise<ContextItem[]> => {
|
|
12
|
+
const result = (await response.json()) as RelatedContextResponse<'v1'>;
|
|
13
|
+
// parse each raw API entry into a ContextItem
|
|
14
|
+
return result.map(parseContextItem);
|
|
15
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* @property graphic - Optional graphical representation, either as a string or an object containing type and content.
|
|
17
17
|
* @property meta - Optional meta information, either as a string or an object containing type and content.
|
|
18
18
|
*
|
|
19
|
-
* @todo - convert to Zod schema for validation and type safety.
|
|
19
|
+
* @todo(#5122) - convert to Zod schema for validation and type safety.
|
|
20
20
|
*/
|
|
21
21
|
export type ContextItem<TType extends Record<string, unknown> = Record<string, unknown>> = {
|
|
22
22
|
id: string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// GUID pattern
|
|
2
|
+
const matchGUID =
|
|
3
|
+
/^(?:(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12})$/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Method will try to extract a context id from a path.
|
|
7
|
+
* The default matcher is a GUID pattern.
|
|
8
|
+
* Will iterate over the path and return the first match.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const path = '/apps/context/7fd97952-7fe6-409b-a6dc-292dbf0e50d7?dsadasdas#example';
|
|
13
|
+
* const contextId = extractContextIdFromPath(path); // '7fd97952-7fe6-409b-a6dc-292dbf0e50d7'
|
|
14
|
+
*
|
|
15
|
+
* // Custom matcher for numeric IDs
|
|
16
|
+
* extractContextIdFromPath('/projects/42/details', /^\d+$/); // '42'
|
|
17
|
+
*
|
|
18
|
+
* // No match
|
|
19
|
+
* extractContextIdFromPath('/apps/my-app/settings'); // undefined
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @param path string - the path to extract the context id from
|
|
23
|
+
* @param matcher RegExp - the pattern to match against
|
|
24
|
+
* @returns string | undefined - the context id or undefined
|
|
25
|
+
*/
|
|
26
|
+
export const extractContextIdFromPath = (
|
|
27
|
+
path: string,
|
|
28
|
+
matcher: RegExp = matchGUID,
|
|
29
|
+
): string | undefined =>
|
|
30
|
+
//
|
|
31
|
+
path
|
|
32
|
+
// remove query-string and hash fragments before segment matching
|
|
33
|
+
.split(/[?#]/)[0]
|
|
34
|
+
// remove leading slashes
|
|
35
|
+
.replace(/^\/+/, '')
|
|
36
|
+
// split path by slashes
|
|
37
|
+
.split('/')
|
|
38
|
+
// find the first path fragment that matches the matcher
|
|
39
|
+
.find((x) => x.match(matcher));
|