743 lines · 23.7 KB · TypeScript
RawDownload
import { Context, Data, Effect, Layer, Predicate, Ref, Schema, Scope } from "effect";import {  HttpClient,  HttpRouter,  HttpServerRequest,  HttpServerResponse,} from "effect/unstable/http";import {  HttpApi,  HttpApiBuilder,  HttpApiEndpoint,  HttpApiGroup,  OpenApi,} from "effect/unstable/httpapi";import { OAuthTestServer, serveTestHttpServerLayer } from "@executor-js/sdk/testing";import {  AuthTemplateSlug,  ConnectionName,  IntegrationSlug,  ProviderItemId,  ProviderKey,  ToolAddress,  isToolResult,  type Connection,  type CreateConnectionInput,  type Owner,  type ToolAddress as ToolAddressType,} from "@executor-js/sdk/core";import type { OpenApiPluginExtension, OpenApiSpecConfig } from "../sdk/plugin"; export class OpenApiTestServerAddressError extends Data.TaggedError(  "OpenApiTestServerAddressError",)<{  readonly address: unknown;}> {} export class OpenApiTestServerSpecError extends Data.TaggedError("OpenApiTestServerSpecError")<{  readonly cause: unknown;}> {} export interface OpenApiTestServerShape {  readonly baseUrl: string;  readonly specJson: string;  readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>;} export interface OpenApiHttpApiTestServerOptions {  readonly api: HttpApi.Any;  readonly handlersLayer: Layer.Layer<any, any, any>;  readonly specPath?: `/${string}`;  readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;  readonly captureSpecRequest?: (    request: HttpServerRequest.HttpServerRequest,  ) => Effect.Effect<void>;  readonly guardSpecRequest?: (    request: HttpServerRequest.HttpServerRequest,  ) => Effect.Effect<HttpServerResponse.HttpServerResponse | null>;} export interface OpenApiHttpApiTestServerShape extends OpenApiTestServerShape {  readonly specUrl: string;} export interface MutableOpenApiSpecTestServerShape extends OpenApiTestServerShape {  readonly specUrl: string;  readonly setApi: (api: HttpApi.Any) => Effect.Effect<void, OpenApiTestServerSpecError>;  readonly requestCount: Effect.Effect<number>;} export interface OpenApiTestRequest {  readonly method: string;  readonly url: string;  readonly path: string;  readonly headers: Readonly<Record<string, string>>;  readonly body: string;} export interface OpenApiEchoTestServerOptions {  readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;  readonly oauth2?: {    readonly authorizationUrl: string;    readonly tokenUrl: string;    readonly scopes?: Readonly<Record<string, string>>;    readonly validateAuthorization?: (authorization: string | null) => Effect.Effect<boolean>;    readonly wwwAuthenticate?: string;  };} export interface OpenApiEchoTestServerShape extends OpenApiTestServerShape {  readonly specUrl: string;  readonly requests: Effect.Effect<readonly OpenApiTestRequest[]>;  readonly clearRequests: Effect.Effect<void>;} export type OpenApiTestSourceOptions = Omit<OpenApiSpecConfig, "spec" | "baseUrl" | "slug"> & {  readonly baseUrl?: string | null;  readonly slug?: string;}; export type OpenApiHttpApiTestSourceOptions = Omit<  OpenApiSpecConfig,  "spec" | "slug" | "baseUrl"> & {  readonly slug?: string;  readonly baseUrl?: string | null;  readonly specBaseUrl?: string;  readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;}; type OpenApiHttpApiAddSpecHeadersInput = {  readonly headers?: Record<string, string>;  readonly queryParams?: Record<string, string>;}; export type OpenApiHttpApiTestAddSpecPayloadOptions = Omit<  OpenApiHttpApiTestSourceOptions,  "headers" | "queryParams"> &  OpenApiHttpApiAddSpecHeadersInput; export type OpenApiTestSourceExecutor = {  readonly openapi: Pick<OpenApiPluginExtension, "addSpec">;}; // ---------------------------------------------------------------------------// v2 connection helper. In v2 a connection IS the credential and tools are// produced per-connection — so a test that invokes an openapi tool must// (1) addSpec to register the integration and (2) create a connection so the// tools are stamped + persisted. This helper does both and returns the// invokable address builder.// --------------------------------------------------------------------------- export type OpenApiTestConnectionExecutor = {  readonly openapi: Pick<OpenApiPluginExtension, "addSpec">;  readonly connections: {    readonly create: (input: CreateConnectionInput) => Effect.Effect<Connection, unknown>;  };}; export interface OpenApiTestConnectionOptions {  readonly slug?: string;  readonly owner?: Owner;  readonly connection?: string;  readonly template?: string;  /** Inline credential value. Defaults to a throwaway token via the in-memory   *  provider's `from` path is avoided — pass `value` to store a real secret. */  readonly value?: string;  readonly baseUrl?: string | null;} export interface OpenApiTestConnection {  readonly slug: string;  readonly owner: Owner;  readonly connection: string;  readonly address: (tool: string) => ToolAddressType;} /** addSpec + connections.create from a raw server spec; returns an address *  builder for `executor.execute`. */export const addOpenApiTestConnection = (  executor: OpenApiTestConnectionExecutor,  server: OpenApiTestServerShape,  sourceOptions: OpenApiTestSourceOptions,  connectionOptions: OpenApiTestConnectionOptions = {},): Effect.Effect<OpenApiTestConnection, unknown> =>  Effect.gen(function* () {    const config = makeOpenApiTestSourceConfig(server, sourceOptions);    const result = yield* executor.openapi.addSpec(config);    const slug = String(result.slug);    const owner: Owner = connectionOptions.owner ?? "org";    const connection = connectionOptions.connection ?? "main";    const template = connectionOptions.template ?? "apiKey";    yield* executor.connections.create({      owner,      name: ConnectionName.make(connection),      integration: IntegrationSlug.make(slug),      template: AuthTemplateSlug.make(template),      ...(connectionOptions.value !== undefined        ? { value: connectionOptions.value }        : {            from: {              provider: ProviderKey.make("memory"),              id: ProviderItemId.make(`${slug}-token`),            },          }),    } satisfies CreateConnectionInput);    return {      slug,      owner,      connection,      address: (tool: string) => ToolAddress.make(`tools.${slug}.${owner}.${connection}.${tool}`),    };  }); export interface OpenApiTestSpecOptions {  readonly baseUrl?: string;  readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;} export const makeOpenApiTestSpecJson = (  api: HttpApi.Any,  options: OpenApiTestSpecOptions = {},): string => {  const annotations = OpenApi.annotations({    ...(options.baseUrl !== undefined ? { servers: [{ url: options.baseUrl }] } : {}),    transform: options.transformSpec,  });  const annotated = (api as HttpApi.AnyWithProps).annotateMerge(annotations);  return JSON.stringify(OpenApi.fromApi(annotated));}; export const makeOpenApiTestSourceConfig = (  server: OpenApiTestServerShape,  options: OpenApiTestSourceOptions,): OpenApiSpecConfig => {  const { baseUrl, slug, ...rest } = options;  return {    ...rest,    spec: { kind: "blob", value: server.specJson },    slug: slug ?? "test_api",    ...(baseUrl === null ? {} : { baseUrl: baseUrl ?? server.baseUrl }),  } satisfies OpenApiSpecConfig;}; export const addOpenApiTestSource = (  executor: OpenApiTestSourceExecutor,  server: OpenApiTestServerShape,  options: OpenApiTestSourceOptions,) => executor.openapi.addSpec(makeOpenApiTestSourceConfig(server, options)); export const makeOpenApiHttpApiTestSourceConfig = (  api: HttpApi.Any,  options: OpenApiHttpApiTestSourceOptions,): OpenApiSpecConfig => {  const { baseUrl, specBaseUrl, transformSpec, slug, ...config } = options;  return {    ...config,    spec: {      kind: "blob",      value: makeOpenApiTestSpecJson(api, { baseUrl: specBaseUrl, transformSpec }),    },    slug: slug ?? "test_api",    ...(baseUrl === null ? {} : { baseUrl: baseUrl ?? specBaseUrl ?? "https://api.example.test" }),  } satisfies OpenApiSpecConfig;}; export const addOpenApiHttpApiTestSource = (  executor: OpenApiTestSourceExecutor,  api: HttpApi.Any,  options: OpenApiHttpApiTestSourceOptions,) => executor.openapi.addSpec(makeOpenApiHttpApiTestSourceConfig(api, options)); export const makeOpenApiHttpApiTestAddSpecPayload = (  api: HttpApi.Any,  options: OpenApiHttpApiTestAddSpecPayloadOptions,) => {  const { headers, queryParams, ...sourceOptions } = options;  const config = makeOpenApiHttpApiTestSourceConfig(api, sourceOptions);  return {    spec: config.spec,    slug: config.slug,    baseUrl: config.baseUrl,    ...(headers !== undefined ? { headers } : {}),    ...(queryParams !== undefined ? { queryParams } : {}),    ...(config.authenticationTemplate !== undefined      ? { authenticationTemplate: config.authenticationTemplate }      : {}),  };}; export const makeOpenApiHttpApiTestSpecPayload = (  api: HttpApi.Any,  options: OpenApiTestSpecOptions = {},) => ({  spec: makeOpenApiTestSpecJson(api, options),}); const isJsonObject = (value: unknown): value is Readonly<Record<string, unknown>> =>  typeof value === "object" && value !== null && !Array.isArray(value); const OpenApiEchoItem = Schema.Struct({  id: Schema.Number,  name: Schema.String,}); const OpenApiEchoHeaders = Schema.Struct({  authorization: Schema.optional(Schema.String),  "x-static": Schema.optional(Schema.String),}); const OpenApiEchoMessage = Schema.Struct({  message: Schema.String,  suffix: Schema.optional(Schema.String),  path: Schema.String,}); const OpenApiEchoItemsGroup = HttpApiGroup.make("items")  .add(    HttpApiEndpoint.get("listItems", "/items", {      success: Schema.Array(OpenApiEchoItem),    }),  )  .add(    HttpApiEndpoint.get("echoHeaders", "/echo-headers", {      success: OpenApiEchoHeaders,    }),  ); const OpenApiEchoGroup = HttpApiGroup.make("echo").add(  HttpApiEndpoint.get("echoMessage", "/echo/:message", {    params: Schema.Struct({ message: Schema.String }),    query: Schema.Struct({ suffix: Schema.optional(Schema.String) }),    success: OpenApiEchoMessage,  }),); const OpenApiEchoApi = HttpApi.make("executorOpenApiTest")  .add(OpenApiEchoItemsGroup)  .add(OpenApiEchoGroup)  .annotateMerge(    OpenApi.annotations({      title: "Executor OpenAPI Test Server",      version: "1.0.0",    }),  ); const openApiSpecJsonFromHttpApi = (  api: HttpApi.Any,  baseUrl: string,  transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>,): Effect.Effect<string, OpenApiTestServerSpecError> =>  Effect.try({    try: () => makeOpenApiTestSpecJson(api, { baseUrl, transformSpec }),    catch: (cause) => new OpenApiTestServerSpecError({ cause }),  }); export const serveOpenApiHttpApiTestServer = (  options: OpenApiHttpApiTestServerOptions,): Effect.Effect<  OpenApiHttpApiTestServerShape,  OpenApiTestServerAddressError | OpenApiTestServerSpecError,  Scope.Scope> =>  Effect.gen(function* () {    const specPath = options.specPath ?? "/spec.json";    let specJson = "";    const SpecRoute = HttpRouter.addAll([      HttpRouter.route(        "GET",        specPath,        Effect.gen(function* () {          const request = yield* HttpServerRequest.HttpServerRequest;          if (options.captureSpecRequest) {            yield* options.captureSpecRequest(request);          }          const guardResponse = options.guardSpecRequest            ? yield* options.guardSpecRequest(request)            : null;          if (guardResponse) return guardResponse;          return HttpServerResponse.text(specJson, {            status: 200,            contentType: "application/json",          });        }),      ),    ]);    const ApiLive = HttpApiBuilder.layer(options.api as HttpApi.AnyWithProps).pipe(      Layer.provide(options.handlersLayer),    );    const ServerLayer = HttpRouter.serve(Layer.mergeAll(ApiLive, SpecRoute), {      disableListenLog: true,      disableLogger: true,    });    const server = yield* serveTestHttpServerLayer(ServerLayer).pipe(      Effect.mapError((error) =>        Predicate.isTagged(error, "TestHttpServerAddressError")          ? new OpenApiTestServerAddressError({ address: error.address })          : new OpenApiTestServerSpecError({ cause: error.cause }),      ),    );    specJson = yield* openApiSpecJsonFromHttpApi(      options.api,      server.baseUrl,      options.transformSpec,    );     return {      baseUrl: server.baseUrl,      specUrl: server.url(specPath),      specJson,      httpClientLayer: server.httpClientLayer,    };  }); export class OpenApiHttpApiTestServer extends Context.Service<  OpenApiHttpApiTestServer,  OpenApiHttpApiTestServerShape>()("@executor-js/plugin-openapi/testing/OpenApiHttpApiTestServer") {  static readonly layer = (options: OpenApiHttpApiTestServerOptions) =>    Layer.effect(OpenApiHttpApiTestServer, serveOpenApiHttpApiTestServer(options));} export const serveMutableOpenApiSpecTestServer = (options: {  readonly initialApi: HttpApi.Any;  readonly specPath?: `/${string}`;  readonly transformSpec?: (spec: Record<string, unknown>) => Record<string, unknown>;}): Effect.Effect<  MutableOpenApiSpecTestServerShape,  OpenApiTestServerAddressError | OpenApiTestServerSpecError,  Scope.Scope> =>  Effect.gen(function* () {    const specPath = options.specPath ?? "/spec.json";    const specJson = yield* Ref.make("");    const requests = yield* Ref.make(0);    const SpecRoute = HttpRouter.addAll([      HttpRouter.route(        "GET",        specPath,        Effect.gen(function* () {          yield* Ref.update(requests, (count) => count + 1);          const current = yield* Ref.get(specJson);          return HttpServerResponse.text(current, {            status: 200,            contentType: "application/json",          });        }),      ),    ]);    const server = yield* serveTestHttpServerLayer(      HttpRouter.serve(SpecRoute, {        disableListenLog: true,        disableLogger: true,      }),    ).pipe(      Effect.mapError((error) =>        Predicate.isTagged(error, "TestHttpServerAddressError")          ? new OpenApiTestServerAddressError({ address: error.address })          : new OpenApiTestServerSpecError({ cause: error.cause }),      ),    );    const renderSpec = (api: HttpApi.Any) =>      openApiSpecJsonFromHttpApi(api, server.baseUrl, options.transformSpec);    yield* Ref.set(specJson, yield* renderSpec(options.initialApi));     return {      baseUrl: server.baseUrl,      specUrl: server.url(specPath),      specJson: yield* Ref.get(specJson),      httpClientLayer: server.httpClientLayer,      setApi: (api) => renderSpec(api).pipe(Effect.flatMap((next) => Ref.set(specJson, next))),      requestCount: Ref.get(requests),    };  }); const openApiOperationMethods = new Set([  "get",  "put",  "post",  "delete",  "options",  "head",  "patch",]); const withOAuth2Security =  (oauth2: NonNullable<OpenApiEchoTestServerOptions["oauth2"]>) =>  (spec: Record<string, unknown>): Record<string, unknown> => {    const scopes = oauth2.scopes ?? { read: "Read test resources" };    const security = [{ oauth2: Object.keys(scopes) }];    const paths = isJsonObject(spec.paths)      ? Object.fromEntries(          Object.entries(spec.paths).map(([path, pathItem]) => [            path,            isJsonObject(pathItem)              ? Object.fromEntries(                  Object.entries(pathItem).map(([method, operation]) => [                    method,                    openApiOperationMethods.has(method) && isJsonObject(operation)                      ? { ...operation, security }                      : operation,                  ]),                )              : pathItem,          ]),        )      : spec.paths;    const components = isJsonObject(spec.components) ? spec.components : {};    const securitySchemes = isJsonObject(components.securitySchemes)      ? components.securitySchemes      : {};     return {      ...spec,      paths,      components: {        ...components,        securitySchemes: {          ...securitySchemes,          oauth2: {

Lines 1–500 of 743.