Showing the changes to plugins/openapi (1 of 78 files). Show all
Unified · Split
| @@ -9,9 +9,12 @@ import { · 8 unmodified lines | |||
| 9 | import type { Source } from "@executor/platform-sdk/schema"; | 9 | import type { Source } from "@executor/platform-sdk/schema"; |
| 10 | import type { | 10 | import type { |
| 11 | ExecutorSdkPlugin, | 11 | ExecutorSdkPlugin, |
| 12 | ExecutorSdkPluginHost, | ||
| 13 | ExecutorSourceConnector, | ||
| 12 | SourcePluginRuntime, | 14 | SourcePluginRuntime, |
| 13 | } from "@executor/platform-sdk/plugins"; | 15 | } from "@executor/platform-sdk/plugins"; |
| 14 | import { | 16 | import { |
| 17 | OpenApiConnectionAuthSchema, | ||
| 15 | deriveOpenApiNamespace, | 18 | deriveOpenApiNamespace, |
| 16 | previewOpenApiDocument, | 19 | previewOpenApiDocument, |
| 17 | type OpenApiConnectInput, | 20 | type OpenApiConnectInput, |
| @@ -90,6 +93,16 @@ export type OpenApiSdkPluginOptions = { · 72 unmodified lines | |||
| 90 | secrets?: OpenApiSecrets; | 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 | const createStoredSourceData = ( | 106 | const createStoredSourceData = ( |
| 94 | input: OpenApiConnectInput, | 107 | input: OpenApiConnectInput, |
| 95 | ): OpenApiStoredSourceData => ({ | 108 | ): OpenApiStoredSourceData => ({ |
| @@ -249,6 +262,117 @@ const responseHeadersRecord = (response: Response): Record<string, string> => { · 153 unmodified lines | |||
| 249 | return headers; | 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 | const decodeResponseBody = async (response: Response): Promise<unknown> => { | 376 | const decodeResponseBody = async (response: Response): Promise<unknown> => { |
| 253 | if (response.status === 204) { | 377 | if (response.status === 204) { |
| 254 | return null; | 378 | return null; |
| @@ -541,102 +665,29 @@ export const openApiSdkPlugin = ( · 286 unmodified lines | |||
| 541 | ): ExecutorSdkPlugin<"openapi", OpenApiSdk> => ({ | 665 | ): ExecutorSdkPlugin<"openapi", OpenApiSdk> => ({ |
| 542 | key: "openapi", | 666 | key: "openapi", |
| 543 | sources: [createOpenApiSourceRuntime(options)], | 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 | previewDocument: (input) => | 677 | previewDocument: (input) => |
| 546 | Effect.tryPromise({ | 678 | Effect.tryPromise({ |
| 547 | try: () => previewOpenApiDocument(input), | 679 | try: () => previewOpenApiDocument(input), |
| 548 | catch: (cause) => | 680 | catch: (cause) => |
| 549 | cause instanceof Error ? cause : new Error(String(cause)), | 681 | cause instanceof Error ? cause : new Error(String(cause)), |
| 550 | }), | 682 | }), |
| 551 | getSourceConfig: (sourceId) => | 683 | getSourceConfig: (sourceId) => |
| 552 | Effect.gen(function* () { | 684 | provideRuntime(sourceSdk.getSourceConfig(sourceId)), |
| 553 | const source = yield* host.sources.get(sourceId); | 685 | createSource: (input) => |
| 554 | if (source.kind !== "openapi") { | 686 | provideRuntime(sourceSdk.createSource(input)), |
| 555 | return yield* Effect.fail( | 687 | updateSource: (input) => |
| 556 | new Error(`Source ${sourceId} is not an OpenAPI source.`), | 688 | provideRuntime(sourceSdk.updateSource(input)), |
| 557 | ); | 689 | removeSource: (sourceId) => |
| 558 | } | 690 | provideRuntime(sourceSdk.removeSource(sourceId)), |
| 559 | 691 | }; | |
| 560 | const stored = yield* options.storage.get({ | 692 | }, |
| 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 | }), | ||
| 642 | }); | 693 | }); |