@equinor/fusion-framework-module-context 9.0.0-next.0 → 9.0.1
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/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +13 -10
- package/CHANGELOG.md +0 -1095
- package/docs/data-model.md +0 -134
- package/docs/lifecycle.md +0 -163
- package/docs/recipes.md +0 -89
- package/src/ContextModuleConfig.ts +0 -147
- package/src/ContextModuleConfigurator.interface.ts +0 -145
- package/src/ContextModuleConfigurator.ts +0 -295
- package/src/ContextProvider.ts +0 -1158
- package/src/__tests__/ContextModuleConfigurator.test.ts +0 -412
- package/src/__tests__/mock/context-mock.test.ts +0 -137
- package/src/__tests__/mock/create-context-item-factory.test.ts +0 -60
- package/src/__tests__/mock/create-context-items.test.ts +0 -58
- package/src/client/ContextClient.ts +0 -149
- package/src/errors/FusionContextSearchError.ts +0 -56
- package/src/errors/index.ts +0 -1
- package/src/index.ts +0 -29
- package/src/mock/ContextMockConfigurator.ts +0 -244
- package/src/mock/fixtures/create-context-item-factory.ts +0 -62
- package/src/mock/fixtures/create-context-items.ts +0 -80
- package/src/mock/fixtures/index.ts +0 -28
- package/src/mock/fixtures/string-to-seed.ts +0 -18
- package/src/mock/index.ts +0 -33
- package/src/mock/module.ts +0 -54
- package/src/module.ts +0 -178
- package/src/selectors/get-context-selector.ts +0 -14
- package/src/selectors/index.ts +0 -12
- package/src/selectors/query-context-selector.ts +0 -15
- package/src/selectors/related-context-selector.ts +0 -15
- package/src/types.ts +0 -98
- package/src/utils/enable-context.ts +0 -40
- package/src/utils/extract-context-id-from-path.ts +0 -39
- package/src/utils/index.ts +0 -15
- package/src/utils/parse-context-item.ts +0 -39
- package/src/utils/resolve-context-from-path.ts +0 -118
- package/src/utils/resolve-initial-context.ts +0 -58
- package/src/version.ts +0 -2
- package/tsconfig.json +0 -33
- package/vitest.config.ts +0 -11
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { Observable, BehaviorSubject, EMPTY, lastValueFrom, firstValueFrom } from 'rxjs';
|
|
2
|
-
import { catchError, map } from 'rxjs/operators';
|
|
3
|
-
|
|
4
|
-
import equal from 'fast-deep-equal';
|
|
5
|
-
|
|
6
|
-
import { Query, type QueryCtorOptions } from '@equinor/fusion-query';
|
|
7
|
-
|
|
8
|
-
import type { ContextItem } from '../types';
|
|
9
|
-
|
|
10
|
-
export type GetContextParameters = { id: string };
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* `ContextClient` is an observable client for managing and retrieving context items.
|
|
14
|
-
*
|
|
15
|
-
* This class extends `Observable<ContextItem | null | undefined>`, allowing consumers to subscribe to context changes.
|
|
16
|
-
* It encapsulates a `Query` instance for fetching context items by ID and maintains the current context state using a `BehaviorSubject`.
|
|
17
|
-
*
|
|
18
|
-
* ### Features
|
|
19
|
-
* - Exposes the current context synchronously and as an observable stream.
|
|
20
|
-
* - Provides methods to set the current context by ID or item, resolving and updating as needed.
|
|
21
|
-
* - Supports asynchronous context resolution with optional await behavior.
|
|
22
|
-
* - Handles errors during context resolution, unwrapping nested causes.
|
|
23
|
-
* - Implements a `dispose` method to clean up internal subscriptions.
|
|
24
|
-
*
|
|
25
|
-
* @template ContextItem The type of the context item managed by the client.
|
|
26
|
-
* @extends Observable<ContextItem | null | undefined>
|
|
27
|
-
*
|
|
28
|
-
* @todo(#5116) - should this have `undefined` as a valid value?
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```typescript
|
|
32
|
-
* const client = new ContextClient(options);
|
|
33
|
-
* client.setCurrentContext('context-id');
|
|
34
|
-
* client.currentContext$.subscribe(ctx => { ... });
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export class ContextClient extends Observable<ContextItem | null | undefined> {
|
|
38
|
-
#client: Query<ContextItem, { id: string }>;
|
|
39
|
-
/** might change to reactive state, for comparing state with reducer */
|
|
40
|
-
#currentContext$: BehaviorSubject<ContextItem | null | undefined>;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Gets the current context item.
|
|
44
|
-
*
|
|
45
|
-
* @returns The current {@link ContextItem}, or `null` if no context is set, or `undefined` if the context has not been initialized.
|
|
46
|
-
*/
|
|
47
|
-
get currentContext(): ContextItem | null | undefined {
|
|
48
|
-
return this.#currentContext$.value;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* An observable stream that emits the current context item.
|
|
53
|
-
*
|
|
54
|
-
* @remarks
|
|
55
|
-
* This observable emits the current `ContextItem` whenever it changes.
|
|
56
|
-
* It can emit `null` or `undefined` if there is no current context.
|
|
57
|
-
*
|
|
58
|
-
* @returns Observable that emits the current `ContextItem`, `null`, or `undefined`.
|
|
59
|
-
*/
|
|
60
|
-
get currentContext$(): Observable<ContextItem | null | undefined> {
|
|
61
|
-
return this.#currentContext$.asObservable();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Gets the query client for retrieving a specific `ContextItem` by its ID.
|
|
66
|
-
*
|
|
67
|
-
* @returns A `Query` instance configured to fetch a `ContextItem` using an object containing an `id` property.
|
|
68
|
-
*/
|
|
69
|
-
get client(): Query<ContextItem, { id: string }> {
|
|
70
|
-
return this.#client;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Creates a new `ContextClient`.
|
|
75
|
-
* @param options - Query constructor options used to fetch a `ContextItem` by its ID.
|
|
76
|
-
*/
|
|
77
|
-
constructor(options: QueryCtorOptions<ContextItem, GetContextParameters>) {
|
|
78
|
-
super((observer) => this.#currentContext$.subscribe(observer));
|
|
79
|
-
this.#client = new Query(options);
|
|
80
|
-
this.#currentContext$ = new BehaviorSubject<ContextItem | null | undefined>(undefined);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Sets the current context based on the provided identifier or context item.
|
|
85
|
-
*
|
|
86
|
-
* If a string identifier is provided, attempts to resolve the corresponding context asynchronously.
|
|
87
|
-
* If a `ContextItem` or `null` is provided, updates the current context only if it differs from the existing one.
|
|
88
|
-
*
|
|
89
|
-
* @param idOrItem - The context identifier (string), a `ContextItem`, or `null`. If omitted, the current context may be cleared.
|
|
90
|
-
*/
|
|
91
|
-
public setCurrentContext(idOrItem?: string | ContextItem | null): void {
|
|
92
|
-
// resolve string identifiers to a context item before setting
|
|
93
|
-
if (typeof idOrItem === 'string') {
|
|
94
|
-
// TODO(#5117) - compare context
|
|
95
|
-
// TODO(#5117) should this catch error?
|
|
96
|
-
this.resolveContext(idOrItem)
|
|
97
|
-
.pipe(catchError(() => EMPTY))
|
|
98
|
-
.subscribe((value) => this.setCurrentContext(value));
|
|
99
|
-
/** only add context if not match */
|
|
100
|
-
} else if (!equal(idOrItem, this.#currentContext$.value)) {
|
|
101
|
-
this.#currentContext$.next(idOrItem);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Resolves a context item by its unique identifier.
|
|
107
|
-
*
|
|
108
|
-
* @param id - The unique identifier of the context item to resolve.
|
|
109
|
-
* @returns An Observable that emits the resolved {@link ContextItem}.
|
|
110
|
-
* @throws Rethrows the underlying error cause if present, otherwise throws the original error.
|
|
111
|
-
*/
|
|
112
|
-
public resolveContext(id: string): Observable<ContextItem> {
|
|
113
|
-
// unwrap the query result into the resolved context item
|
|
114
|
-
return this.#client.query({ id }).pipe(
|
|
115
|
-
map((x) => x.value),
|
|
116
|
-
// unwrap error
|
|
117
|
-
catchError((err) => {
|
|
118
|
-
// unwrap the underlying cause so callers see the original error
|
|
119
|
-
if (err.cause) {
|
|
120
|
-
throw err.cause;
|
|
121
|
-
}
|
|
122
|
-
throw err;
|
|
123
|
-
}),
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Resolves a context item asynchronously by its ID.
|
|
129
|
-
*
|
|
130
|
-
* @param id - The unique identifier of the context item to resolve.
|
|
131
|
-
* @param opt - Optional settings for resolution.
|
|
132
|
-
* @param opt.awaitResolve - If true, waits for the observable to complete and returns the last emitted value;
|
|
133
|
-
* otherwise, returns the first emitted value.
|
|
134
|
-
* @returns A promise that resolves to the requested {@link ContextItem}.
|
|
135
|
-
*/
|
|
136
|
-
public resolveContextAsync(id: string, opt?: { awaitResolve: boolean }): Promise<ContextItem> {
|
|
137
|
-
const fn = opt?.awaitResolve ? lastValueFrom : firstValueFrom;
|
|
138
|
-
return fn(this.resolveContext(id));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Disposes of the client, completing the internal current-context subject.
|
|
143
|
-
*/
|
|
144
|
-
public dispose(): void {
|
|
145
|
-
this.#currentContext$.complete();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export default ContextClient;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Represents an error that occurs during a Fusion context search operation.
|
|
3
|
-
*
|
|
4
|
-
* This error provides a title and an optional description to give more context
|
|
5
|
-
* about the failure. It extends the built-in `Error` class and sets the error
|
|
6
|
-
* name to `'FusionContextSearchError'`.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```typescript
|
|
10
|
-
* throw new FusionContextSearchError({ title: 'Search failed', description: 'No results found.' });
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* @public
|
|
14
|
-
*/
|
|
15
|
-
export class FusionContextSearchError extends Error {
|
|
16
|
-
#details;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The title of the error.
|
|
20
|
-
* @returns The error title.
|
|
21
|
-
*/
|
|
22
|
-
get title(): string {
|
|
23
|
-
return this.#details.title;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The description of the error, if available.
|
|
28
|
-
* @returns The error description, or `undefined` if none was provided.
|
|
29
|
-
*/
|
|
30
|
-
get description(): string | undefined {
|
|
31
|
-
return this.#details.description;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Creates a new instance of FusionContextSearchError.
|
|
36
|
-
* @param details - The details of the error.
|
|
37
|
-
* @param options - Optional parameters for the error.
|
|
38
|
-
*/
|
|
39
|
-
constructor(
|
|
40
|
-
details: {
|
|
41
|
-
/**
|
|
42
|
-
* The title of the error.
|
|
43
|
-
*/
|
|
44
|
-
title: string;
|
|
45
|
-
/**
|
|
46
|
-
* The description of the error, if available.
|
|
47
|
-
*/
|
|
48
|
-
description?: string;
|
|
49
|
-
},
|
|
50
|
-
options?: ErrorOptions,
|
|
51
|
-
) {
|
|
52
|
-
super(details.description ?? details.title, options);
|
|
53
|
-
this.#details = details;
|
|
54
|
-
this.name = 'FusionContextSearchError';
|
|
55
|
-
}
|
|
56
|
-
}
|
package/src/errors/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './FusionContextSearchError';
|
package/src/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Context module for the Fusion Framework.
|
|
3
|
-
*
|
|
4
|
-
* Provides context management for Fusion-based applications and portals,
|
|
5
|
-
* including setting, querying, validating, and resolving context items.
|
|
6
|
-
*
|
|
7
|
-
* Use {@link enableContext} to register the module in a configurator,
|
|
8
|
-
* then access the {@link IContextProvider} from the module instance
|
|
9
|
-
* to interact with context state.
|
|
10
|
-
*
|
|
11
|
-
* @packageDocumentation
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export { ContextModuleConfigurator } from './ContextModuleConfigurator';
|
|
15
|
-
export type { IContextModuleConfigurator } from './ContextModuleConfigurator.interface';
|
|
16
|
-
export type { ContextModuleConfig } from './ContextModuleConfig';
|
|
17
|
-
|
|
18
|
-
export { IContextProvider, ContextProvider } from './ContextProvider';
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
default,
|
|
22
|
-
ContextModule,
|
|
23
|
-
module as contextModule,
|
|
24
|
-
moduleKey as contextModuleKey,
|
|
25
|
-
} from './module';
|
|
26
|
-
|
|
27
|
-
export { enableContext } from './utils/enable-context';
|
|
28
|
-
|
|
29
|
-
export * from './types';
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { EMPTY, from, lastValueFrom, of, throwError } from 'rxjs';
|
|
2
|
-
|
|
3
|
-
import type { ModuleInitializerArgs } from '@equinor/fusion-framework-module';
|
|
4
|
-
import type { NavigationModule } from '@equinor/fusion-framework-module-navigation';
|
|
5
|
-
import type { ServicesModule } from '@equinor/fusion-framework-module-services';
|
|
6
|
-
|
|
7
|
-
import { ContextModuleConfigurator } from '../ContextModuleConfigurator';
|
|
8
|
-
import type { ContextModuleConfig } from '../ContextModuleConfig';
|
|
9
|
-
import type { IContextModuleConfigurator } from '../ContextModuleConfigurator.interface';
|
|
10
|
-
import type { ContextItem } from '../types';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Resolves a context item by id, or `undefined` if none matches.
|
|
14
|
-
*
|
|
15
|
-
* @remarks
|
|
16
|
-
* The escape hatch for {@link ContextMockConfigurator.setResolver} — anything
|
|
17
|
-
* the friendly seeding methods did not anticipate can be expressed here instead.
|
|
18
|
-
*/
|
|
19
|
-
export type ContextResolverFn = (id: string) => ContextItem | undefined;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A {@link ContextModuleConfigurator} backed by in-memory context items instead
|
|
23
|
-
* of a real context API, for seeding context in tests.
|
|
24
|
-
*
|
|
25
|
-
* @remarks
|
|
26
|
-
* Two layers cover different needs, both running through the real
|
|
27
|
-
* `ContextProvider` logic underneath — only the data source is substituted,
|
|
28
|
-
* never `validateContext`, `resolveContext`, or parent-context propagation:
|
|
29
|
-
*
|
|
30
|
-
* - **Friendly layer** — {@link setCurrentContext}, {@link setContexts},
|
|
31
|
-
* {@link addContext}, {@link setRelatedContexts} — a small, context-domain
|
|
32
|
-
* vocabulary for the common case: seed a known item, get it back.
|
|
33
|
-
* - **Escape hatch** — {@link setResolver} — a raw id-lookup function for a
|
|
34
|
-
* custom id-based resolution strategy or a shape the friendly layer did not
|
|
35
|
-
* cover. Only replaces id-based lookup (used by `setCurrentContextById` and
|
|
36
|
-
* the initial context); it does not affect related-context resolution by
|
|
37
|
-
* item, which always goes through {@link setRelatedContexts} instead.
|
|
38
|
-
*
|
|
39
|
-
* Seeding the initial context (via {@link setCurrentContext}) overrides
|
|
40
|
-
* `resolveInitialContext` directly with the seeded item, so a test never needs
|
|
41
|
-
* to construct a fake navigation module or parent framework instance to make an
|
|
42
|
-
* app start up with a known context selected. Resolving a context id from a URL
|
|
43
|
-
* path on startup is not covered — that needs a fake router, which is separate,
|
|
44
|
-
* not-yet-built work.
|
|
45
|
-
*
|
|
46
|
-
* Seeded ids are never generated for you — every item is looked up and
|
|
47
|
-
* returned exactly by the id it was seeded with, so a test never sees a
|
|
48
|
-
* different id than the one it wrote. Use `createContextItemFactory` or
|
|
49
|
-
* `createContextItems` for fixtures needing ids of their own, rather than a
|
|
50
|
-
* random generator that would change on every run.
|
|
51
|
-
*
|
|
52
|
-
* Related-context resolution defaults to the same seeded pool, filtered to
|
|
53
|
-
* whichever type(s) the query asked for — related context is the same
|
|
54
|
-
* context, just a different type, exactly as `ContextProvider.resolveContext`
|
|
55
|
-
* uses it to resolve an item of an unexpected type into one of the configured
|
|
56
|
-
* type. Seed items of both types and resolution works with no extra wiring;
|
|
57
|
-
* {@link setRelatedContexts} overrides that default for one specific item.
|
|
58
|
-
*
|
|
59
|
-
* @example Seed and select a known context item
|
|
60
|
-
* ```ts
|
|
61
|
-
* enableContextMock(configurator, (mock) => {
|
|
62
|
-
* mock.setCurrentContext({ id: 'my-ctx', type: { id: 'ProjectMaster' }, value: {} });
|
|
63
|
-
* });
|
|
64
|
-
* ```
|
|
65
|
-
*
|
|
66
|
-
* @example Resolve a child-typed item into its parent type
|
|
67
|
-
* ```ts
|
|
68
|
-
* enableContextMock(configurator, (mock) => {
|
|
69
|
-
* // no explicit wiring needed — relatedContexts filters this same pool by type
|
|
70
|
-
* mock.setContexts([project, facility]);
|
|
71
|
-
* });
|
|
72
|
-
* ```
|
|
73
|
-
*
|
|
74
|
-
* @example Override related contexts for one specific item
|
|
75
|
-
* ```ts
|
|
76
|
-
* enableContextMock(configurator, (mock) => {
|
|
77
|
-
* mock.addContext(project);
|
|
78
|
-
* mock.setRelatedContexts(project.id, [facilityA, facilityB]);
|
|
79
|
-
* });
|
|
80
|
-
* ```
|
|
81
|
-
*
|
|
82
|
-
* @example Escape hatch for a custom resolution need
|
|
83
|
-
* ```ts
|
|
84
|
-
* enableContextMock(configurator, (mock) => {
|
|
85
|
-
* mock.setResolver((id) => (id === 'special' ? specialContextItem : undefined));
|
|
86
|
-
* });
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
export class ContextMockConfigurator extends ContextModuleConfigurator {
|
|
90
|
-
#contexts = new Map<string, ContextItem>();
|
|
91
|
-
#related = new Map<string, ContextItem[]>();
|
|
92
|
-
#resolver?: ContextResolverFn;
|
|
93
|
-
#pendingCurrentId?: string;
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Registers the in-memory client up front, so `createConfig` never falls
|
|
97
|
-
* back to building one from a real `ServicesModule` — a mock needs neither
|
|
98
|
-
* an API provider nor a network to answer `get`/`query`/`related`.
|
|
99
|
-
*/
|
|
100
|
-
constructor() {
|
|
101
|
-
super();
|
|
102
|
-
this.addConfigBuilder((builder) => {
|
|
103
|
-
builder.setContextClient({
|
|
104
|
-
get: (args) => {
|
|
105
|
-
const item = this.#resolve(args.id);
|
|
106
|
-
// a caller asking for an unseeded id gets a clear error, not a silent miss
|
|
107
|
-
return item
|
|
108
|
-
? of(item)
|
|
109
|
-
: throwError(
|
|
110
|
-
() =>
|
|
111
|
-
new Error(
|
|
112
|
-
`ContextMockConfigurator: no context item resolves for id "${args.id}" — seed it with setCurrentContext/setContexts/addContext, or provide setResolver.`,
|
|
113
|
-
),
|
|
114
|
-
);
|
|
115
|
-
},
|
|
116
|
-
query: () => of([...this.#contexts.values()]),
|
|
117
|
-
related: (args) => {
|
|
118
|
-
const override = this.#related.get(args.item.id);
|
|
119
|
-
// an explicit override for this exact item always wins
|
|
120
|
-
if (override) return of(override);
|
|
121
|
-
const types = args.filter?.type;
|
|
122
|
-
// otherwise: related context is the same seeded pool, filtered to the requested type(s)
|
|
123
|
-
return of(
|
|
124
|
-
[...this.#contexts.values()].filter(
|
|
125
|
-
(item) => item.id !== args.item.id && (!types || types.includes(item.type.id)),
|
|
126
|
-
),
|
|
127
|
-
);
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Seeds a context item and selects it as the context the app resolves on startup.
|
|
135
|
-
*
|
|
136
|
-
* @param item - The context item to seed and select.
|
|
137
|
-
* @returns This configurator, for chaining.
|
|
138
|
-
*/
|
|
139
|
-
public setCurrentContext(item: ContextItem): this {
|
|
140
|
-
this.#contexts.set(item.id, item);
|
|
141
|
-
this.#pendingCurrentId = item.id;
|
|
142
|
-
return this;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Seeds multiple context items, making each resolvable by id.
|
|
147
|
-
*
|
|
148
|
-
* @remarks
|
|
149
|
-
* Does not select any of them as current — pair with {@link setCurrentContext}
|
|
150
|
-
* for that.
|
|
151
|
-
*
|
|
152
|
-
* @param items - The context items to seed.
|
|
153
|
-
* @returns This configurator, for chaining.
|
|
154
|
-
*/
|
|
155
|
-
public setContexts(items: ContextItem[]): this {
|
|
156
|
-
// seed every item so each is individually resolvable by id
|
|
157
|
-
for (const item of items) this.#contexts.set(item.id, item);
|
|
158
|
-
return this;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Seeds a single context item, making it resolvable by id.
|
|
163
|
-
*
|
|
164
|
-
* @param item - The context item to seed.
|
|
165
|
-
* @returns This configurator, for chaining.
|
|
166
|
-
*/
|
|
167
|
-
public addContext(item: ContextItem): this {
|
|
168
|
-
this.#contexts.set(item.id, item);
|
|
169
|
-
return this;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Overrides related-context resolution for one specific source item.
|
|
174
|
-
*
|
|
175
|
-
* @remarks
|
|
176
|
-
* Without this, `relatedContexts` filters the seeded pool by the requested
|
|
177
|
-
* type(s) — this is only needed when a test wants a specific item to resolve
|
|
178
|
-
* something other than that default (e.g. no related items at all).
|
|
179
|
-
*
|
|
180
|
-
* @param itemId - The id of the item relations are being overridden for.
|
|
181
|
-
* @param items - The context items to return from `relatedContexts` for that item.
|
|
182
|
-
* @returns This configurator, for chaining.
|
|
183
|
-
*/
|
|
184
|
-
public setRelatedContexts(itemId: string, items: ContextItem[]): this {
|
|
185
|
-
this.#related.set(itemId, items);
|
|
186
|
-
return this;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Escape hatch: overrides id-based context resolution directly.
|
|
191
|
-
*
|
|
192
|
-
* @remarks
|
|
193
|
-
* For a custom id-lookup strategy or a shape the friendly seeding methods
|
|
194
|
-
* above did not cover — reaching for this means they were already tried
|
|
195
|
-
* and did not fit. Only replaces lookup by id (used by
|
|
196
|
-
* `setCurrentContextById` and the seeded initial context) — related-context
|
|
197
|
-
* resolution by item always goes through {@link setRelatedContexts} instead,
|
|
198
|
-
* regardless of this setting.
|
|
199
|
-
*
|
|
200
|
-
* @param fn - Resolves a context item by id, or returns `undefined` if none matches.
|
|
201
|
-
* @returns This configurator, for chaining.
|
|
202
|
-
*/
|
|
203
|
-
public setResolver(fn: ContextResolverFn): this {
|
|
204
|
-
this.#resolver = fn;
|
|
205
|
-
return this;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Resolves an id through the escape hatch if one is set, otherwise the seeded map.
|
|
210
|
-
*
|
|
211
|
-
* @param id - The context id to resolve.
|
|
212
|
-
* @returns The matching context item, or `undefined`.
|
|
213
|
-
*/
|
|
214
|
-
#resolve(id: string): ContextItem | undefined {
|
|
215
|
-
// the escape hatch, when set, replaces the seeded map entirely
|
|
216
|
-
return this.#resolver ? this.#resolver(id) : this.#contexts.get(id);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Defaults `resolveInitialContext` from the seeded/resolved state, once the
|
|
221
|
-
* real config has been assembled.
|
|
222
|
-
*
|
|
223
|
-
* @param config - The context module config assembled by the base configurator.
|
|
224
|
-
* @param init - Module initializer arguments.
|
|
225
|
-
* @returns The context module configuration, backed by seeded/resolved data.
|
|
226
|
-
*/
|
|
227
|
-
protected override async _processConfig(
|
|
228
|
-
config: Partial<ContextModuleConfig>,
|
|
229
|
-
init: ModuleInitializerArgs<IContextModuleConfigurator, [ServicesModule, NavigationModule]>,
|
|
230
|
-
): Promise<ContextModuleConfig> {
|
|
231
|
-
// the base default is a plain Observable (`of(...)`), not a thenable — unwrap it explicitly
|
|
232
|
-
const resolved = await lastValueFrom(from(super._processConfig(config, init)));
|
|
233
|
-
const id = this.#pendingCurrentId;
|
|
234
|
-
|
|
235
|
-
// The base default resolves from the URL path or a parent framework, and
|
|
236
|
-
// throws when neither is present — the common case for an isolated mock.
|
|
237
|
-
// Resolve the seeded item when one was selected, otherwise resolve none.
|
|
238
|
-
resolved.resolveInitialContext = id ? () => of(this.#resolve(id)) : () => EMPTY;
|
|
239
|
-
|
|
240
|
-
return resolved;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export default ContextMockConfigurator;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Faker, en } from '@faker-js/faker';
|
|
2
|
-
|
|
3
|
-
import type { ContextItem } from '../../types';
|
|
4
|
-
import { stringToSeed } from './string-to-seed';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Creates one {@link ContextItem} fixture per call, for a single factory instance.
|
|
8
|
-
*
|
|
9
|
-
* @param overrides - Fields to override on the generated item. Supplying `id`
|
|
10
|
-
* skips the sequential generator for that one item.
|
|
11
|
-
* @returns A fully-formed {@link ContextItem}.
|
|
12
|
-
*/
|
|
13
|
-
export type MockContextItemFactory = (overrides?: Partial<ContextItem>) => ContextItem;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Builds a {@link MockContextItemFactory} producing deterministic, sequential
|
|
17
|
-
* ids of the form `<prefix>-<n>`, with a realistic `title` filled in by
|
|
18
|
-
* {@link https://fakerjs.dev/ | faker} (`@faker-js/faker`, a peer dependency —
|
|
19
|
-
* only required if this factory is imported).
|
|
20
|
-
*
|
|
21
|
-
* @remarks
|
|
22
|
-
* Ids are scoped to the returned factory, not shared globally, so two tests
|
|
23
|
-
* creating their own factory each start counting from `1` — a random id (e.g.
|
|
24
|
-
* `crypto.randomUUID()`) would change on every run and break any assertion
|
|
25
|
-
* that seeds one item and expects to see that same id again. The faker seed
|
|
26
|
-
* is derived from that same id, so the generated `title` is just as
|
|
27
|
-
* deterministic. For fixtures spanning several context types (e.g. a
|
|
28
|
-
* parent/child type hierarchy), use `createContextItems` instead — it
|
|
29
|
-
* assigns ids per type and wires the type hierarchy for you.
|
|
30
|
-
*
|
|
31
|
-
* @param prefix - The id prefix. Defaults to `'ctx'`.
|
|
32
|
-
* @returns A function creating one {@link ContextItem} fixture per call.
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```ts
|
|
36
|
-
* const createContextItem = createContextItemFactory();
|
|
37
|
-
*
|
|
38
|
-
* const project = createContextItem(); // id: 'ctx-1', title: a deterministic faker company name
|
|
39
|
-
* const facility = createContextItem({ title: 'Facility A' }); // id: 'ctx-2', title overridden
|
|
40
|
-
*
|
|
41
|
-
* mock.setContexts([project, facility]);
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export const createContextItemFactory = (prefix = 'ctx'): MockContextItemFactory => {
|
|
45
|
-
const created: ContextItem[] = [];
|
|
46
|
-
return (overrides: Partial<ContextItem> = {}): ContextItem => {
|
|
47
|
-
// overrides.id, when supplied, always wins over the sequential generator
|
|
48
|
-
const id = overrides.id ?? `${prefix}-${created.length + 1}`;
|
|
49
|
-
const faker = new Faker({ seed: stringToSeed(id), locale: en });
|
|
50
|
-
const item: ContextItem = {
|
|
51
|
-
type: { id: 'Mock' },
|
|
52
|
-
value: {},
|
|
53
|
-
title: faker.company.name(),
|
|
54
|
-
...overrides,
|
|
55
|
-
id,
|
|
56
|
-
};
|
|
57
|
-
created.push(item);
|
|
58
|
-
return item;
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export default createContextItemFactory;
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Faker, en } from '@faker-js/faker';
|
|
2
|
-
|
|
3
|
-
import type { ContextItem } from '../../types';
|
|
4
|
-
import { stringToSeed } from './string-to-seed';
|
|
5
|
-
|
|
6
|
-
/** Overridable {@link ContextItem} fields. `id` and `type` are always assigned by the type seed itself. */
|
|
7
|
-
export type ContextItemOverrides = Partial<Omit<ContextItem, 'id' | 'type'>>;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Describes one context type to generate fixtures for.
|
|
11
|
-
*
|
|
12
|
-
* @param type - The context type id (e.g. `'ProjectMaster'`, `'Contract'`).
|
|
13
|
-
* @param count - Number of items to generate for this type. Defaults to `1`.
|
|
14
|
-
* @param parentTypeIds - Parent type ids, when this type is a child in the
|
|
15
|
-
* type hierarchy — sets `type.isChildType`/`type.parentTypeIds` on every
|
|
16
|
-
* generated item of this type.
|
|
17
|
-
* @param item - Per-item overrides, called with the item's 1-based index
|
|
18
|
-
* within this type.
|
|
19
|
-
*/
|
|
20
|
-
export interface ContextTypeSeed {
|
|
21
|
-
type: string;
|
|
22
|
-
count?: number;
|
|
23
|
-
parentTypeIds?: string[];
|
|
24
|
-
item?: (index: number) => ContextItemOverrides;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Generates a batch of {@link ContextItem} fixtures across one or more
|
|
29
|
-
* context types, with deterministic ids, a realistic `title` from
|
|
30
|
-
* {@link https://fakerjs.dev/ | faker} (`@faker-js/faker`, a peer dependency —
|
|
31
|
-
* only required if this factory is imported), and a consistent type hierarchy.
|
|
32
|
-
*
|
|
33
|
-
* @remarks
|
|
34
|
-
* Ids are `<type>-<n>` (lowercased type, 1-based per type), so results are
|
|
35
|
-
* stable across runs and readable in test failures. `title` is filled in from
|
|
36
|
-
* a faker instance seeded by that same id, so it's just as deterministic.
|
|
37
|
-
* Feeding the result to `ContextMockConfigurator.setContexts` is enough to
|
|
38
|
-
* make related-context resolution work across types — the mock's
|
|
39
|
-
* `relatedContexts` filters this same seeded pool by type, so a child-typed
|
|
40
|
-
* item resolves into a parent of its `parentTypeIds` without any per-item
|
|
41
|
-
* wiring. That default is a simplification: the real context API resolves
|
|
42
|
-
* relations per specific instance (a `Contract` belongs to one particular
|
|
43
|
-
* `ProjectMaster`, not every `ProjectMaster` in the system) — seed only one
|
|
44
|
-
* instance per type when that distinction doesn't matter to the test, or use
|
|
45
|
-
* `ContextMockConfigurator.setRelatedContexts` to pin a specific item's
|
|
46
|
-
* relations when it does.
|
|
47
|
-
*
|
|
48
|
-
* @param types - The context types, and how many items to generate for each.
|
|
49
|
-
* @returns The generated context items, in the order the type seeds were given.
|
|
50
|
-
*
|
|
51
|
-
* @example Seed a project type and a child contract type
|
|
52
|
-
* ```ts
|
|
53
|
-
* const [project] = createContextItems([{ type: 'ProjectMaster' }]);
|
|
54
|
-
* const [contract] = createContextItems([
|
|
55
|
-
* { type: 'Contract', parentTypeIds: ['ProjectMaster'] },
|
|
56
|
-
* ]);
|
|
57
|
-
*
|
|
58
|
-
* enableContextMock(configurator, (mock) => {
|
|
59
|
-
* mock.setContexts([project, contract]);
|
|
60
|
-
* // relatedContexts({ item: contract, filter: { type: ['ProjectMaster'] } }) now resolves `project`
|
|
61
|
-
* });
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
export const createContextItems = (types: ContextTypeSeed[]): ContextItem[] =>
|
|
65
|
-
types.flatMap(({ type, count = 1, parentTypeIds, item }) =>
|
|
66
|
-
Array.from({ length: count }, (_, i): ContextItem => {
|
|
67
|
-
const index = i + 1;
|
|
68
|
-
const id = `${type.toLowerCase()}-${index}`;
|
|
69
|
-
const faker = new Faker({ seed: stringToSeed(id), locale: en });
|
|
70
|
-
return {
|
|
71
|
-
value: {},
|
|
72
|
-
title: faker.company.name(),
|
|
73
|
-
...item?.(index),
|
|
74
|
-
id,
|
|
75
|
-
type: parentTypeIds ? { id: type, isChildType: true, parentTypeIds } : { id: type },
|
|
76
|
-
};
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
export default createContextItems;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fixture generators for {@link ContextMockConfigurator} test data.
|
|
3
|
-
*
|
|
4
|
-
* @remarks
|
|
5
|
-
* A separate entry point from `@equinor/fusion-framework-module-context/mock`
|
|
6
|
-
* because these factories use {@link https://fakerjs.dev/ | faker}
|
|
7
|
-
* (`@faker-js/faker`) for realistic values — an optional peer dependency, only
|
|
8
|
-
* required if this entry point is imported. `enableContextMock` and
|
|
9
|
-
* `ContextMockConfigurator` on the main `/mock` entry point never require it.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```typescript
|
|
13
|
-
* import { createContextItems } from '@equinor/fusion-framework-module-context/mock/fixtures';
|
|
14
|
-
*
|
|
15
|
-
* const [project] = createContextItems([{ type: 'ProjectMaster' }]);
|
|
16
|
-
* ```
|
|
17
|
-
*
|
|
18
|
-
* @packageDocumentation
|
|
19
|
-
*/
|
|
20
|
-
export {
|
|
21
|
-
createContextItemFactory,
|
|
22
|
-
type MockContextItemFactory,
|
|
23
|
-
} from './create-context-item-factory';
|
|
24
|
-
export {
|
|
25
|
-
createContextItems,
|
|
26
|
-
type ContextTypeSeed,
|
|
27
|
-
type ContextItemOverrides,
|
|
28
|
-
} from './create-context-items';
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Derives a numeric seed from a string, for a deterministic {@link https://fakerjs.dev/ | faker} instance.
|
|
3
|
-
*
|
|
4
|
-
* @remarks
|
|
5
|
-
* A pure-JS hash (no `node:crypto`) so fixtures stay usable in browser-like test
|
|
6
|
-
* environments, not just Node. Same id in, same seed out, every run.
|
|
7
|
-
*
|
|
8
|
-
* @param value - The string to derive a seed from (typically a generated item id).
|
|
9
|
-
* @returns A 32-bit unsigned integer seed.
|
|
10
|
-
*/
|
|
11
|
-
export const stringToSeed = (value: string): number =>
|
|
12
|
-
// FNV-1a-style hash: fold each char in, keep the running hash unsigned 32-bit
|
|
13
|
-
[...value].reduce(
|
|
14
|
-
(hash, char) => Math.imul(hash ^ char.charCodeAt(0), 16777619) >>> 0,
|
|
15
|
-
2166136261,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
export default stringToSeed;
|