@equinor/fusion-framework-module-service-discovery
Advanced tools
Comparing version 7.1.13 to 8.0.0-alpha-17c4eed176e5b4a0fcf867ed245471d3863af237
# Change Log | ||
## 8.0.0-alpha-17c4eed176e5b4a0fcf867ed245471d3863af237 | ||
### Major Changes | ||
- [#2459](https://github.com/equinor/fusion-framework/pull/2459) [`15152e4`](https://github.com/equinor/fusion-framework/commit/15152e413c054a5f57af93211a470c98c7696caa) Thanks [@odinr](https://github.com/odinr)! - The Service Discovery module has been totally revamped to provide a more flexible and robust solution for service discovery in the Fusion Framework. | ||
The module now relies on the Fusion Service Discovery API to fetch services and their configurations, which allows for more dynamic and real-time service discovery. | ||
The module now follows the "best practices" for configuration and usage, and it is now easier to configure and use the Service Discovery module in your applications. But this also means that the module has breaking changes that may require updates to existing implementations. | ||
> [!NOTE] | ||
> This module can still be configured to resolve custom services, as long as the client implements the `IServiceDiscoveryClient` interface. | ||
**Documentation Updates** | ||
- The README file has been updated to reflect the new configuration options and usage patterns for the Service Discovery module. | ||
- Added sections for simple and advanced configurations, including examples of how to override the default HTTP client key and set a custom service discovery client. | ||
**Code Changes:** | ||
- 🔨 package.json: Added `zod` as a new dependency for schema validation. | ||
- 💫 api-schema.ts: Added schema for the expected response from the `ServiceProviderClient` | ||
- 💫 client.ts: Created `serviceResponseSelector` for parsing and validating client respons. | ||
- 🔨 client.ts: Updated `IServiceDiscoveryClient` interface to include methods for resolving services and fetching services from the API. | ||
- 🔨 client.ts: Updated `ServiceDiscoveryClient` to use the new `serviceResponseSelector` | ||
- 💫 configurator.ts: Introduced new methods for setting and configuring the service discovery client. | ||
- 🔨 configurator.ts: Updated `ServiceDiscoveryConfigurator` to extend the `BaseConfigBuilder` | ||
- 🔨 configurator.ts: Added error handling and validation for required configurations. | ||
**BREAKING CHANGES:** | ||
- The type `Service` has deprecated the `defaultScopes` property in favor of `scopes`. | ||
- The `IServiceDiscoveryClient` interface has been updated, which may require changes in implementations that use this interface. | ||
- The `ServiceDiscoveryConfigurator` now extends `BaseConfigBuilder`, which will affect existing configurations. | ||
- The `ServiceDiscoveryProvider.resolveServices` method now returns `Service[]` (previously `Environment`). | ||
> [!NOTE] | ||
> Only the `ServiceDiscoveryProvider.resolveServices` should affect end-users, | ||
> as it changes the return type of the method. | ||
> The other changes are internal and should not affect existing implementations. | ||
**Consumer Migration Guide:** | ||
`defaultScopes` has been replaced with `scopes` in the `Service` type. Update your code to use the new property. | ||
If you are using the `ServiceDiscoveryProvider.resolveServices` method, update your code to expect an array of `Service` objects instead of an `Environment` object. | ||
```typescript | ||
// Before | ||
const { services } = await serviceDiscoveryProvider.resolveServices('my-service'); | ||
// After | ||
const services = await serviceDiscoveryProvider.resolveServices('my-service'); | ||
``` | ||
> [!WARNING] | ||
> The preious `Environment` object had a `clientId` property, which is now removed, since every service can have its own client id, hence the `scopes` property in the `Service` object. | ||
**Configuration Migration Guide:** | ||
> If you are consuming the `@equinor/fusion-framework` and only configuring the http client, no changes are required. | ||
If you are manually enabling the Service Discovery module, update your configuration to use the new methods provided by `ServiceDiscoveryConfigurator`. | ||
Refer to the updated README for detailed configuration examples and usage patterns. | ||
> [!WARNING] | ||
> The `ServiceDiscoveryConfigurator` now extends `BaseConfigBuilder`, which means that the configuration methods have changed. | ||
### Patch Changes | ||
- Updated dependencies [[`c776845`](https://github.com/equinor/fusion-framework/commit/c776845e753acf4a0bceda1c59d31e5939c44c31), [`2644b3d`](https://github.com/equinor/fusion-framework/commit/2644b3d63939aede736a3b1950db32dbd487877d)]: | ||
- @equinor/fusion-framework-module-http@6.1.0-alpha-17c4eed176e5b4a0fcf867ed245471d3863af237 | ||
- @equinor/fusion-framework-module@4.3.5-alpha-17c4eed176e5b4a0fcf867ed245471d3863af237 | ||
## 7.1.13 | ||
@@ -4,0 +76,0 @@ |
@@ -10,7 +10,2 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
}; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
@@ -22,61 +17,51 @@ if (kind === "m") throw new TypeError("Private method is not writable"); | ||
}; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var _ServiceDiscoveryClient_query; | ||
import { Query } from '@equinor/fusion-query'; | ||
import { firstValueFrom } from 'rxjs'; | ||
import { firstValueFrom, from, lastValueFrom, map } from 'rxjs'; | ||
import { jsonSelector } from '@equinor/fusion-framework-module-http/selectors'; | ||
import { ApiServices } from './api-schema'; | ||
const queryKey = 'services'; | ||
/** | ||
* Transforms a Response object into an ObservableInput of Service arrays. | ||
* | ||
* @param response - The Response object to be transformed. | ||
* @returns An ObservableInput that emits an array of Service objects. | ||
*/ | ||
const serviceResponseSelector = (response) => | ||
// parse response by using the jsonSelector | ||
from(jsonSelector(response)).pipe( | ||
// parse and validate the response | ||
map((value) => ApiServices.default([]).parse(value))); | ||
export class ServiceDiscoveryClient { | ||
get environment() { | ||
var _a; | ||
const env = (_a = __classPrivateFieldGet(this, _ServiceDiscoveryClient_query, "f").cache.getItem(queryKey)) === null || _a === void 0 ? void 0 : _a.value; | ||
if (!env) { | ||
throw Error('no cached environment found'); | ||
} | ||
return env; | ||
} | ||
constructor({ http, endpoint }) { | ||
// TODO - make better | ||
_ServiceDiscoveryClient_query.set(this, void 0); | ||
this.http = http; | ||
this.endpoint = endpoint; | ||
// setup api handler (queue and cache) | ||
__classPrivateFieldSet(this, _ServiceDiscoveryClient_query, new Query({ | ||
client: { | ||
fn: () => http.fetch$(endpoint, { selector: this.selector.bind(this) }), | ||
fn: () => http.fetch$(endpoint !== null && endpoint !== void 0 ? endpoint : '', { selector: serviceResponseSelector }), | ||
}, | ||
key: () => queryKey, | ||
// Cache for 5 minutes | ||
expire: 5 * 60 * 1000, | ||
// queueOperator: (_) => ($) => $.pipe(throttleTime(100)), | ||
}), "f"); | ||
} | ||
fetchEnvironment() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return firstValueFrom(Query.extractQueryValue(__classPrivateFieldGet(this, _ServiceDiscoveryClient_query, "f").query(undefined, { cache: { suppressInvalid: true } }))); | ||
}); | ||
resolveServices(allow_cache) { | ||
const fn = allow_cache ? firstValueFrom : lastValueFrom; | ||
return fn(Query.extractQueryValue(__classPrivateFieldGet(this, _ServiceDiscoveryClient_query, "f").query())); | ||
} | ||
selector(response) { | ||
resolveService(key, allow_cache) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const env = (yield response.json()); | ||
const services = env.services.reduce((acc, service) => { | ||
var _a; | ||
return Object.assign(acc, { | ||
[service.key]: { | ||
clientId: env.clientId, | ||
uri: service.uri, | ||
defaultScopes: (_a = service.defaultScopes) !== null && _a !== void 0 ? _a : [env.clientId + '/.default'], | ||
}, | ||
}); | ||
}, {}); | ||
return Object.assign(Object.assign({}, env), { services }); | ||
}); | ||
} | ||
resolveService(key) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const { services } = yield this.fetchEnvironment(); | ||
const service = services[key]; | ||
return service; | ||
const services = yield this.resolveServices(allow_cache); | ||
const service = services.find((s) => s.key === key); | ||
if (!service) { | ||
throw Error(`Failed to resolve service, invalid key [${key}]`); | ||
} | ||
catch (err) { | ||
console.error(err); | ||
throw err; | ||
} | ||
return service; | ||
}); | ||
@@ -83,0 +68,0 @@ } |
@@ -10,28 +10,92 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
}; | ||
export class ServiceDiscoveryConfigurator { | ||
constructor(args) { | ||
this.clientKey = args.clientKey; | ||
this.endpoint = args.endpoint; | ||
this.clientCtor = args.clientCtor; | ||
} | ||
createHttpClientClient(http) { | ||
import { from, lastValueFrom } from 'rxjs'; | ||
import { BaseConfigBuilder, } from '@equinor/fusion-framework-module'; | ||
import { ServiceDiscoveryClient } from './client'; | ||
export class ServiceDiscoveryConfigurator extends BaseConfigBuilder { | ||
_createConfig(init, initial) { | ||
const _super = Object.create(null, { | ||
_createConfig: { get: () => super._createConfig } | ||
}); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.clientKey) { | ||
throw Error('no http client for service discovery is provided!'); | ||
if (!init.hasModule('http')) { | ||
throw new Error('http module is required'); | ||
} | ||
return http.createClient(this.clientKey); | ||
// check if http module has a client with key 'service_discovery' | ||
const httpProvider = yield init.requireInstance('http'); | ||
if (httpProvider.hasClient('service_discovery')) { | ||
this.configureServiceDiscoveryClientByClientKey('service_discovery'); | ||
} | ||
// convert parent to promise | ||
return lastValueFrom(from(_super._createConfig.call(this, init, initial))); | ||
}); | ||
} | ||
createClient(http) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.endpoint) { | ||
throw Error('no endpoint defined!'); | ||
/** | ||
* Processes the service discovery configuration. | ||
* | ||
* @param config - A partial configuration object for service discovery. | ||
* @param init - Initialization arguments for the configuration builder callback. | ||
* @returns An observable input of the complete service discovery configuration. | ||
* @throws Will throw an error if `discoveryClient` is not configured. | ||
*/ | ||
_processConfig(config, init) { | ||
if (!config.discoveryClient) { | ||
throw new Error('discoveryClient is required, please configure it'); | ||
} | ||
return super._processConfig(config, init); | ||
} | ||
/** | ||
* Sets the service discovery client. | ||
* | ||
* @param discoveryClient - An instance of `IServiceDiscoveryClient` or a callback function that returns an instance of `IServiceDiscoveryClient`. | ||
* | ||
* This method configures the service discovery client by either directly setting the provided instance or by invoking the provided callback function to obtain the instance. | ||
*/ | ||
setServiceDiscoveryClient(discoveryClient) { | ||
this._set('discoveryClient', typeof discoveryClient === 'function' ? discoveryClient : () => __awaiter(this, void 0, void 0, function* () { return discoveryClient; })); | ||
} | ||
/** | ||
* Configures the Service Discovery Client with the provided configuration callback. | ||
* | ||
* @param configCallback - A callback function that takes an argument of type `{ httpClient: IHttpClient; endpoint?: string }` | ||
* and returns a configuration object. The `httpClient` is required, while the `endpoint` is optional. | ||
* | ||
* @throws {Error} Throws an error if `httpClient` is not provided in the configuration. | ||
*/ | ||
configureServiceDiscoveryClient(configCallback) { | ||
this.setServiceDiscoveryClient((args) => __awaiter(this, void 0, void 0, function* () { | ||
var _a; | ||
const { httpClient, endpoint } = (_a = (yield lastValueFrom(from(configCallback(args))))) !== null && _a !== void 0 ? _a : {}; | ||
if (httpClient) { | ||
return new ServiceDiscoveryClient({ | ||
http: httpClient, | ||
endpoint, | ||
}); | ||
} | ||
return new this.clientCtor({ | ||
http, | ||
endpoint: this.endpoint, | ||
}); | ||
}); | ||
throw Error('httpClient is required'); | ||
})); | ||
} | ||
/** | ||
* Configures a service discovery client using the provided client key and optional endpoint. | ||
* | ||
* @param clientKey - The key used to identify the client. | ||
* @param endpoint - An optional endpoint to be used by the service discovery client. | ||
* | ||
* @remarks | ||
* The http module must have a configured client which match provided `clientKey`. | ||
* | ||
* This method sets up the service discovery client by requiring an HTTP provider instance, | ||
* creating an HTTP client with the given client key, and returning an object containing | ||
* the HTTP client and the optional endpoint. | ||
*/ | ||
configureServiceDiscoveryClientByClientKey(clientKey, endpoint) { | ||
this.configureServiceDiscoveryClient((_a) => __awaiter(this, [_a], void 0, function* ({ requireInstance }) { | ||
const httpProvider = yield requireInstance('http'); | ||
const httpClient = httpProvider.createClient(clientKey); | ||
return { | ||
httpClient, | ||
endpoint, | ||
}; | ||
})); | ||
} | ||
} | ||
//# sourceMappingURL=configurator.js.map |
@@ -8,3 +8,3 @@ /** | ||
export { ServiceDiscoveryProvider } from './provider'; | ||
export { default, setupServiceDiscoveryModule } from './module'; | ||
export { default, configureServiceDiscovery, enableServiceDiscovery, } from './module'; | ||
//# sourceMappingURL=index.js.map |
@@ -12,26 +12,82 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
import { ServiceDiscoveryProvider } from './provider'; | ||
import { ServiceDiscoveryClient } from './client'; | ||
import { moduleName } from './constants'; | ||
export const moduleName = 'serviceDiscovery'; | ||
/** | ||
* Configure http-client | ||
* Represents the Service Discovery module configuration. | ||
* | ||
* @remarks | ||
* This module is responsible for configuring and initializing the service discovery mechanism. | ||
* It ensures that the service discovery configuration is created and the necessary dependencies | ||
* are initialized before providing the service discovery provider. | ||
* | ||
* @returns {Promise<ServiceDiscoveryProvider>} - Returns a promise that resolves to the service discovery provider. | ||
* | ||
* @remarks | ||
* The initialization process involves creating the service discovery configuration, which may include | ||
* inheriting configurations from a parent module. This pattern, while allowing reuse of cache and ensuring | ||
* up-to-date configurations, can be risky as it exposes the child module to potential breaking changes | ||
* from the parent module's client. | ||
* | ||
* Additionally, the service discovery module requires the HTTP module to be initialized before it can | ||
* function properly. | ||
*/ | ||
export const module = { | ||
name: moduleName, | ||
// deps: ['http'], | ||
configure: () => new ServiceDiscoveryConfigurator({ | ||
clientCtor: ServiceDiscoveryClient, | ||
clientKey: 'service_discovery', | ||
endpoint: '/_discovery/environments/current', | ||
}), | ||
initialize: (_a) => __awaiter(void 0, [_a], void 0, function* ({ config, requireInstance }) { | ||
configure: () => new ServiceDiscoveryConfigurator(), | ||
initialize: (init) => __awaiter(void 0, void 0, void 0, function* () { | ||
var _a; | ||
const { requireInstance, ref } = init; | ||
// create service discovery configuration | ||
const config = yield init.config.createConfigAsync(init, Object.assign({}, (_a = ref === null || ref === void 0 ? void 0 : ref.serviceDiscovery) === null || _a === void 0 ? void 0 : _a.config)); | ||
// service discovery requires http module to be initialized | ||
const httpModule = yield requireInstance('http'); | ||
const httpClient = yield config.createHttpClientClient(httpModule); | ||
const discoClient = yield config.createClient(httpClient); | ||
return new ServiceDiscoveryProvider(discoClient, httpModule); | ||
// return service discovery provider | ||
return new ServiceDiscoveryProvider(config, httpModule); | ||
}), | ||
}; | ||
export const setupServiceDiscoveryModule = (config, callback) => { | ||
callback(config.serviceDiscovery); | ||
/** | ||
* Configures the Service Discovery module. | ||
* | ||
* @template TRef - The type reference for the module configurator. | ||
* @param callback - A function that takes a `ServiceDiscoveryConfigurator` and returns a promise that resolves when the configuration is complete. | ||
* @returns An object implementing `IModuleConfigurator` for the `ServiceDiscoveryModule` with the provided configuration. | ||
* | ||
* @example | ||
* ```typescript | ||
* import { configureServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery'; | ||
* | ||
* const config = (configurator: ModuleConfigurator) => { | ||
* configurator.addConfig(configureServiceDiscovery(async (config) => { | ||
* // custom configuration | ||
* }); | ||
* ``` | ||
*/ | ||
export const configureServiceDiscovery = (callback) => ({ | ||
module, | ||
configure: (config) => callback(config), | ||
}); | ||
/** | ||
* Enables the service discovery module by adding its configuration to the provided configurator. | ||
* | ||
* @param configurator - The configurator to which the service discovery configuration will be added. | ||
* @param callback - An optional callback function that can be used to customize the service discovery configuration. | ||
* | ||
* @example | ||
* ```typescript | ||
* import { enableServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery'; | ||
* | ||
* const config = (configurator: ModuleConfigurator) => { | ||
* // simple | ||
* enableServiceDiscovery(configurator); | ||
* | ||
* // with custom configuration | ||
* enableServiceDiscovery(configurator, async (config) => { | ||
* config.configureServiceDiscoveryClientByClientKey('service-discovery-custom'); | ||
* }); | ||
* }; | ||
* ``` | ||
*/ | ||
export const enableServiceDiscovery = (configurator, callback) => { | ||
configurator.addConfig(configureServiceDiscovery(callback !== null && callback !== void 0 ? callback : (() => Promise.resolve()))); | ||
}; | ||
export default module; | ||
//# sourceMappingURL=module.js.map |
@@ -12,15 +12,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
export class ServiceDiscoveryProvider { | ||
constructor(_client, _http) { | ||
this._client = _client; | ||
constructor(config, _http) { | ||
this.config = config; | ||
this._http = _http; | ||
} | ||
get environment() { | ||
return this._client.environment; | ||
} | ||
resolveServices() { | ||
return this._client.fetchEnvironment(); | ||
return this.config.discoveryClient.resolveServices(); | ||
} | ||
resolveService(key) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return this._client.resolveService(key); | ||
return this.config.discoveryClient.resolveService(key); | ||
}); | ||
@@ -34,3 +31,3 @@ } | ||
} | ||
return this._http.createClient(Object.assign(Object.assign({}, opt), { baseUri: service.uri, defaultScopes: service.defaultScopes })); | ||
return this._http.createClient(Object.assign(Object.assign({}, opt), { baseUri: service.uri, defaultScopes: service.scopes })); | ||
}); | ||
@@ -43,3 +40,3 @@ } | ||
: serviceName; | ||
const { uri: baseUri, defaultScopes } = yield this.resolveService(key); | ||
const { uri: baseUri, scopes: defaultScopes } = yield this.resolveService(key); | ||
config.addConfig(configureHttpClient(alias, { baseUri, defaultScopes })); | ||
@@ -46,0 +43,0 @@ }); |
import { IHttpClient } from '@equinor/fusion-framework-module-http'; | ||
import { Environment, Service } from './types'; | ||
import type { Service } from './types'; | ||
export interface IServiceDiscoveryClient { | ||
readonly environment: Environment; | ||
resolveService(key: string): Promise<Service>; | ||
fetchEnvironment(): Promise<Environment>; | ||
/** | ||
* Resolves a service by key | ||
* @param key - The key of the service to resolve | ||
*/ | ||
resolveService(key: string, allow_cache?: boolean): Promise<Service>; | ||
/** | ||
* Fetches services from the service discovery API. | ||
*/ | ||
resolveServices(allow_cache?: boolean): Promise<Service[]>; | ||
} | ||
export interface IServiceDiscoveryClientCtor<TEnv extends Environment = Environment> { | ||
new (args: ServiceDiscoveryClientCtorArgs): ServiceDiscoveryClient<TEnv>; | ||
export interface IServiceDiscoveryClientCtor { | ||
new (args: ServiceDiscoveryClientCtorArgs): ServiceDiscoveryClient; | ||
} | ||
type ServiceDiscoveryClientCtorArgs = { | ||
http: IHttpClient; | ||
endpoint: string; | ||
endpoint?: string; | ||
}; | ||
export declare class ServiceDiscoveryClient<T extends Environment = Environment> implements IServiceDiscoveryClient { | ||
export declare class ServiceDiscoveryClient implements IServiceDiscoveryClient { | ||
#private; | ||
endpoint: string; | ||
http: IHttpClient; | ||
get environment(): T; | ||
/** Endpoint for fetching services from API */ | ||
readonly endpoint?: string; | ||
/** HTTP client for fetching services */ | ||
readonly http: IHttpClient; | ||
constructor({ http, endpoint }: ServiceDiscoveryClientCtorArgs); | ||
fetchEnvironment(): Promise<T>; | ||
selector(response: Response): Promise<T>; | ||
resolveService(key: string): Promise<Service>; | ||
resolveServices(allow_cache?: boolean): Promise<Service[]>; | ||
resolveService(key: string, allow_cache?: boolean): Promise<Service>; | ||
} | ||
export {}; |
@@ -1,23 +0,54 @@ | ||
import { ModulesInstanceType } from '@equinor/fusion-framework-module'; | ||
import { HttpModule, IHttpClient } from '@equinor/fusion-framework-module-http'; | ||
import type { IServiceDiscoveryClient, IServiceDiscoveryClientCtor } from './client'; | ||
export interface IServiceDiscoveryConfigurator { | ||
/** name of HttpClient */ | ||
clientKey?: string; | ||
endpoint?: string; | ||
clientCtor: IServiceDiscoveryClientCtor; | ||
createHttpClientClient: (http: ModulesInstanceType<[HttpModule]>['http']) => Promise<IHttpClient>; | ||
createClient: (http: IHttpClient) => Promise<IServiceDiscoveryClient>; | ||
import { ObservableInput } from 'rxjs'; | ||
import { BaseConfigBuilder, type ConfigBuilderCallback, type ConfigBuilderCallbackArgs } from '@equinor/fusion-framework-module'; | ||
import { type IHttpClient } from '@equinor/fusion-framework-module-http'; | ||
import { type IServiceDiscoveryClient } from './client'; | ||
export interface ServiceDiscoveryConfig { | ||
/** Service Discovery client */ | ||
discoveryClient: IServiceDiscoveryClient; | ||
} | ||
export declare class ServiceDiscoveryConfigurator implements IServiceDiscoveryConfigurator { | ||
clientKey?: string; | ||
endpoint?: string; | ||
clientCtor: IServiceDiscoveryClientCtor; | ||
constructor(args: { | ||
clientCtor: IServiceDiscoveryClientCtor; | ||
clientKey?: string; | ||
export declare class ServiceDiscoveryConfigurator extends BaseConfigBuilder<ServiceDiscoveryConfig> { | ||
protected _createConfig(init: ConfigBuilderCallbackArgs, initial?: Partial<ServiceDiscoveryConfig> | undefined): Promise<ServiceDiscoveryConfig>; | ||
/** | ||
* Processes the service discovery configuration. | ||
* | ||
* @param config - A partial configuration object for service discovery. | ||
* @param init - Initialization arguments for the configuration builder callback. | ||
* @returns An observable input of the complete service discovery configuration. | ||
* @throws Will throw an error if `discoveryClient` is not configured. | ||
*/ | ||
protected _processConfig(config: Partial<ServiceDiscoveryConfig>, init: ConfigBuilderCallbackArgs): ObservableInput<ServiceDiscoveryConfig>; | ||
/** | ||
* Sets the service discovery client. | ||
* | ||
* @param discoveryClient - An instance of `IServiceDiscoveryClient` or a callback function that returns an instance of `IServiceDiscoveryClient`. | ||
* | ||
* This method configures the service discovery client by either directly setting the provided instance or by invoking the provided callback function to obtain the instance. | ||
*/ | ||
setServiceDiscoveryClient(discoveryClient: IServiceDiscoveryClient | ConfigBuilderCallback<IServiceDiscoveryClient>): void; | ||
/** | ||
* Configures the Service Discovery Client with the provided configuration callback. | ||
* | ||
* @param configCallback - A callback function that takes an argument of type `{ httpClient: IHttpClient; endpoint?: string }` | ||
* and returns a configuration object. The `httpClient` is required, while the `endpoint` is optional. | ||
* | ||
* @throws {Error} Throws an error if `httpClient` is not provided in the configuration. | ||
*/ | ||
configureServiceDiscoveryClient(configCallback: ConfigBuilderCallback<{ | ||
httpClient: IHttpClient; | ||
endpoint?: string; | ||
}); | ||
createHttpClientClient(http: ModulesInstanceType<[HttpModule]>['http']): Promise<IHttpClient>; | ||
createClient(http: IHttpClient): Promise<IServiceDiscoveryClient>; | ||
}>): void; | ||
/** | ||
* Configures a service discovery client using the provided client key and optional endpoint. | ||
* | ||
* @param clientKey - The key used to identify the client. | ||
* @param endpoint - An optional endpoint to be used by the service discovery client. | ||
* | ||
* @remarks | ||
* The http module must have a configured client which match provided `clientKey`. | ||
* | ||
* This method sets up the service discovery client by requiring an HTTP provider instance, | ||
* creating an HTTP client with the given client key, and returning an object containing | ||
* the HTTP client and the optional endpoint. | ||
*/ | ||
configureServiceDiscoveryClientByClientKey(clientKey: string, endpoint?: string): void; | ||
} |
@@ -6,4 +6,4 @@ /** | ||
export * from './types'; | ||
export { IServiceDiscoveryConfigurator, ServiceDiscoveryConfigurator } from './configurator'; | ||
export { ServiceDiscoveryConfigurator } from './configurator'; | ||
export { IServiceDiscoveryProvider, ServiceDiscoveryProvider } from './provider'; | ||
export { default, ServiceDiscoveryModule, setupServiceDiscoveryModule } from './module'; | ||
export { default, ServiceDiscoveryModule, configureServiceDiscovery, enableServiceDiscovery, } from './module'; |
@@ -1,14 +0,69 @@ | ||
import { IServiceDiscoveryConfigurator } from './configurator'; | ||
import { ServiceDiscoveryConfigurator } from './configurator'; | ||
import { IServiceDiscoveryProvider } from './provider'; | ||
import type { Module, ModulesConfigType } from '@equinor/fusion-framework-module'; | ||
import type { IModuleConfigurator, Module, ModulesConfigurator } from '@equinor/fusion-framework-module'; | ||
import type { HttpModule } from '@equinor/fusion-framework-module-http'; | ||
import { moduleName } from './constants'; | ||
export type ServiceDiscoveryModule = Module<typeof moduleName, IServiceDiscoveryProvider, IServiceDiscoveryConfigurator, [ | ||
export declare const moduleName = "serviceDiscovery"; | ||
export type ServiceDiscoveryModule = Module<typeof moduleName, IServiceDiscoveryProvider, ServiceDiscoveryConfigurator, [ | ||
HttpModule | ||
]>; | ||
/** | ||
* Configure http-client | ||
* Represents the Service Discovery module configuration. | ||
* | ||
* @remarks | ||
* This module is responsible for configuring and initializing the service discovery mechanism. | ||
* It ensures that the service discovery configuration is created and the necessary dependencies | ||
* are initialized before providing the service discovery provider. | ||
* | ||
* @returns {Promise<ServiceDiscoveryProvider>} - Returns a promise that resolves to the service discovery provider. | ||
* | ||
* @remarks | ||
* The initialization process involves creating the service discovery configuration, which may include | ||
* inheriting configurations from a parent module. This pattern, while allowing reuse of cache and ensuring | ||
* up-to-date configurations, can be risky as it exposes the child module to potential breaking changes | ||
* from the parent module's client. | ||
* | ||
* Additionally, the service discovery module requires the HTTP module to be initialized before it can | ||
* function properly. | ||
*/ | ||
export declare const module: ServiceDiscoveryModule; | ||
export declare const setupServiceDiscoveryModule: (config: ModulesConfigType<[ServiceDiscoveryModule]>, callback: (config: IServiceDiscoveryConfigurator) => void) => void | Promise<void>; | ||
/** | ||
* Configures the Service Discovery module. | ||
* | ||
* @template TRef - The type reference for the module configurator. | ||
* @param callback - A function that takes a `ServiceDiscoveryConfigurator` and returns a promise that resolves when the configuration is complete. | ||
* @returns An object implementing `IModuleConfigurator` for the `ServiceDiscoveryModule` with the provided configuration. | ||
* | ||
* @example | ||
* ```typescript | ||
* import { configureServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery'; | ||
* | ||
* const config = (configurator: ModuleConfigurator) => { | ||
* configurator.addConfig(configureServiceDiscovery(async (config) => { | ||
* // custom configuration | ||
* }); | ||
* ``` | ||
*/ | ||
export declare const configureServiceDiscovery: <TRef>(callback: (config: ServiceDiscoveryConfigurator) => Promise<void>) => IModuleConfigurator<ServiceDiscoveryModule, TRef>; | ||
/** | ||
* Enables the service discovery module by adding its configuration to the provided configurator. | ||
* | ||
* @param configurator - The configurator to which the service discovery configuration will be added. | ||
* @param callback - An optional callback function that can be used to customize the service discovery configuration. | ||
* | ||
* @example | ||
* ```typescript | ||
* import { enableServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery'; | ||
* | ||
* const config = (configurator: ModuleConfigurator) => { | ||
* // simple | ||
* enableServiceDiscovery(configurator); | ||
* | ||
* // with custom configuration | ||
* enableServiceDiscovery(configurator, async (config) => { | ||
* config.configureServiceDiscoveryClientByClientKey('service-discovery-custom'); | ||
* }); | ||
* }; | ||
* ``` | ||
*/ | ||
export declare const enableServiceDiscovery: (configurator: ModulesConfigurator<[HttpModule]>, callback?: (config: ServiceDiscoveryConfigurator) => Promise<void>) => void; | ||
declare module '@equinor/fusion-framework-module' { | ||
@@ -15,0 +70,0 @@ interface Modules { |
@@ -1,17 +0,50 @@ | ||
import { IServiceDiscoveryClient } from './client'; | ||
import type { ModulesConfigurator, ModuleType } from '@equinor/fusion-framework-module'; | ||
import type { HttpClientOptions, HttpModule, IHttpClient } from '@equinor/fusion-framework-module-http'; | ||
import type { Environment, Service } from './types'; | ||
import type { Service } from './types'; | ||
import { ServiceDiscoveryConfig } from './configurator'; | ||
export interface IServiceDiscoveryProvider { | ||
/** | ||
* Try to resolve services for requested key | ||
* Resolves a service by key | ||
* @param key - The key of the service to resolve | ||
*/ | ||
resolveService(key: string): Promise<Service>; | ||
/** | ||
* service environment | ||
* this might throw error if no environment is loaded | ||
* Fetch all services | ||
*/ | ||
readonly environment: Environment; | ||
resolveServices(): Promise<Environment>; | ||
resolveServices(): Promise<Service[]>; | ||
/** | ||
* create http client for a service | ||
* @param name key of the service | ||
* @param opt http client options | ||
* | ||
* @example | ||
* ```typescript | ||
* const myClient = await serviceDiscovery.createClient('my-service', {}); | ||
* ``` | ||
*/ | ||
createClient(name: string, opt?: Omit<HttpClientOptions, 'baseUri' | 'defaultScopes' | 'ctor'>): Promise<IHttpClient>; | ||
/** | ||
* Used in the framework to configure a http client which is resolved by service discovery | ||
* | ||
* @deprecating this method will be reworked in later versions, please don`t use! | ||
* | ||
* @param serviceName key of the service or an object with key and alias | ||
* @param config http client configurator | ||
* | ||
* @example | ||
* ```typescript | ||
* configure = ( | ||
* configurator: ModuleConfigBuilder<[HttpModule]>, | ||
* ref: ModuleInstanceType<ServiceDiscovery> | ||
* )=> { | ||
* ref.serviceDiscovery.configureClient( | ||
* // use `my-service` from service discovery | ||
* // and register it as `foo` in the http module | ||
* { key: 'my-service', alias: 'foo'}, | ||
* configurator | ||
* ); | ||
* }); | ||
* | ||
* ``` | ||
*/ | ||
configureClient(serviceName: string | { | ||
@@ -21,9 +54,9 @@ key: string; | ||
}, config: ModulesConfigurator<[HttpModule]>): Promise<void>; | ||
readonly config: ServiceDiscoveryConfig; | ||
} | ||
export declare class ServiceDiscoveryProvider implements IServiceDiscoveryProvider { | ||
protected readonly _client: IServiceDiscoveryClient; | ||
readonly config: ServiceDiscoveryConfig; | ||
protected readonly _http: ModuleType<HttpModule>; | ||
constructor(_client: IServiceDiscoveryClient, _http: ModuleType<HttpModule>); | ||
get environment(): Environment; | ||
resolveServices(): Promise<Environment>; | ||
constructor(config: ServiceDiscoveryConfig, _http: ModuleType<HttpModule>); | ||
resolveServices(): Promise<Service[]>; | ||
resolveService(key: string): Promise<Service>; | ||
@@ -30,0 +63,0 @@ createClient(name: string, opt?: Omit<HttpClientOptions, 'baseUri' | 'defaultScopes' | 'ctor'>): Promise<IHttpClient>; |
@@ -1,19 +0,12 @@ | ||
export type EnvironmentResponse = { | ||
clientId: string; | ||
services: Array<{ | ||
key: string; | ||
uri: string; | ||
defaultScopes?: Array<string>; | ||
}>; | ||
}; | ||
export type Environment = { | ||
type?: string; | ||
clientId: string; | ||
services: Record<string, Service>; | ||
}; | ||
export type Service = { | ||
name?: null | string; | ||
clientId?: string; | ||
key: string; | ||
uri: string; | ||
scopes?: string[]; | ||
id?: string; | ||
name?: string; | ||
tags?: string[]; | ||
/** | ||
* @deprecated use scopes instead | ||
*/ | ||
defaultScopes: string[]; | ||
}; |
{ | ||
"name": "@equinor/fusion-framework-module-service-discovery", | ||
"version": "7.1.13", | ||
"version": "8.0.0-alpha-17c4eed176e5b4a0fcf867ed245471d3863af237", | ||
"description": "", | ||
@@ -26,5 +26,6 @@ "main": "dist/esm/index.js", | ||
"rxjs": "^7.8.1", | ||
"@equinor/fusion-framework-module-http": "^6.0.3", | ||
"@equinor/fusion-query": "^5.1.3", | ||
"@equinor/fusion-framework-module": "^4.3.4" | ||
"zod": "^3.23.8", | ||
"@equinor/fusion-framework-module": "^4.3.5-alpha-17c4eed176e5b4a0fcf867ed245471d3863af237", | ||
"@equinor/fusion-framework-module-http": "^6.1.0-alpha-17c4eed176e5b4a0fcf867ed245471d3863af237", | ||
"@equinor/fusion-query": "^5.1.3" | ||
}, | ||
@@ -31,0 +32,0 @@ "devDependencies": { |
139
README.md
@@ -1,24 +0,133 @@ | ||
# Fusion Service Provider | ||
# Fusion Service Discovery Module | ||
This module is for resolving service endpoints from a service discovery service. | ||
> [!WARNING] | ||
> This module requires `@equinor/fusion-framework-module-http` to to create http clients, so the module must be enabled in runtime. | ||
## Configure | ||
```ts | ||
This module requires a http client to be configured. The http client should be configured with a key that is used to resolve the service endpoints. | ||
/** configure the client which should fetch service descriptions */ | ||
config.http.configureClient('my_service_discovery', { | ||
baseUri: 'https://foo.bar', | ||
defaultScopes: ['321312bab2-3213123bb-321312aa2/.default'], | ||
> [!NOTE] | ||
> The Service Discovery Module inherits configuration when used in sub-modules (ex. Application), which means that the http client should be configured in the root module (ex Portal). | ||
> | ||
> Skip this step if the http client is already configured. | ||
```typescript | ||
import { ModulesConfigurator } from '@equinor/fusion-framework-module'; | ||
const configurator = new ModulesConfigurator(); | ||
configurator.addConfig( | ||
configureHttpClient( | ||
'service_discovery', | ||
{ /* http config */ } | ||
) | ||
); | ||
``` | ||
### Simple Configuration | ||
In the simplest form, the service discovery module can be enabled with the following code: | ||
```typescript | ||
import enableServiceDiscovery from '@equinor/fusion-framework-service-discovery'; | ||
// the module will setup the service discovery client with default configuration | ||
// Assumes that http client is configured with key 'service_discovery' | ||
enableServiceDiscovery(configurator); | ||
``` | ||
#### Override default http client key | ||
If the http client is configured with a different key, the key can be specified as follows: | ||
```typescript | ||
enableServiceDiscovery( | ||
configurator, | ||
async(builder: ServiceDiscoveryConfigurator) => { | ||
builder.configureServiceDiscoveryClientByClientKey( | ||
// assume that http client is configured with this key | ||
'service_discovery_custom', | ||
// optional endpoint path | ||
'/custom/services' | ||
); | ||
}); | ||
``` | ||
/** key of configured http client, default `service_discovery` */ | ||
config.serviceDiscovery.clientKey = 'my_service_discovery' | ||
### Advanced Configuration | ||
/** endpoint for fetching services */ | ||
config.serviceDiscovery.uri = 'api/services'; | ||
#### Override default http client | ||
/** parse http response to service discovery environment */ | ||
config.serviceDiscovery.selector = async (response: Response): Promise<Environment> => { | ||
const services = await response.json() as Service[]; | ||
return services.reduce((acc, service) => Object.assign(acc, {[service.key]: service}), {}); | ||
} | ||
If a custom http client is required, the client can be configured as follows: | ||
```typescript | ||
enableServiceDiscovery( | ||
configurator, | ||
async(builder: ServiceDiscoveryConfigurator) => { | ||
builder.configureServiceDiscoveryClient( | ||
// configurator callback | ||
async (args: ConfigBuilderCallbackArgs) => { | ||
// using build environment to create a http client | ||
const httpProvider = await requireInstance('http'); | ||
const httpClient = httpProvider.createClient('some_key'); | ||
return { | ||
httpClient: httpProvider.createClient('some_key'), | ||
endpoint: '/custom/services' | ||
}; | ||
} | ||
); | ||
}); | ||
``` | ||
#### Setting a custom service discovery client | ||
If a custom service discovery client is required, the client can be configured as follows: | ||
```typescript | ||
enableServiceDiscovery( | ||
configurator, | ||
async(builder: ServiceDiscoveryConfigurator) => { | ||
builder.setServiceDiscoveryClient( | ||
{ | ||
resolveServices() { | ||
return [ | ||
{ | ||
key: 'service1', | ||
url: 'http://service1.com' | ||
}, | ||
{ | ||
key: 'service2', | ||
url: 'http://service2.com' | ||
} | ||
] | ||
}, | ||
resolveService(key: string): Promise<ServiceEndpoint> { | ||
return this.services.find(s => s.key === key); | ||
} | ||
} | ||
); | ||
}); | ||
``` | ||
If custom logic for creating the service discovery client is required, the client can be configured as follows: | ||
```typescript | ||
enableServiceDiscovery( | ||
configurator, | ||
async(builder: ServiceDiscoveryConfigurator) => { | ||
builder.setServiceDiscoveryClient( | ||
async(args: ConfigBuilderCallbackArgs) => { | ||
const httpProvider = await requireInstance('http'); | ||
const httpClient = httpProvider.createClient('my_key'); | ||
return { | ||
resolveServices() { | ||
return httpClient.get('/services'); | ||
}, | ||
resolveService(key: string): Promise<ServiceEndpoint> { | ||
return httpClient.get(`/services/${key}`); | ||
} | ||
}; | ||
} | ||
); | ||
}); | ||
``` |
import { IHttpClient } from '@equinor/fusion-framework-module-http'; | ||
import { Query } from '@equinor/fusion-query'; | ||
import { Environment, EnvironmentResponse, Service } from './types'; | ||
import type { Service } from './types'; | ||
import { firstValueFrom } from 'rxjs'; | ||
import { firstValueFrom, from, lastValueFrom, map, ObservableInput } from 'rxjs'; | ||
import { jsonSelector } from '@equinor/fusion-framework-module-http/selectors'; | ||
import { ApiServices } from './api-schema'; | ||
export interface IServiceDiscoveryClient { | ||
readonly environment: Environment; | ||
resolveService(key: string): Promise<Service>; | ||
fetchEnvironment(): Promise<Environment>; | ||
/** | ||
* Resolves a service by key | ||
* @param key - The key of the service to resolve | ||
*/ | ||
resolveService(key: string, allow_cache?: boolean): Promise<Service>; | ||
/** | ||
* Fetches services from the service discovery API. | ||
*/ | ||
resolveServices(allow_cache?: boolean): Promise<Service[]>; | ||
} | ||
export interface IServiceDiscoveryClientCtor<TEnv extends Environment = Environment> { | ||
new (args: ServiceDiscoveryClientCtorArgs): ServiceDiscoveryClient<TEnv>; | ||
export interface IServiceDiscoveryClientCtor { | ||
new (args: ServiceDiscoveryClientCtorArgs): ServiceDiscoveryClient; | ||
} | ||
@@ -20,3 +29,3 @@ | ||
http: IHttpClient; | ||
endpoint: string; | ||
endpoint?: string; | ||
}; | ||
@@ -26,64 +35,52 @@ | ||
export class ServiceDiscoveryClient<T extends Environment = Environment> | ||
implements IServiceDiscoveryClient | ||
{ | ||
// TODO - make better | ||
#query: Query<T, void>; | ||
/** | ||
* Transforms a Response object into an ObservableInput of Service arrays. | ||
* | ||
* @param response - The Response object to be transformed. | ||
* @returns An ObservableInput that emits an array of Service objects. | ||
*/ | ||
const serviceResponseSelector = (response: Response): ObservableInput<Service[]> => | ||
// parse response by using the jsonSelector | ||
from(jsonSelector(response)).pipe( | ||
// parse and validate the response | ||
map((value) => ApiServices.default([]).parse(value)), | ||
); | ||
public endpoint: string; | ||
public http: IHttpClient; | ||
export class ServiceDiscoveryClient implements IServiceDiscoveryClient { | ||
#query: Query<Service[], void>; | ||
get environment(): T { | ||
const env = this.#query.cache.getItem(queryKey)?.value; | ||
if (!env) { | ||
throw Error('no cached environment found'); | ||
} | ||
return env; | ||
} | ||
/** Endpoint for fetching services from API */ | ||
public readonly endpoint?: string; | ||
/** HTTP client for fetching services */ | ||
public readonly http: IHttpClient; | ||
constructor({ http, endpoint }: ServiceDiscoveryClientCtorArgs) { | ||
this.http = http; | ||
this.endpoint = endpoint; | ||
this.#query = new Query<T, void>({ | ||
// setup api handler (queue and cache) | ||
this.#query = new Query<Service[], void>({ | ||
client: { | ||
fn: () => http.fetch$(endpoint, { selector: this.selector.bind(this) }), | ||
fn: () => http.fetch$(endpoint ?? '', { selector: serviceResponseSelector }), | ||
}, | ||
key: () => queryKey, | ||
// Cache for 5 minutes | ||
expire: 5 * 60 * 1000, | ||
// queueOperator: (_) => ($) => $.pipe(throttleTime(100)), | ||
}); | ||
} | ||
public async fetchEnvironment(): Promise<T> { | ||
return firstValueFrom( | ||
Query.extractQueryValue( | ||
this.#query.query(undefined, { cache: { suppressInvalid: true } }), | ||
), | ||
); | ||
public resolveServices(allow_cache?: boolean): Promise<Service[]> { | ||
const fn = allow_cache ? firstValueFrom : lastValueFrom; | ||
return fn(Query.extractQueryValue(this.#query.query())); | ||
} | ||
public async selector(response: Response): Promise<T> { | ||
const env = (await response.json()) as EnvironmentResponse; | ||
const services = env.services.reduce((acc, service) => { | ||
return Object.assign(acc, { | ||
[service.key]: { | ||
clientId: env.clientId, | ||
uri: service.uri, | ||
defaultScopes: service.defaultScopes ?? [env.clientId + '/.default'], | ||
}, | ||
}); | ||
}, {} as T); | ||
return { ...env, services } as unknown as T; | ||
} | ||
public async resolveService(key: string): Promise<Service> { | ||
try { | ||
const { services } = await this.fetchEnvironment(); | ||
const service = services[key]; | ||
return service; | ||
} catch (err) { | ||
console.error(err); | ||
throw err; | ||
public async resolveService(key: string, allow_cache?: boolean): Promise<Service> { | ||
const services = await this.resolveServices(allow_cache); | ||
const service = services.find((s) => s.key === key); | ||
if (!service) { | ||
throw Error(`Failed to resolve service, invalid key [${key}]`); | ||
} | ||
return service; | ||
} | ||
} |
@@ -1,54 +0,118 @@ | ||
import { ModulesInstanceType } from '@equinor/fusion-framework-module'; | ||
import { HttpModule, IHttpClient } from '@equinor/fusion-framework-module-http'; | ||
import { from, lastValueFrom, ObservableInput } from 'rxjs'; | ||
import type { IServiceDiscoveryClient, IServiceDiscoveryClientCtor } from './client'; | ||
import { | ||
BaseConfigBuilder, | ||
type ConfigBuilderCallback, | ||
type ConfigBuilderCallbackArgs, | ||
} from '@equinor/fusion-framework-module'; | ||
export interface IServiceDiscoveryConfigurator { | ||
/** name of HttpClient */ | ||
clientKey?: string; | ||
import { type IHttpClient } from '@equinor/fusion-framework-module-http'; | ||
endpoint?: string; | ||
import { type IServiceDiscoveryClient, ServiceDiscoveryClient } from './client'; | ||
clientCtor: IServiceDiscoveryClientCtor; | ||
export interface ServiceDiscoveryConfig { | ||
/** Service Discovery client */ | ||
discoveryClient: IServiceDiscoveryClient; | ||
} | ||
createHttpClientClient: ( | ||
http: ModulesInstanceType<[HttpModule]>['http'], | ||
) => Promise<IHttpClient>; | ||
export class ServiceDiscoveryConfigurator extends BaseConfigBuilder<ServiceDiscoveryConfig> { | ||
protected async _createConfig( | ||
init: ConfigBuilderCallbackArgs, | ||
initial?: Partial<ServiceDiscoveryConfig> | undefined, | ||
): Promise<ServiceDiscoveryConfig> { | ||
if (!init.hasModule('http')) { | ||
throw new Error('http module is required'); | ||
} | ||
createClient: (http: IHttpClient) => Promise<IServiceDiscoveryClient>; | ||
} | ||
// check if http module has a client with key 'service_discovery' | ||
const httpProvider = await init.requireInstance('http'); | ||
if (httpProvider.hasClient('service_discovery')) { | ||
this.configureServiceDiscoveryClientByClientKey('service_discovery'); | ||
} | ||
export class ServiceDiscoveryConfigurator implements IServiceDiscoveryConfigurator { | ||
public clientKey?: string; | ||
public endpoint?: string; | ||
clientCtor: IServiceDiscoveryClientCtor; | ||
constructor(args: { | ||
clientCtor: IServiceDiscoveryClientCtor; | ||
clientKey?: string; | ||
endpoint?: string; | ||
}) { | ||
this.clientKey = args.clientKey; | ||
this.endpoint = args.endpoint; | ||
this.clientCtor = args.clientCtor; | ||
// convert parent to promise | ||
return lastValueFrom(from(super._createConfig(init, initial))); | ||
} | ||
async createHttpClientClient( | ||
http: ModulesInstanceType<[HttpModule]>['http'], | ||
): Promise<IHttpClient> { | ||
if (!this.clientKey) { | ||
throw Error('no http client for service discovery is provided!'); | ||
/** | ||
* Processes the service discovery configuration. | ||
* | ||
* @param config - A partial configuration object for service discovery. | ||
* @param init - Initialization arguments for the configuration builder callback. | ||
* @returns An observable input of the complete service discovery configuration. | ||
* @throws Will throw an error if `discoveryClient` is not configured. | ||
*/ | ||
protected _processConfig( | ||
config: Partial<ServiceDiscoveryConfig>, | ||
init: ConfigBuilderCallbackArgs, | ||
): ObservableInput<ServiceDiscoveryConfig> { | ||
if (!config.discoveryClient) { | ||
throw new Error('discoveryClient is required, please configure it'); | ||
} | ||
return http.createClient(this.clientKey); | ||
return super._processConfig(config, init); | ||
} | ||
async createClient(http: IHttpClient): Promise<IServiceDiscoveryClient> { | ||
if (!this.endpoint) { | ||
throw Error('no endpoint defined!'); | ||
} | ||
return new this.clientCtor({ | ||
http, | ||
endpoint: this.endpoint, | ||
/** | ||
* Sets the service discovery client. | ||
* | ||
* @param discoveryClient - An instance of `IServiceDiscoveryClient` or a callback function that returns an instance of `IServiceDiscoveryClient`. | ||
* | ||
* This method configures the service discovery client by either directly setting the provided instance or by invoking the provided callback function to obtain the instance. | ||
*/ | ||
setServiceDiscoveryClient( | ||
discoveryClient: IServiceDiscoveryClient | ConfigBuilderCallback<IServiceDiscoveryClient>, | ||
): void { | ||
this._set( | ||
'discoveryClient', | ||
typeof discoveryClient === 'function' ? discoveryClient : async () => discoveryClient, | ||
); | ||
} | ||
/** | ||
* Configures the Service Discovery Client with the provided configuration callback. | ||
* | ||
* @param configCallback - A callback function that takes an argument of type `{ httpClient: IHttpClient; endpoint?: string }` | ||
* and returns a configuration object. The `httpClient` is required, while the `endpoint` is optional. | ||
* | ||
* @throws {Error} Throws an error if `httpClient` is not provided in the configuration. | ||
*/ | ||
configureServiceDiscoveryClient( | ||
configCallback: ConfigBuilderCallback<{ httpClient: IHttpClient; endpoint?: string }>, | ||
): void { | ||
this.setServiceDiscoveryClient(async (args) => { | ||
const { httpClient, endpoint } = | ||
(await lastValueFrom(from(configCallback(args)))) ?? {}; | ||
if (httpClient) { | ||
return new ServiceDiscoveryClient({ | ||
http: httpClient, | ||
endpoint, | ||
}); | ||
} | ||
throw Error('httpClient is required'); | ||
}); | ||
} | ||
/** | ||
* Configures a service discovery client using the provided client key and optional endpoint. | ||
* | ||
* @param clientKey - The key used to identify the client. | ||
* @param endpoint - An optional endpoint to be used by the service discovery client. | ||
* | ||
* @remarks | ||
* The http module must have a configured client which match provided `clientKey`. | ||
* | ||
* This method sets up the service discovery client by requiring an HTTP provider instance, | ||
* creating an HTTP client with the given client key, and returning an object containing | ||
* the HTTP client and the optional endpoint. | ||
*/ | ||
configureServiceDiscoveryClientByClientKey(clientKey: string, endpoint?: string): void { | ||
this.configureServiceDiscoveryClient(async ({ requireInstance }) => { | ||
const httpProvider = await requireInstance('http'); | ||
const httpClient = httpProvider.createClient(clientKey); | ||
return { | ||
httpClient, | ||
endpoint, | ||
}; | ||
}); | ||
} | ||
} |
@@ -7,4 +7,9 @@ /** | ||
export * from './types'; | ||
export { IServiceDiscoveryConfigurator, ServiceDiscoveryConfigurator } from './configurator'; | ||
export { ServiceDiscoveryConfigurator } from './configurator'; | ||
export { IServiceDiscoveryProvider, ServiceDiscoveryProvider } from './provider'; | ||
export { default, ServiceDiscoveryModule, setupServiceDiscoveryModule } from './module'; | ||
export { | ||
default, | ||
ServiceDiscoveryModule, | ||
configureServiceDiscovery, | ||
enableServiceDiscovery, | ||
} from './module'; |
@@ -1,13 +0,17 @@ | ||
import { IServiceDiscoveryConfigurator, ServiceDiscoveryConfigurator } from './configurator'; | ||
import { ServiceDiscoveryConfigurator } from './configurator'; | ||
import { IServiceDiscoveryProvider, ServiceDiscoveryProvider } from './provider'; | ||
import type { Module, ModulesConfigType } from '@equinor/fusion-framework-module'; | ||
import type { | ||
IModuleConfigurator, | ||
Module, | ||
ModulesConfigurator, | ||
} from '@equinor/fusion-framework-module'; | ||
import type { HttpModule } from '@equinor/fusion-framework-module-http'; | ||
import { ServiceDiscoveryClient } from './client'; | ||
import { moduleName } from './constants'; | ||
export const moduleName = 'serviceDiscovery'; | ||
export type ServiceDiscoveryModule = Module< | ||
typeof moduleName, | ||
IServiceDiscoveryProvider, | ||
IServiceDiscoveryConfigurator, | ||
ServiceDiscoveryConfigurator, | ||
[HttpModule] | ||
@@ -17,26 +21,95 @@ >; | ||
/** | ||
* Configure http-client | ||
* Represents the Service Discovery module configuration. | ||
* | ||
* @remarks | ||
* This module is responsible for configuring and initializing the service discovery mechanism. | ||
* It ensures that the service discovery configuration is created and the necessary dependencies | ||
* are initialized before providing the service discovery provider. | ||
* | ||
* @returns {Promise<ServiceDiscoveryProvider>} - Returns a promise that resolves to the service discovery provider. | ||
* | ||
* @remarks | ||
* The initialization process involves creating the service discovery configuration, which may include | ||
* inheriting configurations from a parent module. This pattern, while allowing reuse of cache and ensuring | ||
* up-to-date configurations, can be risky as it exposes the child module to potential breaking changes | ||
* from the parent module's client. | ||
* | ||
* Additionally, the service discovery module requires the HTTP module to be initialized before it can | ||
* function properly. | ||
*/ | ||
export const module: ServiceDiscoveryModule = { | ||
name: moduleName, | ||
// deps: ['http'], | ||
configure: () => | ||
new ServiceDiscoveryConfigurator({ | ||
clientCtor: ServiceDiscoveryClient, | ||
clientKey: 'service_discovery', | ||
endpoint: '/_discovery/environments/current', | ||
}), | ||
initialize: async ({ config, requireInstance }) => { | ||
configure: () => new ServiceDiscoveryConfigurator(), | ||
initialize: async (init) => { | ||
const { requireInstance, ref } = init; | ||
// create service discovery configuration | ||
const config = await init.config.createConfigAsync(init, { | ||
/** | ||
* @remarks | ||
* This is a dangerous pattern, as it allows the child module to access the parent module's client. | ||
* The client client could implement breaking changes that would affect the child module. | ||
* On the positive side, it allows the child module to reuse cache and always be up to date. | ||
*/ | ||
...ref?.serviceDiscovery?.config, | ||
}); | ||
// service discovery requires http module to be initialized | ||
const httpModule = await requireInstance('http'); | ||
const httpClient = await config.createHttpClientClient(httpModule); | ||
const discoClient = await config.createClient(httpClient); | ||
return new ServiceDiscoveryProvider(discoClient, httpModule); | ||
// return service discovery provider | ||
return new ServiceDiscoveryProvider(config, httpModule); | ||
}, | ||
}; | ||
export const setupServiceDiscoveryModule = ( | ||
config: ModulesConfigType<[ServiceDiscoveryModule]>, | ||
callback: (config: IServiceDiscoveryConfigurator) => void, | ||
): void | Promise<void> => { | ||
callback(config.serviceDiscovery); | ||
/** | ||
* Configures the Service Discovery module. | ||
* | ||
* @template TRef - The type reference for the module configurator. | ||
* @param callback - A function that takes a `ServiceDiscoveryConfigurator` and returns a promise that resolves when the configuration is complete. | ||
* @returns An object implementing `IModuleConfigurator` for the `ServiceDiscoveryModule` with the provided configuration. | ||
* | ||
* @example | ||
* ```typescript | ||
* import { configureServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery'; | ||
* | ||
* const config = (configurator: ModuleConfigurator) => { | ||
* configurator.addConfig(configureServiceDiscovery(async (config) => { | ||
* // custom configuration | ||
* }); | ||
* ``` | ||
*/ | ||
export const configureServiceDiscovery = <TRef>( | ||
callback: (config: ServiceDiscoveryConfigurator) => Promise<void>, | ||
): IModuleConfigurator<ServiceDiscoveryModule, TRef> => ({ | ||
module, | ||
configure: (config: ServiceDiscoveryConfigurator) => callback(config), | ||
}); | ||
/** | ||
* Enables the service discovery module by adding its configuration to the provided configurator. | ||
* | ||
* @param configurator - The configurator to which the service discovery configuration will be added. | ||
* @param callback - An optional callback function that can be used to customize the service discovery configuration. | ||
* | ||
* @example | ||
* ```typescript | ||
* import { enableServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery'; | ||
* | ||
* const config = (configurator: ModuleConfigurator) => { | ||
* // simple | ||
* enableServiceDiscovery(configurator); | ||
* | ||
* // with custom configuration | ||
* enableServiceDiscovery(configurator, async (config) => { | ||
* config.configureServiceDiscoveryClientByClientKey('service-discovery-custom'); | ||
* }); | ||
* }; | ||
* ``` | ||
*/ | ||
export const enableServiceDiscovery = ( | ||
configurator: ModulesConfigurator<[HttpModule]>, | ||
callback?: (config: ServiceDiscoveryConfigurator) => Promise<void>, | ||
): void => { | ||
configurator.addConfig(configureServiceDiscovery(callback ?? (() => Promise.resolve()))); | ||
}; | ||
@@ -43,0 +116,0 @@ |
import { configureHttpClient } from '@equinor/fusion-framework-module-http'; | ||
import { IServiceDiscoveryClient } from './client'; | ||
import type { ModulesConfigurator, ModuleType } from '@equinor/fusion-framework-module'; | ||
@@ -12,17 +10,27 @@ import type { | ||
import type { Environment, Service } from './types'; | ||
import type { Service } from './types'; | ||
import { ServiceDiscoveryConfig } from './configurator'; | ||
export interface IServiceDiscoveryProvider { | ||
/** | ||
* Try to resolve services for requested key | ||
* Resolves a service by key | ||
* @param key - The key of the service to resolve | ||
*/ | ||
resolveService(key: string): Promise<Service>; | ||
/** | ||
* service environment | ||
* this might throw error if no environment is loaded | ||
* Fetch all services | ||
*/ | ||
readonly environment: Environment; | ||
resolveServices(): Promise<Service[]>; | ||
resolveServices(): Promise<Environment>; | ||
/** | ||
* create http client for a service | ||
* @param name key of the service | ||
* @param opt http client options | ||
* | ||
* @example | ||
* ```typescript | ||
* const myClient = await serviceDiscovery.createClient('my-service', {}); | ||
* ``` | ||
*/ | ||
createClient( | ||
@@ -33,2 +41,26 @@ name: string, | ||
/** | ||
* Used in the framework to configure a http client which is resolved by service discovery | ||
* | ||
* @deprecating this method will be reworked in later versions, please don`t use! | ||
* | ||
* @param serviceName key of the service or an object with key and alias | ||
* @param config http client configurator | ||
* | ||
* @example | ||
* ```typescript | ||
* configure = ( | ||
* configurator: ModuleConfigBuilder<[HttpModule]>, | ||
* ref: ModuleInstanceType<ServiceDiscovery> | ||
* )=> { | ||
* ref.serviceDiscovery.configureClient( | ||
* // use `my-service` from service discovery | ||
* // and register it as `foo` in the http module | ||
* { key: 'my-service', alias: 'foo'}, | ||
* configurator | ||
* ); | ||
* }); | ||
* | ||
* ``` | ||
*/ | ||
configureClient( | ||
@@ -38,2 +70,4 @@ serviceName: string | { key: string; alias: string }, | ||
): Promise<void>; | ||
readonly config: ServiceDiscoveryConfig; | ||
} | ||
@@ -43,16 +77,12 @@ | ||
constructor( | ||
protected readonly _client: IServiceDiscoveryClient, | ||
public readonly config: ServiceDiscoveryConfig, | ||
protected readonly _http: ModuleType<HttpModule>, | ||
) {} | ||
public get environment(): Environment { | ||
return this._client.environment; | ||
public resolveServices(): Promise<Service[]> { | ||
return this.config.discoveryClient.resolveServices(); | ||
} | ||
public resolveServices(): Promise<Environment> { | ||
return this._client.fetchEnvironment(); | ||
} | ||
public async resolveService(key: string): Promise<Service> { | ||
return this._client.resolveService(key); | ||
return this.config.discoveryClient.resolveService(key); | ||
} | ||
@@ -71,3 +101,3 @@ | ||
baseUri: service.uri, | ||
defaultScopes: service.defaultScopes, | ||
defaultScopes: service.scopes, | ||
}); | ||
@@ -84,5 +114,5 @@ } | ||
: serviceName; | ||
const { uri: baseUri, defaultScopes } = await this.resolveService(key); | ||
const { uri: baseUri, scopes: defaultScopes } = await this.resolveService(key); | ||
config.addConfig(configureHttpClient(alias, { baseUri, defaultScopes })); | ||
} | ||
} |
@@ -1,21 +0,13 @@ | ||
export type EnvironmentResponse = { | ||
clientId: string; | ||
services: Array<{ | ||
key: string; | ||
uri: string; | ||
defaultScopes?: Array<string>; | ||
}>; | ||
}; | ||
export type Environment = { | ||
type?: string; | ||
clientId: string; | ||
services: Record<string, Service>; | ||
}; | ||
export type Service = { | ||
name?: null | string; | ||
clientId?: string; | ||
key: string; | ||
uri: string; | ||
scopes?: string[]; | ||
id?: string; | ||
name?: string; | ||
tags?: string[]; | ||
/** | ||
* @deprecated use scopes instead | ||
*/ | ||
defaultScopes: string[]; | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
232026
1129
133
5
2
1