Showing the changes to plugins/openapi (1 of 78 files). Show all
Unified · Split
| @@ -9,9 +9,12 @@ import { · 8 unmodified lines | ||
| 9 | 9 | import type { Source } from "@executor/platform-sdk/schema"; |
| 10 | 10 | import type { |
| 11 | 11 | ExecutorSdkPlugin, |
| 12 | + ExecutorSdkPluginHost, | |
| 13 | + ExecutorSourceConnector, | |
| 12 | 14 | SourcePluginRuntime, |
| 13 | 15 | } from "@executor/platform-sdk/plugins"; |
| 14 | 16 | import { |
| 17 | + OpenApiConnectionAuthSchema, | |
| 15 | 18 | deriveOpenApiNamespace, |
| 16 | 19 | previewOpenApiDocument, |
| 17 | 20 | type OpenApiConnectInput, |
| @@ -90,6 +93,16 @@ export type OpenApiSdkPluginOptions = { · 72 unmodified lines | ||
| 90 | 93 | secrets?: OpenApiSecrets; |
| 91 | 94 | }; |
| 92 | 95 | |
| 96 | +const OpenApiExecutorAddInputSchema = Schema.Struct({ | |
| 97 | + kind: Schema.Literal("openapi"), | |
| 98 | + name: Schema.String, | |
| 99 | + specUrl: Schema.String, | |
| 100 | + baseUrl: Schema.NullOr(Schema.String), | |
| 101 | + auth: OpenApiConnectionAuthSchema, | |
| 102 | +}); | |
| 103 | + | |
| 104 | +type OpenApiExecutorAddInput = typeof OpenApiExecutorAddInputSchema.Type; | |
| 105 | + | |
| 93 | 106 | const createStoredSourceData = ( |
| 94 | 107 | input: OpenApiConnectInput, |
| 95 | 108 | ): OpenApiStoredSourceData => ({ |
| @@ -249,6 +262,117 @@ const responseHeadersRecord = (response: Response): Record<string, string> => { · 153 unmodified lines | ||
| 249 | 262 | return headers; |
| 250 | 263 | }; |
| 251 | 264 | |
| 265 | +const createOpenApiSourceSdk = ( | |
| 266 | + options: OpenApiSdkPluginOptions, | |
| 267 | + host: ExecutorSdkPluginHost, | |
| 268 | +) => ({ | |
| 269 | + getSourceConfig: (sourceId: Source["id"]) => | |
| 270 | + Effect.gen(function* () { | |
| 271 | + const source = yield* host.sources.get(sourceId); | |
| 272 | + if (source.kind !== "openapi") { | |
| 273 | + return yield* Effect.fail( | |
| 274 | + new Error(`Source ${sourceId} is not an OpenAPI source.`), | |
| 275 | + ); | |
| 276 | + } | |
| 277 | + | |
| 278 | + const stored = yield* options.storage.get({ | |
| 279 | + scopeId: source.scopeId, | |
| 280 | + sourceId: source.id, | |
| 281 | + }); | |
| 282 | + if (stored === null) { | |
| 283 | + return yield* Effect.fail( | |
| 284 | + new Error(`OpenAPI source storage missing for ${source.id}`), | |
| 285 | + ); | |
| 286 | + } | |
| 287 | + | |
| 288 | + return configFromStoredSourceData(source, stored); | |
| 289 | + }), | |
| 290 | + createSource: (input: OpenApiConnectInput) => | |
| 291 | + Effect.gen(function* () { | |
| 292 | + const stored = createStoredSourceData(input); | |
| 293 | + const createdSource = yield* host.sources.create({ | |
| 294 | + source: { | |
| 295 | + name: input.name.trim(), | |
| 296 | + kind: "openapi", | |
| 297 | + status: "connected", | |
| 298 | + enabled: true, | |
| 299 | + namespace: deriveOpenApiNamespace({ | |
| 300 | + specUrl: input.specUrl, | |
| 301 | + title: input.name, | |
| 302 | + }), | |
| 303 | + }, | |
| 304 | + }); | |
| 305 | + | |
| 306 | + yield* options.storage.put({ | |
| 307 | + scopeId: createdSource.scopeId, | |
| 308 | + sourceId: createdSource.id, | |
| 309 | + value: stored, | |
| 310 | + }); | |
| 311 | + | |
| 312 | + return yield* host.sources.refreshCatalog(createdSource.id); | |
| 313 | + }), | |
| 314 | + updateSource: (input: OpenApiUpdateSourceInput) => | |
| 315 | + Effect.gen(function* () { | |
| 316 | + const source = yield* host.sources.get(input.sourceId as Source["id"]); | |
| 317 | + if (source.kind !== "openapi") { | |
| 318 | + return yield* Effect.fail( | |
| 319 | + new Error(`Source ${input.sourceId} is not an OpenAPI source.`), | |
| 320 | + ); | |
| 321 | + } | |
| 322 | + | |
| 323 | + const nextStored = createStoredSourceData(input.config); | |
| 324 | + const savedSource = yield* host.sources.save({ | |
| 325 | + ...source, | |
| 326 | + name: input.config.name.trim(), | |
| 327 | + namespace: deriveOpenApiNamespace({ | |
| 328 | + specUrl: input.config.specUrl, | |
| 329 | + title: input.config.name, | |
| 330 | + }), | |
| 331 | + }); | |
| 332 | + | |
| 333 | + yield* options.storage.put({ | |
| 334 | + scopeId: savedSource.scopeId, | |
| 335 | + sourceId: savedSource.id, | |
| 336 | + value: nextStored, | |
| 337 | + }); | |
| 338 | + | |
| 339 | + return yield* host.sources.refreshCatalog(savedSource.id); | |
| 340 | + }), | |
| 341 | + removeSource: (sourceId: Source["id"]) => | |
| 342 | + Effect.gen(function* () { | |
| 343 | + const source = yield* host.sources.get(sourceId); | |
| 344 | + if (source.kind !== "openapi") { | |
| 345 | + return yield* Effect.fail( | |
| 346 | + new Error(`Source ${sourceId} is not an OpenAPI source.`), | |
| 347 | + ); | |
| 348 | + } | |
| 349 | + | |
| 350 | + if (options.storage.remove) { | |
| 351 | + yield* options.storage.remove({ | |
| 352 | + scopeId: source.scopeId, | |
| 353 | + sourceId: source.id, | |
| 354 | + }); | |
| 355 | + } | |
| 356 | + | |
| 357 | + return yield* host.sources.remove(source.id); | |
| 358 | + }), | |
| 359 | +}); | |
| 360 | + | |
| 361 | +const openApiSourceConnector = ( | |
| 362 | + options: OpenApiSdkPluginOptions, | |
| 363 | +): ExecutorSourceConnector<OpenApiExecutorAddInput> => ({ | |
| 364 | + kind: "openapi", | |
| 365 | + displayName: "OpenAPI", | |
| 366 | + inputSchema: OpenApiExecutorAddInputSchema, | |
| 367 | + inputSignatureWidth: 280, | |
| 368 | + helpText: [ | |
| 369 | + "Provide the OpenAPI document URL and optional base URL override.", | |
| 370 | + "Use `auth.kind = \"bearer\"` with a stored secret ref when required.", | |
| 371 | + ], | |
| 372 | + createSource: ({ args, host }) => | |
| 373 | + createOpenApiSourceSdk(options, host).createSource(args), | |
| 374 | +}); | |
| 375 | + | |
| 252 | 376 | const decodeResponseBody = async (response: Response): Promise<unknown> => { |
| 253 | 377 | if (response.status === 204) { |
| 254 | 378 | return null; |
| @@ -541,102 +665,29 @@ export const openApiSdkPlugin = ( · 286 unmodified lines | ||
| 541 | 665 | ): ExecutorSdkPlugin<"openapi", OpenApiSdk> => ({ |
| 542 | 666 | key: "openapi", |
| 543 | 667 | sources: [createOpenApiSourceRuntime(options)], |
| 544 | − extendExecutor: ({ host }) => ({ | |
| 668 | + sourceConnectors: [openApiSourceConnector(options)], | |
| 669 | + extendExecutor: ({ host, executor }) => { | |
| 670 | + const sourceSdk = createOpenApiSourceSdk(options, host); | |
| 671 | + const provideRuntime = <A>( | |
| 672 | + effect: Effect.Effect<A, Error, any>, | |
| 673 | + ): Effect.Effect<A, Error, never> => | |
| 674 | + effect.pipe(Effect.provide(executor.runtime.managedRuntime)); | |
| 675 | + | |
| 676 | + return { | |
| 545 | 677 | previewDocument: (input) => |
| 546 | 678 | Effect.tryPromise({ |
| 547 | 679 | try: () => previewOpenApiDocument(input), |
| 548 | 680 | catch: (cause) => |
| 549 | 681 | cause instanceof Error ? cause : new Error(String(cause)), |
| 550 | 682 | }), |
| 551 | − getSourceConfig: (sourceId) => | |
| 552 | − Effect.gen(function* () { | |
| 553 | − const source = yield* host.sources.get(sourceId); | |
| 554 | − if (source.kind !== "openapi") { | |
| 555 | − return yield* Effect.fail( | |
| 556 | − new Error(`Source ${sourceId} is not an OpenAPI source.`), | |
| 557 | − ); | |
| 558 | − } | |
| 559 | − | |
| 560 | − const stored = yield* options.storage.get({ | |
| 561 | − scopeId: source.scopeId, | |
| 562 | − sourceId: source.id, | |
| 563 | − }); | |
| 564 | − if (stored === null) { | |
| 565 | − return yield* Effect.fail( | |
| 566 | − new Error(`OpenAPI source storage missing for ${source.id}`), | |
| 567 | − ); | |
| 568 | − } | |
| 569 | − | |
| 570 | − return configFromStoredSourceData(source, stored); | |
| 571 | − }), | |
| 572 | − createSource: (input) => | |
| 573 | − Effect.gen(function* () { | |
| 574 | − const stored = createStoredSourceData(input); | |
| 575 | − const createdSource = yield* host.sources.create({ | |
| 576 | − source: { | |
| 577 | − name: input.name.trim(), | |
| 578 | − kind: "openapi", | |
| 579 | − status: "connected", | |
| 580 | − enabled: true, | |
| 581 | − namespace: deriveOpenApiNamespace({ | |
| 582 | − specUrl: input.specUrl, | |
| 583 | − title: input.name, | |
| 584 | − }), | |
| 585 | − }, | |
| 586 | − }); | |
| 587 | − | |
| 588 | − yield* options.storage.put({ | |
| 589 | − scopeId: createdSource.scopeId, | |
| 590 | − sourceId: createdSource.id, | |
| 591 | − value: stored, | |
| 592 | − }); | |
| 593 | − | |
| 594 | − return yield* host.sources.refreshCatalog(createdSource.id); | |
| 595 | − }), | |
| 596 | − updateSource: (input) => | |
| 597 | − Effect.gen(function* () { | |
| 598 | − const source = yield* host.sources.get(input.sourceId as Source["id"]); | |
| 599 | − if (source.kind !== "openapi") { | |
| 600 | − return yield* Effect.fail( | |
| 601 | − new Error(`Source ${input.sourceId} is not an OpenAPI source.`), | |
| 602 | − ); | |
| 603 | − } | |
| 604 | − | |
| 605 | − const nextStored = createStoredSourceData(input.config); | |
| 606 | − const savedSource = yield* host.sources.save({ | |
| 607 | − ...source, | |
| 608 | − name: input.config.name.trim(), | |
| 609 | − namespace: deriveOpenApiNamespace({ | |
| 610 | − specUrl: input.config.specUrl, | |
| 611 | − title: input.config.name, | |
| 612 | − }), | |
| 613 | − }); | |
| 614 | − | |
| 615 | − yield* options.storage.put({ | |
| 616 | − scopeId: savedSource.scopeId, | |
| 617 | − sourceId: savedSource.id, | |
| 618 | − value: nextStored, | |
| 619 | − }); | |
| 620 | − | |
| 621 | − return yield* host.sources.refreshCatalog(savedSource.id); | |
| 622 | − }), | |
| 623 | − removeSource: (sourceId) => | |
| 624 | − Effect.gen(function* () { | |
| 625 | − const source = yield* host.sources.get(sourceId); | |
| 626 | − if (source.kind !== "openapi") { | |
| 627 | − return yield* Effect.fail( | |
| 628 | − new Error(`Source ${sourceId} is not an OpenAPI source.`), | |
| 629 | − ); | |
| 630 | − } | |
| 631 | − | |
| 632 | − if (options.storage.remove) { | |
| 633 | − yield* options.storage.remove({ | |
| 634 | − scopeId: source.scopeId, | |
| 635 | − sourceId: source.id, | |
| 636 | − }); | |
| 637 | − } | |
| 638 | − | |
| 639 | − return yield* host.sources.remove(source.id); | |
| 640 | − }), | |
| 641 | − }), | |
| 683 | + getSourceConfig: (sourceId) => | |
| 684 | + provideRuntime(sourceSdk.getSourceConfig(sourceId)), | |
| 685 | + createSource: (input) => | |
| 686 | + provideRuntime(sourceSdk.createSource(input)), | |
| 687 | + updateSource: (input) => | |
| 688 | + provideRuntime(sourceSdk.updateSource(input)), | |
| 689 | + removeSource: (sourceId) => | |
| 690 | + provideRuntime(sourceSdk.removeSource(sourceId)), | |
| 691 | + }; | |
| 692 | + }, | |
| 642 | 693 | }); |