UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

231 lines (200 loc) 6.12 kB
import { $hook, $inject, $pipeline, $state, Alepha, AlephaError } from "alepha"; import { $logger } from "alepha/logger"; import { $retry } from "alepha/retry"; import type { ServiceAccountPrimitive } from "alepha/security"; import { serverApiOptions } from "alepha/server"; import { ServerProxyProvider } from "alepha/server/proxy"; import { $remote, type RemotePrimitive } from "../primitives/$remote.ts"; import { type ApiRegistryResponse, apiRegistryResponseSchema, } from "../schemas/apiLinksResponseSchema.ts"; import { LinkProvider } from "./LinkProvider.ts"; export class RemotePrimitiveProvider { protected readonly serverApi = $state(serverApiOptions); protected readonly alepha = $inject(Alepha); protected readonly proxyProvider = $inject(ServerProxyProvider); protected readonly linkProvider = $inject(LinkProvider); protected readonly remotes: Array<ServerRemote> = []; protected readonly log = $logger(); public getRemotes(): ServerRemote[] { return this.remotes; } public readonly configure = $hook({ on: "configure", handler: async () => { const remotes = this.alepha.primitives($remote); for (const remote of remotes) { await this.registerRemote(remote); } }, }); public readonly start = $hook({ on: "start", handler: async () => { for (const remote of this.remotes) { const token = typeof remote.serviceAccount?.token === "function" ? await remote.serviceAccount.token() : undefined; if (!remote.internal) { continue; // skip download links for remotes that are not internal } const registry = await remote.links({ authorization: token }); for (const [name, action] of Object.entries(registry.actions)) { let path = action.path.replace(remote.prefix, ""); if (action.service) { path = `/${action.service}${path}`; } this.linkProvider.registerLink({ name, path, method: action.method ?? undefined, contentType: action.contentType, prefix: remote.prefix, host: remote.url, service: remote.name, }); } this.log.info(`Remote '${remote.name}' OK`, { actions: Object.keys(registry.actions).length, prefix: remote.prefix, }); } }, }); public async registerRemote(value: RemotePrimitive): Promise<void> { const options = value.options; const url = typeof options.url === "string" ? options.url : options.url(); const linkPath = LinkProvider.path.apiLinks; const name = value.name; const proxy = typeof options.proxy === "object" ? options.proxy : {}; const remote: ServerRemote = { url, name, prefix: "/api", serviceAccount: options.serviceAccount, proxy: !!options.proxy, internal: !proxy.noInternal, schema: async (opts) => { const { authorization, name } = opts; return await fetch(`${url}${linkPath}/${name}/schema`, { headers: new Headers( authorization ? { authorization, } : {}, ), }).then((it) => it.json()); // TODO: use schema validation for response }, links: async (opts) => { const { authorization } = opts; const remoteApi = await this.fetchLinks.run({ service: name, url: `${url}${linkPath}`, authorization, }); if (remoteApi.prefix != null) { remote.prefix = remoteApi.prefix; // monkey patch the prefix, not ideal but works } return remoteApi; }, }; this.remotes.push(remote); if (options.proxy) { this.proxyProvider.createProxy({ path: `${this.serverApi.prefix}/${name}/*`, target: url, rewrite: (url) => { url.pathname = url.pathname.replace( `${this.serverApi.prefix}/${name}`, remote.prefix, ); }, ...proxy, }); } } protected readonly fetchLinks = $pipeline({ use: [ $retry({ max: 10, backoff: { initial: 1000, }, onError: (_: Error, attempt: number) => { this.log.warn(`Failed to fetch links, retry (${attempt})...`); }, }), ], handler: async (opts: FetchLinksOptions): Promise<ApiRegistryResponse> => { const { url, authorization } = opts; const response = await fetch(url, { headers: new Headers( authorization ? { authorization, } : {}, ), }); if (!response.ok) { throw new AlephaError(`Failed to fetch links from ${url}`); } return this.alepha.codec.decode( apiRegistryResponseSchema, await response.json(), ); }, }); } // --------------------------------------------------------------------------------------------------------------------- export interface FetchLinksOptions { /** * Name of the remote service. */ service: string; /** * URL to fetch links from. */ url: string; /** * Authorization header containing access token. */ authorization?: string; } export interface ServerRemote { /** * URL of the remote service. */ url: string; /** * Name of the remote service. */ name: string; /** * Expose links as endpoint. It's not only internal. */ proxy: boolean; /** * It's only used inside the application. */ internal: boolean; /** * Links fetcher. */ links: (args: { authorization?: string }) => Promise<ApiRegistryResponse>; /** * Fetches schema for the remote service. */ schema: (args: { name: string; authorization?: string }) => Promise<any>; /** * Force a default access token provider when not provided. */ serviceAccount?: ServiceAccountPrimitive; /** * Prefix for the remote service links. */ prefix: string; }