@equinor/fusion-framework-module-app 8.0.3 → 8.1.0-next.0
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 +42 -0
- package/README.md +32 -1
- package/dist/esm/AppConfigurator.js +1 -0
- package/dist/esm/AppConfigurator.js.map +1 -1
- package/dist/esm/__tests__/AppModuleProvider.test.js +36 -0
- package/dist/esm/__tests__/AppModuleProvider.test.js.map +1 -0
- package/dist/esm/__tests__/MockAppClient.test.js +70 -0
- package/dist/esm/__tests__/MockAppClient.test.js.map +1 -0
- package/dist/esm/app/App.js +1 -0
- package/dist/esm/app/App.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mock/MockAppClient.js +65 -0
- package/dist/esm/mock/MockAppClient.js.map +1 -0
- package/dist/esm/mock/index.js +13 -0
- package/dist/esm/mock/index.js.map +1 -0
- package/dist/esm/schemas.js +23 -0
- package/dist/esm/schemas.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/AppConfigurator.d.ts +1 -0
- package/dist/types/__tests__/AppModuleProvider.test.d.ts +1 -0
- package/dist/types/__tests__/MockAppClient.test.d.ts +1 -0
- package/dist/types/app/App.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mock/MockAppClient.d.ts +56 -0
- package/dist/types/mock/index.d.ts +12 -0
- package/dist/types/schemas.d.ts +29 -0
- package/dist/types/types.d.ts +31 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +19 -9
- package/src/AppConfigurator.ts +1 -0
- package/src/__tests__/AppModuleProvider.test.ts +47 -0
- package/src/__tests__/MockAppClient.test.ts +97 -0
- package/src/app/App.ts +1 -0
- package/src/index.ts +2 -0
- package/src/mock/MockAppClient.ts +77 -0
- package/src/mock/index.ts +13 -0
- package/src/schemas.ts +24 -0
- package/src/types.ts +32 -0
- package/src/version.ts +1 -1
- package/tsconfig.json +3 -0
- package/vitest.config.ts +11 -0
|
@@ -43,6 +43,7 @@ export interface IAppConfigurator {
|
|
|
43
43
|
* via service discovery. The default `assetUri` is `'/apps-proxy'`.
|
|
44
44
|
*/
|
|
45
45
|
export declare class AppConfigurator extends BaseConfigBuilder<AppModuleConfig> implements IAppConfigurator {
|
|
46
|
+
/** Default cache expiration time, in milliseconds, for the app service client. */
|
|
46
47
|
defaultExpireTime: number;
|
|
47
48
|
/**
|
|
48
49
|
* Creates the default HTTP client for the app service, preferring a pre-configured
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/app/App.d.ts
CHANGED
|
@@ -289,6 +289,7 @@ export declare class App<TEnv extends ConfigEnvironment = ConfigEnvironment, TMo
|
|
|
289
289
|
getAppModule(force_refresh?: boolean): Observable<AppScriptModule>;
|
|
290
290
|
/** @inheritdoc */
|
|
291
291
|
getAppModuleAsync(allow_cache?: boolean): Promise<AppScriptModule>;
|
|
292
|
+
/** Releases resources held by this app instance. */
|
|
292
293
|
dispose: VoidFunction;
|
|
293
294
|
}
|
|
294
295
|
export default App;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export { AppModuleConfig, AppConfigurator, IAppConfigurator, type AppModuleConfig as IAppModuleConfig, } from './AppConfigurator';
|
|
20
20
|
export { AppClient, type IAppClient } from './AppClient';
|
|
21
|
+
export { AppConfig } from './AppConfig';
|
|
21
22
|
export { AppModuleProvider } from './AppModuleProvider';
|
|
22
23
|
export { IApp } from './app/App';
|
|
23
24
|
export * from './events';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Observable } from 'rxjs';
|
|
2
|
+
import type { IHttpClient } from '@equinor/fusion-framework-module-http';
|
|
3
|
+
import { AppClient } from '../AppClient.js';
|
|
4
|
+
import type { AppConfig, AppManifest, ConfigEnvironment } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* An {@link AppClient} that answers `getAppManifest` and `getAppConfig` for one
|
|
7
|
+
* known app locally, delegating everything else — other app keys, tagged
|
|
8
|
+
* requests, builds, settings — to the real client it wraps.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* Whatever `client` was resolved to (a pre-configured http client, or one created
|
|
12
|
+
* through service discovery) still backs every method this class doesn't
|
|
13
|
+
* override, so pointing service discovery at a different registry or a real
|
|
14
|
+
* local mock server keeps working unchanged.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* builder.setClient(async ({ requireInstance }) => {
|
|
19
|
+
* const http = await requireInstance('http');
|
|
20
|
+
* return new MockAppClient(http.createClient('apps'), manifest, config);
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class MockAppClient extends AppClient {
|
|
25
|
+
#private;
|
|
26
|
+
/**
|
|
27
|
+
* @param client - The real {@link IHttpClient} to delegate all other requests to.
|
|
28
|
+
* @param manifest - The manifest to return for `getAppManifest({ appKey: manifest.appKey })`.
|
|
29
|
+
* @param config - The config to return for `getAppConfig({ appKey: manifest.appKey })`, if any.
|
|
30
|
+
*/
|
|
31
|
+
constructor(client: IHttpClient, manifest: AppManifest, config?: AppConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Answers with the manifest passed to the constructor when `args` matches
|
|
34
|
+
* this client's own app key and no tag; otherwise delegates to the real client.
|
|
35
|
+
*
|
|
36
|
+
* @param args - The app key and optional tag to resolve a manifest for.
|
|
37
|
+
* @returns An observable of the resolved {@link AppManifest}.
|
|
38
|
+
*/
|
|
39
|
+
getAppManifest(args: {
|
|
40
|
+
appKey: string;
|
|
41
|
+
tag?: string;
|
|
42
|
+
}): Observable<AppManifest>;
|
|
43
|
+
/**
|
|
44
|
+
* Answers with the config passed to the constructor when `args` matches this
|
|
45
|
+
* client's own app key and tag; otherwise delegates to the real client.
|
|
46
|
+
*
|
|
47
|
+
* @template TType - The shape of the config's `environment` data.
|
|
48
|
+
* @param args - The app key and optional tag to resolve config for.
|
|
49
|
+
* @returns An observable of the resolved {@link AppConfig}.
|
|
50
|
+
*/
|
|
51
|
+
getAppConfig<TType extends ConfigEnvironment = ConfigEnvironment>(args: {
|
|
52
|
+
appKey: string;
|
|
53
|
+
tag?: string;
|
|
54
|
+
}): Observable<AppConfig<TType>>;
|
|
55
|
+
}
|
|
56
|
+
export default MockAppClient;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test doubles for the app module.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Imported from `@equinor/fusion-framework-module-app/mock`, so the mock ships
|
|
6
|
+
* and versions with the implementation it stands in for.
|
|
7
|
+
*
|
|
8
|
+
* This entry point has no dependency on any test runner.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
export { MockAppClient } from './MockAppClient.js';
|
package/dist/types/schemas.d.ts
CHANGED
|
@@ -18,6 +18,22 @@ export declare const ApiAppConfigSchema: z.ZodObject<{
|
|
|
18
18
|
}, z.core.$strip>>>;
|
|
19
19
|
}, z.core.$strip>;
|
|
20
20
|
export type ApiAppConfig = z.infer<typeof ApiAppConfigSchema>;
|
|
21
|
+
/**
|
|
22
|
+
* Schema for validating the options object on application builds.
|
|
23
|
+
*
|
|
24
|
+
* Known properties:
|
|
25
|
+
* - `contextRouting` (optional): Routing strategy for context — `'path'` or `'query'`.
|
|
26
|
+
*
|
|
27
|
+
* The schema uses `.catchall(z.unknown())` to permit additional properties
|
|
28
|
+
* at runtime without requiring them to be declared here. Add known properties
|
|
29
|
+
* to this schema as they are introduced.
|
|
30
|
+
*/
|
|
31
|
+
export declare const FrameworkOptionsSchema: z.ZodObject<{
|
|
32
|
+
contextRouting: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
33
|
+
path: "path";
|
|
34
|
+
query: "query";
|
|
35
|
+
}>>>;
|
|
36
|
+
}, z.core.$catchall<z.ZodUnknown>>;
|
|
21
37
|
/**
|
|
22
38
|
* Schema for validating the structure of an API application build.
|
|
23
39
|
*
|
|
@@ -33,6 +49,7 @@ export type ApiAppConfig = z.infer<typeof ApiAppConfigSchema>;
|
|
|
33
49
|
* - `githubRepo`: An optional GitHub repository associated with the build.
|
|
34
50
|
* - `projectPage`: An optional project page URL.
|
|
35
51
|
* - `allowedExtensions`: An optional array of allowed extensions for the build.
|
|
52
|
+
* - `options`: An optional record of additional build options, where the key is a string and the value can be of any type.
|
|
36
53
|
* - `uploadedBy`: An optional schema for the person who uploaded the build.
|
|
37
54
|
*/
|
|
38
55
|
export declare const ApiApplicationBuildSchema: z.ZodObject<{
|
|
@@ -46,6 +63,12 @@ export declare const ApiApplicationBuildSchema: z.ZodObject<{
|
|
|
46
63
|
commitSha: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
47
64
|
githubRepo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
48
65
|
projectPage: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
66
|
+
options: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
67
|
+
contextRouting: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
68
|
+
path: "path";
|
|
69
|
+
query: "query";
|
|
70
|
+
}>>>;
|
|
71
|
+
}, z.core.$catchall<z.ZodUnknown>>>>>;
|
|
49
72
|
allowedExtensions: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
50
73
|
uploadedBy: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
51
74
|
azureUniqueId: z.ZodString;
|
|
@@ -133,6 +156,12 @@ export declare const ApiApplicationSchema: z.ZodObject<{
|
|
|
133
156
|
commitSha: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
134
157
|
githubRepo: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
135
158
|
projectPage: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
159
|
+
options: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
160
|
+
contextRouting: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
161
|
+
path: "path";
|
|
162
|
+
query: "query";
|
|
163
|
+
}>>>;
|
|
164
|
+
}, z.core.$catchall<z.ZodUnknown>>>>>;
|
|
136
165
|
allowedExtensions: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
137
166
|
uploadedBy: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
138
167
|
azureUniqueId: z.ZodString;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -93,6 +93,36 @@ export type RouteSchemaEntry = [
|
|
|
93
93
|
search?: Record<string, string>;
|
|
94
94
|
}
|
|
95
95
|
];
|
|
96
|
+
/**
|
|
97
|
+
* Typed representation of the options object.
|
|
98
|
+
*
|
|
99
|
+
* Only known keys are declared in the type — additional properties are
|
|
100
|
+
* handled by the Zod schema at runtime. Keeping the type narrow avoids
|
|
101
|
+
* structural incompatibilities with `RecursivePartial<AppManifest>`.
|
|
102
|
+
*/
|
|
103
|
+
export type FrameworkOptions = {
|
|
104
|
+
/**
|
|
105
|
+
* Declares how the portal should encode this app's active context into the URL.
|
|
106
|
+
*
|
|
107
|
+
* Read by the `@equinor/fusion-framework-plugin-context-navigation` plugin
|
|
108
|
+
* to select the correct adapter at runtime.
|
|
109
|
+
*
|
|
110
|
+
* - `'path'` — context id as a path segment: `/apps/{appKey}/{contextId}/...`
|
|
111
|
+
* - `'query'` — context id as a query parameter: `/apps/{appKey}/...?$contextId={id}`
|
|
112
|
+
* - `null` — explicitly opt out of automatic context-to-URL encoding
|
|
113
|
+
* - `undefined` (omitted) — falls back to `'path'` (the default adapter)
|
|
114
|
+
*
|
|
115
|
+
* Apps with custom URL shapes should omit this field and register
|
|
116
|
+
* `setContextPathExtractor` / `setContextPathGenerator` hooks instead —
|
|
117
|
+
* the custom adapter picks those up automatically.
|
|
118
|
+
*
|
|
119
|
+
* Set this in the app manifest's `build.options.contextRouting` field,
|
|
120
|
+
* either in `app.manifest.config.ts` or via the app service manifest.
|
|
121
|
+
*
|
|
122
|
+
* @default undefined (falls back to path adapter)
|
|
123
|
+
*/
|
|
124
|
+
contextRouting?: 'path' | 'query' | null;
|
|
125
|
+
};
|
|
96
126
|
/**
|
|
97
127
|
* Build metadata returned by the app service for a specific application version.
|
|
98
128
|
* Contains the script entry point, asset path, tags, and optional CI metadata.
|
|
@@ -109,6 +139,7 @@ export type AppBuildManifest = {
|
|
|
109
139
|
githubRepo?: Nullable<string>;
|
|
110
140
|
projectPage?: Nullable<string>;
|
|
111
141
|
annotations?: Nullable<Record<string, string>>;
|
|
142
|
+
options?: Nullable<FrameworkOptions>;
|
|
112
143
|
allowedExtensions?: Nullable<string[]>;
|
|
113
144
|
uploadedBy?: Nullable<AppOwner>;
|
|
114
145
|
};
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "8.0.
|
|
1
|
+
export declare const version = "8.1.0-next.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework-module-app",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.1.0-next.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
},
|
|
23
23
|
"./app/*.js": {
|
|
24
24
|
"import": "./dist/esm/app/*.js"
|
|
25
|
+
},
|
|
26
|
+
"./mock": {
|
|
27
|
+
"import": "./dist/esm/mock/index.js",
|
|
28
|
+
"types": "./dist/types/mock/index.d.ts"
|
|
25
29
|
}
|
|
26
30
|
},
|
|
27
31
|
"types": "dist/types/index.d.ts",
|
|
@@ -41,6 +45,9 @@
|
|
|
41
45
|
],
|
|
42
46
|
"app/*.js": [
|
|
43
47
|
"dist/types/app/*.d.ts"
|
|
48
|
+
],
|
|
49
|
+
"mock": [
|
|
50
|
+
"dist/types/mock/index.d.ts"
|
|
44
51
|
]
|
|
45
52
|
}
|
|
46
53
|
},
|
|
@@ -61,18 +68,21 @@
|
|
|
61
68
|
"rxjs": "^7.8.1",
|
|
62
69
|
"uuid": "^14.0.0",
|
|
63
70
|
"zod": "^4.4.3",
|
|
64
|
-
"@equinor/fusion-
|
|
65
|
-
"@equinor/fusion-
|
|
71
|
+
"@equinor/fusion-query": "^7.0.4-next.0",
|
|
72
|
+
"@equinor/fusion-observable": "^9.1.2-next.0"
|
|
66
73
|
},
|
|
67
74
|
"devDependencies": {
|
|
68
75
|
"typescript": "^7.0.2",
|
|
69
|
-
"
|
|
70
|
-
"@equinor/fusion-framework-module
|
|
71
|
-
"@equinor/fusion-framework
|
|
72
|
-
"@equinor/fusion-framework-module-
|
|
73
|
-
"@equinor/fusion-framework-module-
|
|
76
|
+
"vitest": "^4.1.10",
|
|
77
|
+
"@equinor/fusion-framework-module": "^6.1.3-next.0",
|
|
78
|
+
"@equinor/fusion-framework": "^8.1.0-next.0",
|
|
79
|
+
"@equinor/fusion-framework-module-event": "^6.1.0-next.0",
|
|
80
|
+
"@equinor/fusion-framework-module-http": "^8.1.0-next.0",
|
|
81
|
+
"@equinor/fusion-framework-module-msal": "^11.0.0-next.0",
|
|
82
|
+
"@equinor/fusion-framework-module-service-discovery": "^10.1.0-next.0"
|
|
74
83
|
},
|
|
75
84
|
"scripts": {
|
|
76
|
-
"build": "tsc -b"
|
|
85
|
+
"build": "tsc -b",
|
|
86
|
+
"test": "vitest run"
|
|
77
87
|
}
|
|
78
88
|
}
|
package/src/AppConfigurator.ts
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
4
|
+
import { createRouterMiddleware } from '@equinor/fusion-framework-module-http/mock';
|
|
5
|
+
|
|
6
|
+
import { enableAppModule } from '../enable-app-module';
|
|
7
|
+
import type { AppModule } from '../module';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Boots a full portal-shaped framework instance the same way `packages/dev-portal`'s
|
|
11
|
+
* `configure.ts` does — `enableAppModule` registered directly on the portal's own
|
|
12
|
+
* configurator, alongside its real `http`, `event`, `msal`, etc. — rather than a
|
|
13
|
+
* standalone module graph assembled just for this test. Only the network call
|
|
14
|
+
* itself is faked; the app module creates its client eagerly during its own
|
|
15
|
+
* `requireInstance('http')` in the SAME configure→initialize pipeline, so this also
|
|
16
|
+
* verifies middleware registered before `initialize()` reaches that client.
|
|
17
|
+
*/
|
|
18
|
+
const initializePortalWith = () =>
|
|
19
|
+
mockFramework<[AppModule]>((configurator) => {
|
|
20
|
+
configurator.http.configureClient('apps', { baseUri: 'https://apps.example.com' });
|
|
21
|
+
configurator.http.addMiddleware(
|
|
22
|
+
createRouterMiddleware('https://apps.example.com', (router) => {
|
|
23
|
+
router.get('/persons/me/apps/:appKey', ({ params }) =>
|
|
24
|
+
Response.json({
|
|
25
|
+
appKey: params.appKey,
|
|
26
|
+
displayName: 'Test App',
|
|
27
|
+
description: 'A test application',
|
|
28
|
+
type: 'standalone',
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
enableAppModule(configurator);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('AppModuleProvider', () => {
|
|
37
|
+
it("fetches the manifest through the portal's own http client once a current app is set", async () => {
|
|
38
|
+
const fusion = await initializePortalWith();
|
|
39
|
+
|
|
40
|
+
fusion.modules.app.setCurrentApp('test-app');
|
|
41
|
+
|
|
42
|
+
await expect(fusion.modules.app.current?.getManifestAsync()).resolves.toMatchObject({
|
|
43
|
+
appKey: 'test-app',
|
|
44
|
+
displayName: 'Test App',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { firstValueFrom, of } from 'rxjs';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import type { IHttpClient } from '@equinor/fusion-framework-module-http';
|
|
5
|
+
|
|
6
|
+
import { AppClient } from '../AppClient.js';
|
|
7
|
+
import { AppConfig } from '../AppConfig.js';
|
|
8
|
+
import { MockAppClient } from '../mock/MockAppClient.js';
|
|
9
|
+
import type { AppManifest } from '../types.js';
|
|
10
|
+
|
|
11
|
+
const manifest: AppManifest = {
|
|
12
|
+
appKey: 'my-app',
|
|
13
|
+
displayName: 'My App',
|
|
14
|
+
description: 'My app',
|
|
15
|
+
type: 'standalone',
|
|
16
|
+
build: { version: '1.2.3', entryPoint: 'index.js' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const config = new AppConfig({ environment: { key: 'value' } });
|
|
20
|
+
|
|
21
|
+
// never invoked directly -- both overrides either answer locally or delegate through
|
|
22
|
+
// the spied `AppClient.prototype` methods below, so the real client is never called
|
|
23
|
+
const client = {} as IHttpClient;
|
|
24
|
+
|
|
25
|
+
describe('MockAppClient', () => {
|
|
26
|
+
describe('getAppManifest', () => {
|
|
27
|
+
it('answers locally for its own app key with no tag', async () => {
|
|
28
|
+
const mockClient = new MockAppClient(client, manifest);
|
|
29
|
+
|
|
30
|
+
const result = mockClient.getAppManifest({ appKey: 'my-app' });
|
|
31
|
+
|
|
32
|
+
await expect(firstValueFrom(result)).resolves.toBe(manifest);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('delegates for its own app key with an explicit empty-string tag', () => {
|
|
36
|
+
const delegate = vi
|
|
37
|
+
.spyOn(AppClient.prototype, 'getAppManifest')
|
|
38
|
+
.mockReturnValue(of(manifest));
|
|
39
|
+
const mockClient = new MockAppClient(client, manifest);
|
|
40
|
+
|
|
41
|
+
mockClient.getAppManifest({ appKey: 'my-app', tag: '' });
|
|
42
|
+
|
|
43
|
+
expect(delegate).toHaveBeenCalledWith({ appKey: 'my-app', tag: '' });
|
|
44
|
+
delegate.mockRestore();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('delegates for a different app key', () => {
|
|
48
|
+
const delegate = vi
|
|
49
|
+
.spyOn(AppClient.prototype, 'getAppManifest')
|
|
50
|
+
.mockReturnValue(of(manifest));
|
|
51
|
+
const mockClient = new MockAppClient(client, manifest);
|
|
52
|
+
|
|
53
|
+
mockClient.getAppManifest({ appKey: 'other-app' });
|
|
54
|
+
|
|
55
|
+
expect(delegate).toHaveBeenCalledWith({ appKey: 'other-app' });
|
|
56
|
+
delegate.mockRestore();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('getAppConfig', () => {
|
|
61
|
+
it('answers locally for its own app key with no tag', async () => {
|
|
62
|
+
const mockClient = new MockAppClient(client, manifest, config);
|
|
63
|
+
|
|
64
|
+
const result = mockClient.getAppConfig({ appKey: 'my-app' });
|
|
65
|
+
|
|
66
|
+
await expect(firstValueFrom(result)).resolves.toBe(config);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("answers locally for its own app key with a tag matching the manifest's build version", async () => {
|
|
70
|
+
const mockClient = new MockAppClient(client, manifest, config);
|
|
71
|
+
|
|
72
|
+
const result = mockClient.getAppConfig({ appKey: 'my-app', tag: '1.2.3' });
|
|
73
|
+
|
|
74
|
+
await expect(firstValueFrom(result)).resolves.toBe(config);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('delegates for its own app key with an explicit empty-string tag', () => {
|
|
78
|
+
const delegate = vi.spyOn(AppClient.prototype, 'getAppConfig').mockReturnValue(of(config));
|
|
79
|
+
const mockClient = new MockAppClient(client, manifest, config);
|
|
80
|
+
|
|
81
|
+
mockClient.getAppConfig({ appKey: 'my-app', tag: '' });
|
|
82
|
+
|
|
83
|
+
expect(delegate).toHaveBeenCalledWith({ appKey: 'my-app', tag: '' });
|
|
84
|
+
delegate.mockRestore();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('delegates for its own app key with a tag not matching the manifest build version', () => {
|
|
88
|
+
const delegate = vi.spyOn(AppClient.prototype, 'getAppConfig').mockReturnValue(of(config));
|
|
89
|
+
const mockClient = new MockAppClient(client, manifest, config);
|
|
90
|
+
|
|
91
|
+
mockClient.getAppConfig({ appKey: 'my-app', tag: '9.9.9' });
|
|
92
|
+
|
|
93
|
+
expect(delegate).toHaveBeenCalledWith({ appKey: 'my-app', tag: '9.9.9' });
|
|
94
|
+
delegate.mockRestore();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
package/src/app/App.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Observable } from 'rxjs';
|
|
2
|
+
import { of } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
import type { IHttpClient } from '@equinor/fusion-framework-module-http';
|
|
5
|
+
|
|
6
|
+
import { AppClient } from '../AppClient.js';
|
|
7
|
+
import type { AppConfig, AppManifest, ConfigEnvironment } from '../types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* An {@link AppClient} that answers `getAppManifest` and `getAppConfig` for one
|
|
11
|
+
* known app locally, delegating everything else — other app keys, tagged
|
|
12
|
+
* requests, builds, settings — to the real client it wraps.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Whatever `client` was resolved to (a pre-configured http client, or one created
|
|
16
|
+
* through service discovery) still backs every method this class doesn't
|
|
17
|
+
* override, so pointing service discovery at a different registry or a real
|
|
18
|
+
* local mock server keeps working unchanged.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* builder.setClient(async ({ requireInstance }) => {
|
|
23
|
+
* const http = await requireInstance('http');
|
|
24
|
+
* return new MockAppClient(http.createClient('apps'), manifest, config);
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class MockAppClient extends AppClient {
|
|
29
|
+
#manifest: AppManifest;
|
|
30
|
+
#config?: AppConfig;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param client - The real {@link IHttpClient} to delegate all other requests to.
|
|
34
|
+
* @param manifest - The manifest to return for `getAppManifest({ appKey: manifest.appKey })`.
|
|
35
|
+
* @param config - The config to return for `getAppConfig({ appKey: manifest.appKey })`, if any.
|
|
36
|
+
*/
|
|
37
|
+
constructor(client: IHttpClient, manifest: AppManifest, config?: AppConfig) {
|
|
38
|
+
super(client);
|
|
39
|
+
this.#manifest = manifest;
|
|
40
|
+
this.#config = config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Answers with the manifest passed to the constructor when `args` matches
|
|
45
|
+
* this client's own app key and no tag; otherwise delegates to the real client.
|
|
46
|
+
*
|
|
47
|
+
* @param args - The app key and optional tag to resolve a manifest for.
|
|
48
|
+
* @returns An observable of the resolved {@link AppManifest}.
|
|
49
|
+
*/
|
|
50
|
+
override getAppManifest(args: { appKey: string; tag?: string }): Observable<AppManifest> {
|
|
51
|
+
return args.appKey === this.#manifest.appKey && args.tag === undefined
|
|
52
|
+
? of(this.#manifest)
|
|
53
|
+
: super.getAppManifest(args);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Answers with the config passed to the constructor when `args` matches this
|
|
58
|
+
* client's own app key and tag; otherwise delegates to the real client.
|
|
59
|
+
*
|
|
60
|
+
* @template TType - The shape of the config's `environment` data.
|
|
61
|
+
* @param args - The app key and optional tag to resolve config for.
|
|
62
|
+
* @returns An observable of the resolved {@link AppConfig}.
|
|
63
|
+
*/
|
|
64
|
+
override getAppConfig<TType extends ConfigEnvironment = ConfigEnvironment>(args: {
|
|
65
|
+
appKey: string;
|
|
66
|
+
tag?: string;
|
|
67
|
+
}): Observable<AppConfig<TType>> {
|
|
68
|
+
// config is fetched against the manifest's own build version, not an explicit override tag,
|
|
69
|
+
// so a matching tag is treated the same as an absent one -- an explicit empty string is not
|
|
70
|
+
const isOwnTag = args.tag === undefined || args.tag === this.#manifest.build?.version;
|
|
71
|
+
return args.appKey === this.#manifest.appKey && isOwnTag && this.#config
|
|
72
|
+
? of(this.#config as AppConfig<TType>)
|
|
73
|
+
: super.getAppConfig(args);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default MockAppClient;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test doubles for the app module.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Imported from `@equinor/fusion-framework-module-app/mock`, so the mock ships
|
|
6
|
+
* and versions with the implementation it stands in for.
|
|
7
|
+
*
|
|
8
|
+
* This entry point has no dependency on any test runner.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { MockAppClient } from './MockAppClient.js';
|
package/src/schemas.ts
CHANGED
|
@@ -60,6 +60,26 @@ const ApiApplicationPersonSchema = z.object({
|
|
|
60
60
|
.nullish(),
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Schema for validating the options object on application builds.
|
|
65
|
+
*
|
|
66
|
+
* Known properties:
|
|
67
|
+
* - `contextRouting` (optional): Routing strategy for context — `'path'` or `'query'`.
|
|
68
|
+
*
|
|
69
|
+
* The schema uses `.catchall(z.unknown())` to permit additional properties
|
|
70
|
+
* at runtime without requiring them to be declared here. Add known properties
|
|
71
|
+
* to this schema as they are introduced.
|
|
72
|
+
*/
|
|
73
|
+
export const FrameworkOptionsSchema = z
|
|
74
|
+
.object({
|
|
75
|
+
contextRouting: z
|
|
76
|
+
.enum(['path', 'query'], {
|
|
77
|
+
message: "The routing strategy for context, which can be either 'path' or 'query'.",
|
|
78
|
+
})
|
|
79
|
+
.nullish(),
|
|
80
|
+
})
|
|
81
|
+
.catchall(z.unknown());
|
|
82
|
+
|
|
63
83
|
/**
|
|
64
84
|
* Schema for validating the structure of an API application build.
|
|
65
85
|
*
|
|
@@ -75,6 +95,7 @@ const ApiApplicationPersonSchema = z.object({
|
|
|
75
95
|
* - `githubRepo`: An optional GitHub repository associated with the build.
|
|
76
96
|
* - `projectPage`: An optional project page URL.
|
|
77
97
|
* - `allowedExtensions`: An optional array of allowed extensions for the build.
|
|
98
|
+
* - `options`: An optional record of additional build options, where the key is a string and the value can be of any type.
|
|
78
99
|
* - `uploadedBy`: An optional schema for the person who uploaded the build.
|
|
79
100
|
*/
|
|
80
101
|
// Deliberately co-located with ApiApplicationPersonSchema, which it depends on
|
|
@@ -90,6 +111,9 @@ export const ApiApplicationBuildSchema = z.object({
|
|
|
90
111
|
commitSha: z.string().nullish(),
|
|
91
112
|
githubRepo: z.string().nullish(),
|
|
92
113
|
projectPage: z.string().nullish(),
|
|
114
|
+
options: FrameworkOptionsSchema.nullish().default({
|
|
115
|
+
contextRouting: 'path',
|
|
116
|
+
}),
|
|
93
117
|
allowedExtensions: z.array(z.string()).nullish(),
|
|
94
118
|
uploadedBy: ApiApplicationPersonSchema.nullish(),
|
|
95
119
|
});
|
package/src/types.ts
CHANGED
|
@@ -117,6 +117,37 @@ export type RouteSchemaEntry = [
|
|
|
117
117
|
},
|
|
118
118
|
];
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Typed representation of the options object.
|
|
122
|
+
*
|
|
123
|
+
* Only known keys are declared in the type — additional properties are
|
|
124
|
+
* handled by the Zod schema at runtime. Keeping the type narrow avoids
|
|
125
|
+
* structural incompatibilities with `RecursivePartial<AppManifest>`.
|
|
126
|
+
*/
|
|
127
|
+
export type FrameworkOptions = {
|
|
128
|
+
/**
|
|
129
|
+
* Declares how the portal should encode this app's active context into the URL.
|
|
130
|
+
*
|
|
131
|
+
* Read by the `@equinor/fusion-framework-plugin-context-navigation` plugin
|
|
132
|
+
* to select the correct adapter at runtime.
|
|
133
|
+
*
|
|
134
|
+
* - `'path'` — context id as a path segment: `/apps/{appKey}/{contextId}/...`
|
|
135
|
+
* - `'query'` — context id as a query parameter: `/apps/{appKey}/...?$contextId={id}`
|
|
136
|
+
* - `null` — explicitly opt out of automatic context-to-URL encoding
|
|
137
|
+
* - `undefined` (omitted) — falls back to `'path'` (the default adapter)
|
|
138
|
+
*
|
|
139
|
+
* Apps with custom URL shapes should omit this field and register
|
|
140
|
+
* `setContextPathExtractor` / `setContextPathGenerator` hooks instead —
|
|
141
|
+
* the custom adapter picks those up automatically.
|
|
142
|
+
*
|
|
143
|
+
* Set this in the app manifest's `build.options.contextRouting` field,
|
|
144
|
+
* either in `app.manifest.config.ts` or via the app service manifest.
|
|
145
|
+
*
|
|
146
|
+
* @default undefined (falls back to path adapter)
|
|
147
|
+
*/
|
|
148
|
+
contextRouting?: 'path' | 'query' | null;
|
|
149
|
+
};
|
|
150
|
+
|
|
120
151
|
/**
|
|
121
152
|
* Build metadata returned by the app service for a specific application version.
|
|
122
153
|
* Contains the script entry point, asset path, tags, and optional CI metadata.
|
|
@@ -133,6 +164,7 @@ export type AppBuildManifest = {
|
|
|
133
164
|
githubRepo?: Nullable<string>;
|
|
134
165
|
projectPage?: Nullable<string>;
|
|
135
166
|
annotations?: Nullable<Record<string, string>>;
|
|
167
|
+
options?: Nullable<FrameworkOptions>;
|
|
136
168
|
allowedExtensions?: Nullable<string[]>;
|
|
137
169
|
uploadedBy?: Nullable<AppOwner>;
|
|
138
170
|
};
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '8.0.
|
|
2
|
+
export const version = '8.1.0-next.0';
|
package/tsconfig.json
CHANGED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineProject } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
import { name, version } from './package.json' with { type: 'json' };
|
|
4
|
+
|
|
5
|
+
export default defineProject({
|
|
6
|
+
test: {
|
|
7
|
+
environment: 'node',
|
|
8
|
+
include: ['src/__tests__/**/*.test.ts'],
|
|
9
|
+
name: `${name}@${version}`,
|
|
10
|
+
},
|
|
11
|
+
});
|