@equinor/fusion-framework-module-context 7.0.4-next.0 → 8.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/CHANGELOG.md +36 -15
- package/README.md +168 -16
- package/dist/esm/ContextConfigBuilder.js +24 -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 +40 -5
- 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/index.js +12 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/module.js +6 -0
- package/dist/esm/module.js.map +1 -1
- 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 +25 -0
- package/dist/esm/utils/extract-context-id-from-path.js.map +1 -0
- package/dist/esm/utils/index.js +10 -0
- package/dist/esm/utils/index.js.map +1 -1
- 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 +2 -0
- package/dist/esm/utils/resolve-initial-context.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ContextConfigBuilder.d.ts +31 -1
- package/dist/types/ContextProvider.d.ts +74 -3
- package/dist/types/client/ContextClient.d.ts +8 -1
- package/dist/types/configurator.d.ts +134 -4
- 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/index.d.ts +12 -0
- package/dist/types/module.d.ts +11 -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 +23 -1
- package/dist/types/utils/extract-context-id-from-path.d.ts +16 -0
- package/dist/types/utils/index.d.ts +10 -0
- package/dist/types/utils/resolve-context-from-path.d.ts +1 -16
- package/dist/types/utils/resolve-initial-context.d.ts +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +12 -12
- package/src/ContextConfigBuilder.ts +47 -7
- package/src/ContextProvider.ts +115 -28
- package/src/client/ContextClient.ts +13 -3
- package/src/configurator.ts +152 -10
- 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/index.ts +13 -0
- package/src/module.ts +11 -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 +23 -1
- package/src/utils/extract-context-id-from-path.ts +30 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/resolve-context-from-path.ts +13 -28
- package/src/utils/resolve-initial-context.ts +2 -0
- 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/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,45 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 8.0.1
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- 80c3e4a: Internal: resolve `noExplicitAny`/`noConfusingVoidType` Biome warnings in `ContextConfigBuilder` and `ContextModuleConfigurator`'s `resolveInitialContext` callback type, using explanatory `biome-ignore` comments for load-bearing `any`/`void` usages. No public API or behavior change.
|
|
8
|
+
- 80c3e4a: Internal: move `FusionContextSearchError` into `src/errors/FusionContextSearchError.ts` with an `errors/index.ts` barrel, resolving a `filename-convention` lint violation; no public API changes.
|
|
9
|
+
- 80c3e4a: Internal: add a required fusion-lint intent comment explaining an `as unknown as T` cast in `ContextProvider`. No behavior change.
|
|
10
|
+
- 80c3e4a: Resolve fusion-lint warnings: add missing TSDoc (constructors, getters, setters, overload implementations, `@throws`/`@template` tags), add intent comments above control-flow and RxJS/iterator chains, and split multi-export files to satisfy `single-export-per-file`.
|
|
11
|
+
- Split `selectors.ts` into `query-context-selector.ts` and `related-context-selector.ts` (re-exported from `selectors.ts`), and extracted `parseContextItem` into `parse-context-item.ts`.
|
|
12
|
+
- Split `extractContextIdFromPath` out of `utils/resolve-context-from-path.ts` into `utils/extract-context-id-from-path.ts` (re-exported from the original file).
|
|
13
|
+
- Deferred work items are now tracked as GitHub issues #5115–#5122.
|
|
8
14
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
- 80c3e4a: Internal: renamed 45 source files across these packages to comply with the `filename-convention` lint rule (e.g. `AIConfigurator.ts` → `AiConfigurator.ts`, `BookmarkProvider.actions.ts` → `bookmark-actions.ts`, `errors/app-build-error.ts` → `errors/AppBuildError.ts`, `plugins/api/plugin.ts` → `plugins/api/ApiPlugin.ts`, `errors.ts` → `UnsupportedApiVersion.ts`, etc.). Also added `enable-signalr.ts` to the `filename-convention` exclude list since the suggested rename would incorrectly split the "SignalR" brand name. No public API changes.
|
|
16
|
+
- 80c3e4a: Internal: added missing intent comments ahead of non-obvious control flow, RxJS `.pipe()` chains, iterator calls, and multi-source object merges to comply with the `require-intent-comment` and `require-tsdoc` lint rules. Also removed dead duplicate files left over from an earlier refactor in `navigation` (`events.ts`, `navigated-event.ts`, `history.flows.ts` — all fully superseded by their split replacements) and renamed `bookmarks/schemas.ts` to `bookmarks/bookmark.schemas.ts` in `services` to match the `*.schemas.ts` filename convention. No public API changes.
|
|
17
|
+
|
|
18
|
+
## 8.0.0
|
|
19
|
+
|
|
20
|
+
### Major Changes
|
|
21
|
+
|
|
22
|
+
- abffa53: Major version bump for Fusion Framework React 19 release.
|
|
23
|
+
|
|
24
|
+
All packages are bumped to the next major version as part of the React 19 upgrade. This release drops support for React versions below 18 and includes breaking changes across the framework.
|
|
25
|
+
|
|
26
|
+
**Breaking changes:**
|
|
27
|
+
- Peer dependencies now require React 18 or 19 (`^18.0.0 || ^19.0.0`)
|
|
28
|
+
- React Router upgraded from v6 to v7
|
|
29
|
+
- Navigation module refactored with new history API
|
|
30
|
+
- `renderComponent` and `renderApp` now use `createRoot` API
|
|
31
|
+
|
|
32
|
+
**Migration:**
|
|
33
|
+
- Update your React version to 18.0.0 or higher before upgrading
|
|
34
|
+
- Replace `NavigationProvider.createRouter()` with `@equinor/fusion-framework-react-router`
|
|
35
|
+
- See individual package changelogs for package-specific migration steps
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- Updated dependencies [abffa53]
|
|
40
|
+
- Updated dependencies [abffa53]
|
|
41
|
+
- @equinor/fusion-framework-module@6.0.0
|
|
42
|
+
- @equinor/fusion-query@7.0.0
|
|
12
43
|
|
|
13
44
|
## 7.0.3
|
|
14
45
|
|
|
@@ -20,16 +51,6 @@
|
|
|
20
51
|
- @equinor/fusion-framework-module@5.0.6
|
|
21
52
|
- @equinor/fusion-query@6.0.4
|
|
22
53
|
|
|
23
|
-
## 7.0.3-next.0
|
|
24
|
-
|
|
25
|
-
### Patch Changes
|
|
26
|
-
|
|
27
|
-
- [#3820](https://github.com/equinor/fusion-framework/pull/3820) [`265bb76`](https://github.com/equinor/fusion-framework/commit/265bb767249989eeb1971e83f3fba94879e0813b) Thanks [@odinr](https://github.com/odinr)! - relase next
|
|
28
|
-
|
|
29
|
-
- Updated dependencies [[`265bb76`](https://github.com/equinor/fusion-framework/commit/265bb767249989eeb1971e83f3fba94879e0813b), [`75c068f`](https://github.com/equinor/fusion-framework/commit/75c068fea13c32435ac26bd9043cc156482bfaf1)]:
|
|
30
|
-
- @equinor/fusion-query@7.0.0-next.0
|
|
31
|
-
- @equinor/fusion-framework-module@5.0.6-next.0
|
|
32
|
-
|
|
33
54
|
## 7.0.2
|
|
34
55
|
|
|
35
56
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,33 +1,185 @@
|
|
|
1
|
-
|
|
1
|
+
# @equinor/fusion-framework-module-context
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
It provides a structured way to handle context information to enrich user experiences and facilitate data sharing across different components of the application.
|
|
3
|
+
Context module for the Fusion Framework. Manages the active context (project, facility, contract, etc.) within Fusion-based applications and portals, providing query, validation, resolution, and parent–child synchronization out of the box.
|
|
5
4
|
|
|
6
|
-
##
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
Use this module when your application or portal needs to:
|
|
8
|
+
|
|
9
|
+
- set, read, or clear the active context (e.g. selecting a project)
|
|
10
|
+
- search and filter available context items from the Fusion context API
|
|
11
|
+
- validate whether a given context item matches the allowed context types
|
|
12
|
+
- resolve context across parent–child provider boundaries (portal → app)
|
|
13
|
+
- deep-link by extracting a context ID from the URL path
|
|
14
|
+
|
|
15
|
+
> **Application developers** will typically use the higher-level
|
|
16
|
+
> `@equinor/fusion-framework-react-app/context` package, which wraps this
|
|
17
|
+
> module with React hooks and providers.
|
|
18
|
+
>
|
|
19
|
+
> **Portal developers** will use `@equinor/fusion-framework-react/context`
|
|
20
|
+
> for the same purpose at the portal level.
|
|
21
|
+
|
|
22
|
+
## Key concepts
|
|
23
|
+
|
|
24
|
+
| Concept | Description |
|
|
25
|
+
|---|---|
|
|
26
|
+
| **ContextItem** | A typed record representing a single context entity (project, facility, etc.). |
|
|
27
|
+
| **ContextProvider** | Runtime service exposing query, set, validate, resolve, and event APIs. |
|
|
28
|
+
| **ContextConfigBuilder** | Fluent builder for configuring context types, filters, clients, and hooks. |
|
|
29
|
+
| **enableContext** | Helper that registers the module on a modules configurator. |
|
|
30
|
+
| **Context resolution** | Automatic lookup of related context items when a context type does not match the configured types. |
|
|
31
|
+
| **Parent connection** | Bi-directional sync between a parent portal context and a child app context. |
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
7
34
|
|
|
8
35
|
```ts
|
|
9
36
|
import { enableContext } from '@equinor/fusion-framework-module-context';
|
|
10
37
|
|
|
11
|
-
const configure = (configurator
|
|
38
|
+
export const configure = (configurator) => {
|
|
12
39
|
enableContext(configurator, (builder) => {
|
|
13
|
-
//
|
|
40
|
+
// only accept ProjectMaster context items
|
|
41
|
+
builder.setContextType(['ProjectMaster']);
|
|
14
42
|
});
|
|
15
43
|
};
|
|
16
44
|
```
|
|
17
45
|
|
|
18
|
-
|
|
46
|
+
Once initialized, access the provider from the module instance:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// observe context changes
|
|
50
|
+
modules.context.currentContext$.subscribe((ctx) => {
|
|
51
|
+
console.log('context changed', ctx);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// search for context items
|
|
55
|
+
const items = await modules.context.queryContextAsync('Johan');
|
|
56
|
+
|
|
57
|
+
// set context by item
|
|
58
|
+
await modules.context.setCurrentContextAsync(items[0]);
|
|
59
|
+
|
|
60
|
+
// set context by ID
|
|
61
|
+
await modules.context.setCurrentContextByIdAsync('7fd97952-...');
|
|
62
|
+
|
|
63
|
+
// clear the active context
|
|
64
|
+
modules.context.clearCurrentContext();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
All configuration flows through `enableContext` → `ContextConfigBuilder`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
enableContext(configurator, (builder) => {
|
|
73
|
+
// restrict accepted context types
|
|
74
|
+
builder.setContextType(['ProjectMaster', 'Facility']);
|
|
75
|
+
|
|
76
|
+
// post-query filter
|
|
77
|
+
builder.setContextFilter((items) => items.filter((i) => i.isActive));
|
|
78
|
+
|
|
79
|
+
// custom parameter mapping for the search API
|
|
80
|
+
builder.setContextParameterFn(({ search, type }) => ({
|
|
81
|
+
search,
|
|
82
|
+
filter: { type },
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
// custom validation logic
|
|
86
|
+
builder.setValidateContext(function (item) {
|
|
87
|
+
return item !== null && this.validateContext(item);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// custom context resolution
|
|
91
|
+
builder.setResolveContext(function (item) {
|
|
92
|
+
return this.relatedContexts({ item, filter: { type: ['ProjectMaster'] } });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// connect (or disconnect) from parent context
|
|
96
|
+
// when enabled (default), onParentContextChanged fires before mirroring the parent's context locally
|
|
97
|
+
builder.connectParentContext(false);
|
|
98
|
+
|
|
99
|
+
// path ↔ context integration
|
|
100
|
+
// the default resolver uses extractContextIdFromPath to pull a GUID from the URL,
|
|
101
|
+
// then fetches the matching context item — see resolveInitialContext in utils
|
|
102
|
+
builder.setContextPathExtractor((path) => path.split('/')[2]);
|
|
103
|
+
builder.setContextPathGenerator((ctx, path) =>
|
|
104
|
+
path.replace(/\/context\/[^/]+/, `/context/${ctx.id}`),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// provide a fully custom context client
|
|
108
|
+
builder.setContextClient({
|
|
109
|
+
get: (args) => fetch(`/api/context/${args.id}`).then((r) => r.json()),
|
|
110
|
+
query: (args) => fetch(`/api/context?q=${args.search}`).then((r) => r.json()),
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Configuration reference
|
|
116
|
+
|
|
117
|
+
| Builder method | Purpose |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `setContextType(types)` | Allowed context type IDs for validation |
|
|
120
|
+
| `setContextFilter(fn)` | Post-query result filter |
|
|
121
|
+
| `setContextParameterFn(fn)` | Maps search + type to API query params |
|
|
122
|
+
| `setValidateContext(fn)` | Custom validation (`this` = provider) |
|
|
123
|
+
| `setResolveContext(fn)` | Custom resolution (`this` = provider) |
|
|
124
|
+
| `connectParentContext(bool)` | Enable/disable parent context sync (fires `onParentContextChanged` on change) |
|
|
125
|
+
| `setContextPathExtractor(fn)` | Extract context ID from URL path |
|
|
126
|
+
| `setContextPathGenerator(fn)` | Generate URL path from context item |
|
|
127
|
+
| `setResolveInitialContext(fn)` | Override initial context resolution (default uses `extractContextIdFromPath` → `resolveContextFromPath`) |
|
|
128
|
+
| `setContextClient(client)` | Custom get/query/related clients |
|
|
129
|
+
|
|
130
|
+
## Events
|
|
131
|
+
|
|
132
|
+
The context module dispatches events via the `@equinor/fusion-framework-module-event` system. All events are scoped to the `ContextProvider` source.
|
|
133
|
+
|
|
134
|
+
| Event | When | Cancelable |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `onCurrentContextChange` | Before the current context is updated | Yes |
|
|
137
|
+
| `onCurrentContextChanged` | After the current context has changed | No |
|
|
138
|
+
| `onParentContextChanged` | Before a parent context change is mirrored locally | Yes |
|
|
139
|
+
| `onSetContextResolve` | Before context resolution begins | Yes |
|
|
140
|
+
| `onSetContextResolved` | After context resolution completes | Yes |
|
|
141
|
+
| `onSetContextValidationFailed` | When validation fails (no resolution) | No |
|
|
142
|
+
| `onSetContextResolveFailed` | When resolution fails with an error | No |
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
modules.event.addEventListener('onCurrentContextChanged', (e) => {
|
|
146
|
+
console.log('previous:', e.detail.previous);
|
|
147
|
+
console.log('next:', e.detail.next);
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Errors
|
|
152
|
+
|
|
153
|
+
The module exports `FusionContextSearchError` (from `@equinor/fusion-framework-module-context/errors.js`) for search-related failures:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { FusionContextSearchError } from '@equinor/fusion-framework-module-context/errors.js';
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await modules.context.queryContextAsync('...');
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (err instanceof FusionContextSearchError) {
|
|
162
|
+
console.error(err.title, err.description);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
19
166
|
|
|
20
|
-
|
|
21
|
-
- query context items based on search strings
|
|
22
|
-
- filter context items based on type
|
|
23
|
-
- validate context items against allowed types
|
|
24
|
-
- resolving related context items
|
|
167
|
+
## Utilities
|
|
25
168
|
|
|
26
|
-
|
|
169
|
+
Additional helpers are available from `@equinor/fusion-framework-module-context/utils`:
|
|
27
170
|
|
|
28
|
-
|
|
171
|
+
| Export | Purpose |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `enableContext` | Register the context module on a configurator |
|
|
174
|
+
| `resolveInitialContext` | Default initial-context resolver (path → parent fallback) |
|
|
175
|
+
| `extractContextIdFromPath` | Extract a GUID-format context ID from a URL path |
|
|
176
|
+
| `resolveContextFromPath` | Create a resolver function that fetches a context item from a URL path |
|
|
29
177
|
|
|
30
|
-
|
|
178
|
+
## Package exports
|
|
31
179
|
|
|
32
|
-
|
|
180
|
+
| Specifier | Description |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `@equinor/fusion-framework-module-context` | Main entry — module, provider, configurator, types |
|
|
183
|
+
| `@equinor/fusion-framework-module-context/errors.js` | `FusionContextSearchError` |
|
|
184
|
+
| `@equinor/fusion-framework-module-context/utils` | Utility functions for path resolution and enablement |
|
|
33
185
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* - The builder pattern allows chaining configuration methods for clarity and convenience.
|
|
22
22
|
* - The `requireInstance` method enables asynchronous retrieval of module instances by name.
|
|
23
23
|
*
|
|
24
|
-
* @todo - this should extend the BaseConfigBuilder
|
|
24
|
+
* @todo(#5120) - this should extend the BaseConfigBuilder
|
|
25
25
|
*
|
|
26
26
|
* @see ContextModuleConfig
|
|
27
27
|
* @see ModuleInitializerArgs
|
|
@@ -29,10 +29,20 @@
|
|
|
29
29
|
export class ContextConfigBuilder {
|
|
30
30
|
config;
|
|
31
31
|
#init;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a new `ContextConfigBuilder`.
|
|
34
|
+
* @param init - Module initializer arguments used to resolve module instances.
|
|
35
|
+
* @param config - The partial context module config to build upon.
|
|
36
|
+
*/
|
|
32
37
|
constructor(init, config = {}) {
|
|
33
38
|
this.config = config;
|
|
34
39
|
this.#init = init;
|
|
35
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Requires a module instance by key or name.
|
|
43
|
+
* @param module - The key or name of the module to resolve.
|
|
44
|
+
* @returns A promise that resolves to the requested module instance.
|
|
45
|
+
*/
|
|
36
46
|
requireInstance(module) {
|
|
37
47
|
return this.#init.requireInstance(module);
|
|
38
48
|
}
|
|
@@ -101,6 +111,13 @@ export class ContextConfigBuilder {
|
|
|
101
111
|
setContextPathGenerator(fn) {
|
|
102
112
|
this.config.generatePathFromContext = fn;
|
|
103
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Sets the function used to resolve the initial context during module post-initialization.
|
|
116
|
+
*
|
|
117
|
+
* @param fn - A function that returns an observable input emitting the initial context item.
|
|
118
|
+
* The default resolver extracts a context ID from the navigation path, falling
|
|
119
|
+
* back to the parent provider's current context.
|
|
120
|
+
*/
|
|
104
121
|
setResolveInitialContext(fn) {
|
|
105
122
|
this.config.resolveInitialContext = fn;
|
|
106
123
|
}
|
|
@@ -117,7 +134,7 @@ export class ContextConfigBuilder {
|
|
|
117
134
|
* @param expire - Optional. The expiration time (in milliseconds) for cached query results. Defaults to 1 minute.
|
|
118
135
|
*/
|
|
119
136
|
setContextClient(client, expire = 1 * 60 * 1000) {
|
|
120
|
-
|
|
137
|
+
const clientConfig = {
|
|
121
138
|
get: typeof client.get === 'function'
|
|
122
139
|
? {
|
|
123
140
|
key: ({ id }) => id,
|
|
@@ -129,7 +146,7 @@ export class ContextConfigBuilder {
|
|
|
129
146
|
: client.get,
|
|
130
147
|
query: typeof client.query === 'function'
|
|
131
148
|
? {
|
|
132
|
-
// TODO - might cast to checksum
|
|
149
|
+
// TODO(#5118) - might cast to checksum
|
|
133
150
|
key: (args) => JSON.stringify(args),
|
|
134
151
|
client: {
|
|
135
152
|
fn: client.query,
|
|
@@ -138,12 +155,13 @@ export class ContextConfigBuilder {
|
|
|
138
155
|
}
|
|
139
156
|
: client.query,
|
|
140
157
|
};
|
|
158
|
+
this.config.client = clientConfig;
|
|
159
|
+
// only override the related-context client config if one was provided
|
|
141
160
|
if (client.related) {
|
|
142
|
-
|
|
143
|
-
this.config.client.related =
|
|
161
|
+
clientConfig.related =
|
|
144
162
|
typeof client.related === 'function'
|
|
145
163
|
? {
|
|
146
|
-
// TODO - might cast to checksum
|
|
164
|
+
// TODO(#5118) - might cast to checksum
|
|
147
165
|
key: (args) => JSON.stringify(args),
|
|
148
166
|
client: {
|
|
149
167
|
fn: client.related,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextConfigBuilder.js","sourceRoot":"","sources":["../../src/ContextConfigBuilder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ContextConfigBuilder.js","sourceRoot":"","sources":["../../src/ContextConfigBuilder.ts"],"names":[],"mappings":"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"}
|
|
@@ -44,29 +44,59 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
44
44
|
#contextFilter;
|
|
45
45
|
#contextParameterFn;
|
|
46
46
|
#contextQueue = new Subject();
|
|
47
|
+
/**
|
|
48
|
+
* The underlying context client used to resolve and hold the current context item.
|
|
49
|
+
* @returns The internal {@link ContextClient} instance.
|
|
50
|
+
*/
|
|
47
51
|
get contextClient() {
|
|
48
52
|
return this.#contextClient;
|
|
49
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The query client used to search for context items.
|
|
56
|
+
* @returns The internal `Query` instance used by {@link queryContext}.
|
|
57
|
+
*/
|
|
50
58
|
get queryClient() {
|
|
51
59
|
return this.#contextQuery;
|
|
52
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Observable stream emitting the current context item.
|
|
63
|
+
* @returns Observable that emits the current `ContextItem`, `null`, or `undefined`.
|
|
64
|
+
*/
|
|
53
65
|
get currentContext$() {
|
|
54
66
|
return this.#contextClient.currentContext$;
|
|
55
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Snapshot of the current context item.
|
|
70
|
+
* @returns The current `ContextItem`, or `null`/`undefined` if not set.
|
|
71
|
+
*/
|
|
56
72
|
get currentContext() {
|
|
57
73
|
return this.#contextClient.currentContext;
|
|
58
74
|
}
|
|
59
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Sets the current context item.
|
|
77
|
+
* @deprecated do not use, will be removed
|
|
78
|
+
* @param context - The context item to set as current. Must not be `undefined`.
|
|
79
|
+
* @throws Error if `context` is `undefined`.
|
|
80
|
+
*/
|
|
60
81
|
set currentContext(context) {
|
|
61
82
|
console.warn('@deprecated', 'ContextProvider.currentContext', 'use setCurrentContextById|setCurrentContext|clearCurrentContext');
|
|
83
|
+
// undefined is reserved to mean "not yet initialized", so it cannot be set explicitly
|
|
62
84
|
if (context === undefined) {
|
|
63
85
|
throw Error('not allowed to set current context as undefined undefined!');
|
|
64
86
|
}
|
|
65
87
|
this.setCurrentContextAsync(context);
|
|
66
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Creates a new instance of `ContextProvider`.
|
|
91
|
+
* @param args - Constructor arguments.
|
|
92
|
+
* @param args.config - The context module configuration.
|
|
93
|
+
* @param args.event - Optional event module instance for dispatching context change events.
|
|
94
|
+
* @param args.parentContext - Optional parent context provider. Deprecated, use {@link connectParentContext}.
|
|
95
|
+
*/
|
|
67
96
|
constructor(args) {
|
|
68
97
|
const { config, event } = args;
|
|
69
98
|
super({ version, config });
|
|
99
|
+
// warn about deprecated parentContext constructor arg
|
|
70
100
|
if (args.parentContext) {
|
|
71
101
|
console.warn('@deprecated', 'parentContext as arg is deprecated, use ContextProvider.connectParentContext');
|
|
72
102
|
}
|
|
@@ -75,15 +105,18 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
75
105
|
if (config.resolveContext) {
|
|
76
106
|
this.resolveContext = config.resolveContext?.bind(this);
|
|
77
107
|
}
|
|
108
|
+
// override validateContext if configured
|
|
78
109
|
if (config.validateContext) {
|
|
79
110
|
this.validateContext = config.validateContext?.bind(this);
|
|
80
111
|
}
|
|
112
|
+
// override extractContextIdFromPath if configured
|
|
81
113
|
if (config.extractContextIdFromPath) {
|
|
82
|
-
// @ts-
|
|
114
|
+
// @ts-expect-error
|
|
83
115
|
this.extractContextIdFromPath = config.extractContextIdFromPath;
|
|
84
116
|
}
|
|
117
|
+
// override generatePathFromContext if configured
|
|
85
118
|
if (config.generatePathFromContext) {
|
|
86
|
-
// @ts-
|
|
119
|
+
// @ts-expect-error
|
|
87
120
|
this.generatePathFromContext = config.generatePathFromContext;
|
|
88
121
|
}
|
|
89
122
|
this.#contextType = config.contextType;
|
|
@@ -91,6 +124,7 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
91
124
|
// create clients
|
|
92
125
|
this.#contextClient = new ContextClient(config.client.get);
|
|
93
126
|
this.#contextQuery = new Query(config.client.query);
|
|
127
|
+
// only create the related-context query when the config provides one
|
|
94
128
|
if (config.client.related) {
|
|
95
129
|
this.#contextRelated = new Query(config.client.related);
|
|
96
130
|
}
|
|
@@ -107,9 +141,8 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
107
141
|
this.#subscriptions.add(
|
|
108
142
|
// observe current context changes
|
|
109
143
|
this.currentContext$
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
pairwise())
|
|
144
|
+
// emit previous and next context together for change comparisons
|
|
145
|
+
.pipe(pairwise())
|
|
113
146
|
.subscribe(([previous, next]) => {
|
|
114
147
|
this.#event?.dispatchEvent('onCurrentContextChanged', {
|
|
115
148
|
source: this,
|
|
@@ -153,11 +186,12 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
153
186
|
* @returns A `Subscription` object representing the connection to the parent context.
|
|
154
187
|
*/
|
|
155
188
|
connectParentContext(provider, opt) {
|
|
189
|
+
// build a stream of validated context changes from the parent provider
|
|
156
190
|
const parentContext$ = provider.currentContext$.pipe(
|
|
157
191
|
// do not set context if parent has not initialized
|
|
158
192
|
filter((x) => x !== undefined), filter((next, index) => {
|
|
159
193
|
// skip first item if opt.skipFirst is true
|
|
160
|
-
// TODO: this is a bit hacky, should be handled in a better way
|
|
194
|
+
// TODO(#5121): this is a bit hacky, should be handled in a better way
|
|
161
195
|
if (opt?.skipFirst && index <= 1) {
|
|
162
196
|
console.debug('ContextProvider::connectParentContext', 'skipping first item', next);
|
|
163
197
|
return false;
|
|
@@ -165,8 +199,8 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
165
199
|
// only set context if it has changed
|
|
166
200
|
return this.currentContext?.id !== next?.id;
|
|
167
201
|
}), switchMap(async (next) => {
|
|
202
|
+
// if parent context is null, just return
|
|
168
203
|
if (!next) {
|
|
169
|
-
// if parent context is null, just return
|
|
170
204
|
return { next };
|
|
171
205
|
}
|
|
172
206
|
// notify event observers that parent context is about to change and await for cancelation
|
|
@@ -180,14 +214,16 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
180
214
|
// filter out canceled context changes
|
|
181
215
|
filter((x) => !x.canceled), switchMap(({ next }) => {
|
|
182
216
|
// set current context with validation and resolution
|
|
183
|
-
return this.setCurrentContext(next, {
|
|
217
|
+
return (this.setCurrentContext(next, {
|
|
184
218
|
validate: true,
|
|
185
219
|
resolve: true,
|
|
186
|
-
})
|
|
220
|
+
})
|
|
221
|
+
// swallow errors so a failed context change doesn't break the parent subscription
|
|
222
|
+
.pipe(catchError((err) => {
|
|
187
223
|
console.warn('ContextProvider::onParentContextChanged', 'setCurrentContext', err);
|
|
188
224
|
// do not emit any value if an error occurs
|
|
189
225
|
return EMPTY;
|
|
190
|
-
}));
|
|
226
|
+
})));
|
|
191
227
|
}), catchError((err) => {
|
|
192
228
|
console.warn('ContextProvider::onParentContextChanged', 'unhandled exception', err);
|
|
193
229
|
// do not emit any value if an error occurs
|
|
@@ -217,6 +253,7 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
217
253
|
this.#contextClient
|
|
218
254
|
// resolve context item by id
|
|
219
255
|
.resolveContext(id)
|
|
256
|
+
// filter out invalid items and set as current context
|
|
220
257
|
.pipe(
|
|
221
258
|
// filter out invalid context items
|
|
222
259
|
filter((item) => !!item),
|
|
@@ -256,12 +293,18 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
256
293
|
* If the observable is subscribe, unsubscribing __WILL__ abort the task and remove it from queue
|
|
257
294
|
*
|
|
258
295
|
* @param context context item which would be queue to set as current
|
|
296
|
+
* @param opt Optional settings.
|
|
297
|
+
* @param opt.validate Whether to validate the context item before setting it.
|
|
298
|
+
* @param opt.resolve Whether to attempt to resolve the context item if validation fails.
|
|
299
|
+
* @template T The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
|
|
300
|
+
* @returns An observable that emits the context item once the queued task completes.
|
|
259
301
|
*/
|
|
260
302
|
setCurrentContext(context, opt) {
|
|
261
303
|
// signal for aborting the queue entry
|
|
262
304
|
const abort$ = new Subject();
|
|
263
305
|
// wrapper for returning an observable to the caller
|
|
264
306
|
const subject$ = new Subject();
|
|
307
|
+
// run the actual context-setting logic and relay results/errors to the caller's subject
|
|
265
308
|
const task$ = this._setCurrentContext(context, opt).pipe(
|
|
266
309
|
// send context item which was set to the caller
|
|
267
310
|
tap((x) => subject$.next(x)),
|
|
@@ -278,6 +321,7 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
278
321
|
}));
|
|
279
322
|
// add task to internal queue
|
|
280
323
|
this.#contextQueue.next(task$);
|
|
324
|
+
// tear down the queued task if the caller unsubscribes
|
|
281
325
|
return subject$.pipe(
|
|
282
326
|
// if caller subscribes, unsubscribe should abort queue entry
|
|
283
327
|
finalize(() => abort$.next(true)));
|
|
@@ -311,7 +355,7 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
311
355
|
* - Emit the context and complete the observable.
|
|
312
356
|
*
|
|
313
357
|
* @protected
|
|
314
|
-
* @
|
|
358
|
+
* @template T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
|
|
315
359
|
* @param context - The new context to set.
|
|
316
360
|
* @param opt - Optional settings:
|
|
317
361
|
* - `validate`: Whether to validate the context before setting.
|
|
@@ -342,7 +386,10 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
342
386
|
// emit error and complete
|
|
343
387
|
return subscriber.error(Error('failed to validate provided context'));
|
|
344
388
|
}
|
|
389
|
+
// if resolve is enabled, attempt to resolve the invalid context before setting it
|
|
345
390
|
if (opt.resolve) {
|
|
391
|
+
// the recursive `_setCurrentContext` call below is re-entered with the already
|
|
392
|
+
// validated/resolved context, so the generic `T` cast is safe here.
|
|
346
393
|
return of(context)
|
|
347
394
|
.pipe(
|
|
348
395
|
// notify event observers that context is about to get resolved
|
|
@@ -360,7 +407,9 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
360
407
|
return context;
|
|
361
408
|
}),
|
|
362
409
|
// resolve context
|
|
363
|
-
switchMap((context) =>
|
|
410
|
+
switchMap((context) =>
|
|
411
|
+
// Pair the resolved context alongside the original for downstream consumers.
|
|
412
|
+
this.resolveContext(context).pipe(map((resolved) => ({
|
|
364
413
|
context,
|
|
365
414
|
resolved,
|
|
366
415
|
})))),
|
|
@@ -413,7 +462,7 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
413
462
|
*
|
|
414
463
|
* @see {@link setCurrentContext} for more details.
|
|
415
464
|
*
|
|
416
|
-
* @
|
|
465
|
+
* @template T - The type of the context item, which extends `ContextItem<Record<string, unknown>>` or can be `null`.
|
|
417
466
|
* @param context - The context item to set as the current context, or `null` to clear it.
|
|
418
467
|
* @param opt - Optional settings for context handling.
|
|
419
468
|
* @param opt.validate - If `true`, validates the context before setting it.
|
|
@@ -433,6 +482,7 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
433
482
|
*
|
|
434
483
|
* @param search - The search string to filter context items.
|
|
435
484
|
* @returns An Observable that emits an array of `ContextItem` objects matching the search criteria.
|
|
485
|
+
* @throws Re-throws the underlying cause of a `QueryClientError`, otherwise re-throws the original error.
|
|
436
486
|
*/
|
|
437
487
|
queryContext(search) {
|
|
438
488
|
const query$ = this.queryClient
|
|
@@ -442,6 +492,7 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
442
492
|
search,
|
|
443
493
|
type: this.#contextType,
|
|
444
494
|
}))
|
|
495
|
+
// unwrap query-client errors and expose the underlying cause to subscribers
|
|
445
496
|
.pipe(catchError((err) => {
|
|
446
497
|
// if query client throws a QueryClientError, extract the cause and throw it
|
|
447
498
|
if (err.name === 'QueryClientError') {
|
|
@@ -470,8 +521,10 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
470
521
|
* @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`.
|
|
471
522
|
*/
|
|
472
523
|
validateContext(item) {
|
|
524
|
+
// no context type configured means every context item is considered valid
|
|
473
525
|
if (!this.#contextType)
|
|
474
526
|
return true;
|
|
527
|
+
// normalize allowed types for a case-insensitive comparison
|
|
475
528
|
return this.#contextType.map((x) => x.toLowerCase()).includes(item.type.id.toLowerCase());
|
|
476
529
|
}
|
|
477
530
|
/**
|
|
@@ -492,7 +545,9 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
492
545
|
// request related context items for the given context item with the same context type which the provider is configured with
|
|
493
546
|
return this.relatedContexts({ item, filter: { type: this.#contextType } }).pipe(
|
|
494
547
|
// filter out invalid context items
|
|
495
|
-
map((x) =>
|
|
548
|
+
map((x) =>
|
|
549
|
+
// keep only context items that validate against the provider's context type
|
|
550
|
+
x.filter((item) => this.validateContext(item))), map((values) => {
|
|
496
551
|
// related context should be resolved to a single context item
|
|
497
552
|
const value = values.shift();
|
|
498
553
|
// if no value is found, throw an error
|
|
@@ -536,6 +591,7 @@ export class ContextProvider extends BaseModuleProvider {
|
|
|
536
591
|
}
|
|
537
592
|
// request related context items
|
|
538
593
|
return this.#contextRelated.query(args).pipe(map(({ value }) => value), catchError((err) => {
|
|
594
|
+
// unwrap the underlying cause so callers see the original error
|
|
539
595
|
if (err.cause) {
|
|
540
596
|
throw err.cause;
|
|
541
597
|
}
|