@equinor/fusion-framework-module-context
v9.0.0
Published
Readme
@equinor/fusion-framework-module-context
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.
When to use
Use this module when your application or portal needs to:
- set, read, or clear the active context (e.g. selecting a project)
- search and filter available context items from the Fusion context API
- validate whether a given context item matches the allowed context types
- resolve context across parent–child provider boundaries (portal → app)
- deep-link by extracting a context ID from the URL path
Application developers will typically use the higher-level
@equinor/fusion-framework-react-app/contextpackage, which wraps this module with React hooks and providers.Portal developers will use
@equinor/fusion-framework-react/contextfor the same purpose at the portal level.
How it fits together
A context module instance never lives in isolation. It depends on the
@equinor/fusion-framework-module-services module for its default HTTP client, optionally
uses @equinor/fusion-framework-module-event to dispatch and listen for context change events,
and optionally reads the current URL through @equinor/fusion-framework-module-navigation to
resolve a context from the path. None of these need manual setup — enableContext requires
only services; event and navigation are picked up automatically when present.
The module is also hierarchy-aware by default: when a Portal and an App both enable context, the App's instance automatically connects to the Portal's and mirrors its context — no wiring required beyond enabling the module on both sides. See Lifecycle for exactly how that sync works, and when it can be overridden or disabled.
Documentation
| Topic | Description |
|---|---|
| Data model | The shape of a ContextItem, how context types relate to each other, and query/related parameters |
| Lifecycle | setCurrentContext's validate/resolve decision flow, automatic initial-context resolution on startup, and parent/child context propagation |
| Recipes | OData query parameters, path rewriting, accepting a family of context types, and custom search errors |
Key concepts
| Concept | Description | |---|---| | ContextItem | A typed record representing a single context entity (project, facility, etc.). | | ContextProvider | Runtime service exposing query, set, validate, resolve, and event APIs. | | ContextModuleConfigurator | Fluent builder for configuring context types, filters, clients, and hooks. | | enableContext | Helper that registers the module on a modules configurator. | | Context resolution | Automatic lookup of related context items when a context type does not match the configured types. | | Parent connection | Bi-directional sync between a parent portal context and a child app context. |
Quick start
import { enableContext } from '@equinor/fusion-framework-module-context';
export const configure = (configurator) => {
enableContext(configurator, (builder) => {
// only accept ProjectMaster context items
builder.setContextType(['ProjectMaster']);
});
};Once initialized, access the provider from the module instance:
// observe context changes
modules.context.currentContext$.subscribe((ctx) => {
console.log('context changed', ctx);
});
// search for context items
const items = await modules.context.queryContextAsync('Johan');
// set context by item
await modules.context.setCurrentContextAsync(items[0]);
// set context by ID
await modules.context.setCurrentContextByIdAsync('7fd97952-...');
// clear the active context
modules.context.clearCurrentContext();Configuration
All configuration flows through enableContext → ContextModuleConfigurator:
enableContext(configurator, (builder) => {
// restrict accepted context types
builder.setContextType(['ProjectMaster', 'Facility']);
// post-query filter
builder.setContextFilter((items) => items.filter((i) => i.isActive));
// custom parameter mapping for the search API
builder.setContextParameterFn(({ search, type }) => ({
search,
filter: { type },
}));
// custom validation logic
builder.setValidateContext(function (item) {
return item !== null && this.validateContext(item);
});
// custom context resolution
builder.setResolveContext(function (item) {
return this.relatedContexts({ item, filter: { type: ['ProjectMaster'] } });
});
// connect (or disconnect) from parent context
// when enabled (default), onParentContextChanged fires before mirroring the parent's context locally
builder.connectParentContext(false);
// path ↔ context integration
// the default resolver uses extractContextIdFromPath to pull a GUID from the URL,
// then fetches the matching context item — see resolveInitialContext in utils
builder.setContextPathExtractor((path) => path.split('/')[2]);
builder.setContextPathGenerator((ctx, path) =>
path.replace(/\/context\/[^/]+/, `/context/${ctx.id}`),
);
// provide a fully custom context client
builder.setContextClient({
get: (args) => fetch(`/api/context/${args.id}`).then((r) => r.json()),
query: (args) => fetch(`/api/context?q=${args.search}`).then((r) => r.json()),
});
});Configuration reference
| Builder method | Purpose |
|---|---|
| setContextType(types) | Allowed context type IDs for validation |
| setContextFilter(fn) | Post-query result filter |
| setContextParameterFn(fn) | Maps search + type to API query params |
| setValidateContext(fn) | Custom validation (this = provider) |
| setResolveContext(fn) | Custom resolution (this = provider) |
| connectParentContext(bool) | Enable/disable parent context sync (fires onParentContextChanged on change) |
| setContextPathExtractor(fn) | Extract context ID from URL path |
| setContextPathGenerator(fn) | Generate URL path from context item |
| setResolveInitialContext(fn) | Override initial context resolution (default uses extractContextIdFromPath → resolveContextFromPath) |
| setContextClient(client) | Custom get/query/related clients |
Context Routing in the URL
The portal keeps the active context synchronized with the browser URL automatically
via the @equinor/fusion-framework-plugin-context-navigation plugin. As an app
developer, you control how context appears in your URL by declaring a routing
strategy in your app manifest.
Declare a routing strategy
Set contextRouting in your manifest's build.options:
// app.manifest.config.ts
{
"build": {
"options": {
"contextRouting": "query"
}
}
}| Value | URL Shape | When to Use |
|---|---|---|
| 'path' (or omitted) | /apps/{appKey}/{contextId}/sub-route | Default — simple apps without complex path routing |
| 'query' | /apps/{appKey}/route?$contextId={id} | Apps with nested routes that would conflict with a context path segment |
Apps with custom URL shapes should omit contextRouting and register custom hooks instead (see below).
[!NOTE] When
contextRoutingis not set (or set tonull), the portal defaults to the path adapter which encodes context as a path segment after the app key. If your app also registers custom hooks (setContextPathExtractor/setContextPathGenerator), the custom adapter takes priority over the path adapter regardless of thecontextRoutingvalue.
Custom URL shapes
If your app uses a non-standard URL layout for context (e.g. /route-a/{contextId}),
register custom hooks instead of (or in addition to) declaring contextRouting:
enableContext(configurator, (builder) => {
builder.setContextType(['ProjectMaster']);
// Tell the portal how to find the context id in your URL
builder.setContextPathExtractor((path) => {
const segments = path.split('/').filter(Boolean);
return segments[1]; // e.g. /route-a/{contextId}
});
// Tell the portal how to build a URL with context
builder.setContextPathGenerator((context, path) => {
const segments = path.split('/').filter(Boolean);
const route = segments[0] ?? '';
return `/${route}/${context.id}`;
});
});When these hooks are registered, the portal's custom adapter uses them automatically —
no contextRouting declaration needed in the manifest.
How it works (for reference)
The portal's context-navigation plugin reads contextRouting from your app's
manifest at runtime and selects the appropriate URL adapter. You don't install
or configure the plugin yourself — the portal handles it. Your app only needs to
declare its preference.
Events
The context module dispatches events via the @equinor/fusion-framework-module-event system. All events are scoped to the ContextProvider source.
| Event | When | Cancelable |
|---|---|---|
| onCurrentContextChange | Before the current context is updated | Yes |
| onCurrentContextChanged | After the current context has changed | No |
| onParentContextChanged | Before a parent context change is mirrored locally | Yes |
| onSetContextResolve | Before context resolution begins | Yes |
| onSetContextResolved | After context resolution completes | Yes |
| onSetContextValidationFailed | When validation fails (no resolution) | No |
| onSetContextResolveFailed | When resolution fails with an error | No |
modules.event.addEventListener('onCurrentContextChanged', (e) => {
console.log('previous:', e.detail.previous);
console.log('next:', e.detail.next);
});Errors
The module exports FusionContextSearchError (from @equinor/fusion-framework-module-context/errors.js) for search-related failures:
import { FusionContextSearchError } from '@equinor/fusion-framework-module-context/errors.js';
try {
await modules.context.queryContextAsync('...');
} catch (err) {
if (err instanceof FusionContextSearchError) {
console.error(err.title, err.description);
}
}Testing
@equinor/fusion-framework-module-context/mock provides enableContextMock, a purpose-built test double backed by an in-memory pool of seeded context items — no context API, no HTTP mock, and no service-discovery mock required. Real ContextProvider behaviour (validateContext, resolveContext, parent-context propagation) still runs against the seeded data; only the data source is substituted.
Defaults
- The context pool starts empty and no current context is selected.
- Querying the empty pool returns no items.
- Looking up an unseeded id throws an error that names the id and the seeding methods.
- Related-context lookup uses the seeded pool, excludes the source item, and filters by the requested type.
setCurrentContextboth seeds the item and selects it for initial app resolution;setContextsonly seeds items.
import { enableContextMock } from '@equinor/fusion-framework-module-context/mock';
enableContextMock(configurator, (mock) => {
mock.setCurrentContext({ id: 'my-ctx', type: { id: 'ProjectMaster' }, value: {} });
});- Friendly layer —
setCurrentContext,setContexts,addContext,setRelatedContexts— a context-domain vocabulary for the common case: seed a known item, get it back. - Escape hatch —
setResolver— a raw resolution function for a customresolveContextstrategy or a shape the friendly layer did not cover.
When used with @equinor/fusion-framework/mock, FrameworkMockConfigurator.context returns this same configurator, so seeding happens directly through configurator.context with no callback needed.
This is one of two ways to fake context data in tests. The other is mocking the context API's HTTP responses directly (with configurator.http.addMiddleware(...), optionally paired with @equinor/fusion-framework-module-http/mock's createOpenApiMockMiddleware for faker-generated data straight from context's OpenAPI spec) — which exercises the real ContextModuleConfigurator/services/HTTP pipeline instead of substituting it. Reach for enableContextMock to seed one known item with no transport involved; reach for the HTTP middleware when a test needs to cover that pipeline itself. Fixture generators for realistic seeded data (createContextItemFactory, createContextItems) are available from @equinor/fusion-framework-module-context/mock/fixtures.
Generate deterministic context fixtures
The optional /mock/fixtures entry point uses @faker-js/faker to produce realistic but
repeatable titles and readable ids. Install Faker only when importing this entry point.
import { enableContextMock } from '@equinor/fusion-framework-module-context/mock';
import { createContextItems } from '@equinor/fusion-framework-module-context/mock/fixtures';
const [project, contract] = createContextItems([
{ type: 'ProjectMaster' },
{ type: 'Contract', parentTypeIds: ['ProjectMaster'] },
]);
enableContextMock(configurator, (mock) => {
mock.setContexts([project, contract]);
mock.setCurrentContext(project);
});Use createContextItemFactory(prefix?) for sequential fixtures of one type. Use
createContextItems for multiple types and parent/child type metadata. Generated values are
deterministic per id; use setRelatedContexts when relations must differ per specific item.
Utilities
Additional helpers are available from @equinor/fusion-framework-module-context/utils:
| Export | Purpose |
|---|---|
| enableContext | Register the context module on a configurator |
| resolveInitialContext | Default initial-context resolver (path → parent fallback) |
| extractContextIdFromPath | Extract a GUID-format context ID from a URL path |
| resolveContextFromPath | Create a resolver function that fetches a context item from a URL path |
Package exports
| Specifier | Description |
|---|---|
| @equinor/fusion-framework-module-context | Main entry — module, provider, configurator, types |
| @equinor/fusion-framework-module-context/errors.js | FusionContextSearchError |
| @equinor/fusion-framework-module-context/utils | Utility functions for path resolution and enablement |
| @equinor/fusion-framework-module-context/mock | enableContextMock, ContextMockConfigurator — in-memory test double |
| @equinor/fusion-framework-module-context/mock/fixtures | Fixture factories for generating seeded context items |
