Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/plugins/graphql/src/api/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ const AddIntegrationResponse = Schema.Struct({
name: Schema.String,
});

// The full opaque integration config, surfaced for the configure UX. Carries
// the `authenticationTemplate` the configure / custom-method flow reads/writes.
// The integration config surfaced for the configure UX. Carries the
// `authenticationTemplate` the configure / custom-method flow reads/writes.
// The introspection snapshot is deliberately NOT served: it's a multi-MB
// build artifact in the plugin blob store, and no client reads it.
const GraphqlConfigView = Schema.Struct({
endpoint: Schema.String,
name: Schema.String,
introspectionJson: Schema.optional(Schema.String),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
authenticationTemplate: Schema.Array(GraphqlAuthMethod),
Expand Down
1 change: 0 additions & 1 deletion packages/plugins/graphql/src/api/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export const GraphqlHandlers = HttpApiBuilder.group(ExecutorApiWithGraphql, "gra
? {
endpoint: config.endpoint,
name: config.name,
introspectionJson: config.introspectionJson,
headers: config.headers ? { ...config.headers } : undefined,
queryParams: config.queryParams ? { ...config.queryParams } : undefined,
authenticationTemplate: [...config.authenticationTemplate],
Expand Down
31 changes: 12 additions & 19 deletions packages/plugins/graphql/src/sdk/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,18 +414,18 @@ const introspectHeadersForConnection = (
return { headers, queryParams };
};

/** Resolve a config's introspection snapshot text: legacy rows inline it in
* `introspectionJson`; new rows point at the plugin blob store via
* `introspectionHash`. Null when the integration has no snapshot (live
* introspection territory). */
/** Resolve a config's introspection snapshot text from the plugin blob store
* (`introspectionHash`). Null when the integration has no snapshot (live
* introspection territory). Pre-blob rows that inlined the JSON are
* rewritten by the introspection-to-blob migrations before this code reads
* them. */
const loadIntrospectionJson = (
storage: GraphqlStore,
config: GraphqlIntegrationConfig,
): Effect.Effect<string | null, StorageFailure> => {
if (config.introspectionJson != null) return Effect.succeed(config.introspectionJson);
if (config.introspectionHash != null) return storage.getIntrospection(config.introspectionHash);
return Effect.succeed(null);
};
): Effect.Effect<string | null, StorageFailure> =>
config.introspectionHash != null
? storage.getIntrospection(config.introspectionHash)
: Effect.succeed(null);

/** Introspect a config live or from its stored snapshot, applying connection
* auth. A non-null `introspectionJson` (loaded via `loadIntrospectionJson`)
Expand Down Expand Up @@ -558,9 +558,6 @@ const makeGraphqlExtension = (ctx: PluginCtx<GraphqlStore>) => {
GraphqlIntegrationConfig.make({
endpoint: input.endpoint,
name: input.name?.trim() || slugFromEndpoint(input.endpoint),
...(input.introspectionJson !== undefined
? { introspectionJson: input.introspectionJson }
: {}),
...(input.headers !== undefined ? { headers: input.headers } : {}),
...(input.queryParams !== undefined ? { queryParams: input.queryParams } : {}),
authenticationTemplate: input.authenticationTemplate
Expand Down Expand Up @@ -602,7 +599,7 @@ const makeGraphqlExtension = (ctx: PluginCtx<GraphqlStore>) => {
// their operation bindings) are produced lazily when a connection is
// created (`resolveTools`) / a tool is first invoked (`invokeTool`),
// using that connection's credential.
if (baseConfig.introspectionJson === undefined) {
if (input.introspectionJson === undefined) {
yield* ctx.transaction(
ctx.core.integrations.register({
slug,
Expand All @@ -617,7 +614,7 @@ const makeGraphqlExtension = (ctx: PluginCtx<GraphqlStore>) => {

// Pre-supplied introspection JSON: parse it offline (no network) and
// persist the operation bindings + snapshot so production stays offline.
const introspection = yield* parseIntrospectionJson(baseConfig.introspectionJson);
const introspection = yield* parseIntrospectionJson(input.introspectionJson);
const { result } = yield* extract(introspection);
const prepared = prepareOperations(result.fields, introspection);

Expand All @@ -629,9 +626,8 @@ const makeGraphqlExtension = (ctx: PluginCtx<GraphqlStore>) => {
// carries only its hash.
const snapshotJson = JSON.stringify({ data: introspection });
const introspectionHash = yield* sha256Hex(snapshotJson);
const { introspectionJson: _inline, ...withoutInline } = baseConfig;
const config = GraphqlIntegrationConfig.make({
...withoutInline,
...baseConfig,
introspectionHash,
});

Expand Down Expand Up @@ -677,9 +673,6 @@ const makeGraphqlExtension = (ctx: PluginCtx<GraphqlStore>) => {
const next = GraphqlIntegrationConfig.make({
endpoint: input.endpoint ?? current.endpoint,
name: input.name?.trim() || current.name,
...(current.introspectionJson !== undefined
? { introspectionJson: current.introspectionJson }
: {}),
...(current.introspectionHash !== undefined
? { introspectionHash: current.introspectionHash }
: {}),
Expand Down
8 changes: 3 additions & 5 deletions packages/plugins/graphql/src/sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,10 @@ export const GraphqlIntegrationConfig = Schema.Struct({
endpoint: Schema.String,
/** Display name for the integration. */
name: Schema.String,
/** Inlined introspection JSON text. Legacy rows only: new snapshots live in
* the plugin blob store and the config carries `introspectionHash` instead,
* so a multi-MB schema never rides along on catalog reads. */
introspectionJson: Schema.optional(Schema.String),
/** Hex SHA-256 of the introspection JSON snapshot — the content address of
* the blob (`introspection/<hash>` in the plugin blob store). */
* the blob (`introspection/<hash>` in the plugin blob store). Rows that
* predate the blob store (inline `introspectionJson` text) are rewritten
* by the introspection-to-blob migrations before this schema sees them. */
introspectionHash: Schema.optional(Schema.String),
/** Static headers applied to every request (and to add-time introspection). */
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
Expand Down
13 changes: 6 additions & 7 deletions packages/plugins/openapi/src/sdk/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import type { Authentication } from "./types";
//
// In v2 there are NO credential bindings, NO per-source secret slots, and NO
// StoredSource credential config. The config carries only:
// - the content hash of the spec blob (legacy rows inline the text instead)
// and/or the source URL to (re)fetch from,
// - the content hash of the spec blob and/or the source URL to (re)fetch
// from,
// - the optional base URL override,
// - the auth templates a connection's value is rendered through.
// The resolved spec text itself lives in the plugin blob store, keyed
// `spec/<specHash>` — it's a build input for resolveTools/refresh, not data
// any list/invoke path should pay to load.
// any list/invoke path should pay to load. Rows that predate the blob store
// (inline `spec` text) are rewritten before this schema sees them: cloud by
// the out-of-band migrate-specs-to-blobs script, the libSQL hosts by the
// boot-time ledger migration.
// ---------------------------------------------------------------------------

const OAuthAuthenticationSchema = Schema.Struct({
Expand All @@ -35,10 +38,6 @@ const OAuthAuthenticationSchema = Schema.Struct({
export const AuthenticationSchema = Schema.Union([OAuthAuthenticationSchema, ApiKeyAuthMethod]);

export const OpenApiIntegrationConfigSchema = Schema.Struct({
/** Inlined OpenAPI document text. Legacy rows only: new registrations store
* the resolved text in the plugin blob store and carry `specHash` instead,
* so multi-MB documents never ride along on catalog reads. */
spec: Schema.optional(Schema.String),
/** Hex SHA-256 of the resolved spec text — the content address of the spec
* blob (`spec/<hash>` in the plugin blob store). */
specHash: Schema.optional(Schema.String),
Expand Down
12 changes: 4 additions & 8 deletions packages/plugins/openapi/src/sdk/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,19 +542,15 @@ export const describeOpenApiIntegrationDisplay = (

// ---------------------------------------------------------------------------
// Spec text resolution — the stored config carries the spec's content hash
// (`specHash` → blob `spec/<hash>`); legacy rows still inline the text in
// `spec`. Every reader goes through this loader so both shapes work until the
// inline rows are backfilled.
// (`specHash` → blob `spec/<hash>`). Pre-blob rows that inlined the text are
// rewritten by the spec-to-blob migrations before this code reads them.
// ---------------------------------------------------------------------------

const loadSpecText = (
storage: OpenapiStore,
config: OpenApiIntegrationConfig,
): Effect.Effect<string | null, StorageFailure> => {
if (config.spec != null) return Effect.succeed(config.spec);
if (config.specHash != null) return storage.getSpec(config.specHash);
return Effect.succeed(null);
};
): Effect.Effect<string | null, StorageFailure> =>
config.specHash != null ? storage.getSpec(config.specHash) : Effect.succeed(null);

// ---------------------------------------------------------------------------
// Spec → tool definitions (shared by addSpec, resolveTools, and detect)
Expand Down
61 changes: 35 additions & 26 deletions packages/plugins/openapi/src/sdk/spec-blob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe("OpenAPI plugin — spec blob storage", () => {
});

const config = yield* executor.openapi.getConfig("blob_api");
expect(config?.spec).toBeUndefined();
expect(Object.keys(config ?? {})).not.toContain("spec");
expect(config?.specHash).toBe(yield* sha256Hex(text));
}),
),
Expand Down Expand Up @@ -126,41 +126,50 @@ describe("OpenAPI plugin — spec blob storage", () => {
),
);

it.effect("resolveTools still derives tools from a legacy inline-spec config", () =>
it.effect("resolveTools reads the spec from the store, never an inline field", () =>
Effect.gen(function* () {
const plugin = openApiPlugin({ httpClientLayer: FetchHttpClient.layer });
// A legacy row inlines the spec; the loader must never touch the store.
const text = specText();
const hash = yield* sha256Hex(text);
const storage: OpenapiStore = {
putOperations: () => Effect.void,
getOperation: () => Effect.succeed(null),
listOperations: () => Effect.succeed([]),
removeOperations: () => Effect.void,
putSpec: () => Effect.void,
getSpec: () => Effect.succeed(null),
getSpec: (specHash) => Effect.succeed(specHash === hash ? text : null),
};

const result = yield* plugin.resolveTools!({
integration: {
slug: IntegrationSlug.make("legacy_api"),
description: "legacy",
kind: "openapi",
canRemove: true,
canRefresh: false,
authMethods: [],
},
config: { spec: specText() } as IntegrationConfig,
connection: {
owner: "org",
integration: IntegrationSlug.make("legacy_api"),
name: ConnectionName.make("main"),
},
template: null,
storage,
getValue: () => Effect.succeed(null),
getValues: () => Effect.succeed({}),
});

expect(result.tools.map((tool) => String(tool.name))).toContain("items.echoHeaders");
const resolve = (config: IntegrationConfig) =>
plugin.resolveTools!({
integration: {
slug: IntegrationSlug.make("pointer_api"),
description: "pointer",
kind: "openapi",
canRemove: true,
canRefresh: false,
authMethods: [],
},
config,
connection: {
owner: "org",
integration: IntegrationSlug.make("pointer_api"),
name: ConnectionName.make("main"),
},
template: null,
storage,
getValue: () => Effect.succeed(null),
getValues: () => Effect.succeed({}),
});

const fromPointer = yield* resolve({ specHash: hash } as IntegrationConfig);
expect(fromPointer.tools.map((tool) => String(tool.name))).toContain("items.echoHeaders");

// A pre-migration row that still inlines `spec` yields no tools: the
// spec-to-blob migrations rewrite those rows before this code runs, so
// the runtime carries no inline-read path.
const fromInline = yield* resolve({ spec: text } as IntegrationConfig);
expect(fromInline.tools).toHaveLength(0);
}),
);

Expand Down
Loading