From 22e0bb77eab0352ffa9917a26ea7f2e8a001fa6e Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Mon, 4 May 2026 12:16:54 -0700 Subject: [PATCH] Normalize openapi plugin secret/connection refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move secret/connection references out of the JSON columns on openapi_source and openapi_source_binding into: - openapi_source_binding gains kind/secret_id/connection_id/text_value columns (replacing the discriminated-union json value) - openapi_source_query_param: child table for query_params, mirrors graphql_source_query_param shape - openapi_source_spec_fetch_header / spec_fetch_query_param: child tables for invocation_config.specFetchCredentials.{headers,queryParams} Drops openapi_source.query_params and openapi_source.invocation_config JSON columns. headers and oauth2 stay JSON because they hold slot names rather than direct refs — the slots resolve through bindings, which ARE normalized. Plugin gains usagesForSecret / usagesForConnection that fan out across all four locations in one indexed SELECT each. secrets.remove now correctly RESTRICTs when an openapi binding still uses the secret. Migration 0008 backfills via json_extract / json_each, then drops the old columns. Three migration tests cover the backfill from a hand-seeded pre-migration DB; two plugin-level tests cover usage fan-out and remove RESTRICT. --- apps/cloud/drizzle/0009_normalize_openapi.sql | 127 + apps/cloud/drizzle/meta/0009_snapshot.json | 2261 +++++++++++++++++ apps/cloud/drizzle/meta/_journal.json | 7 + apps/cloud/src/services/executor-schema.ts | 59 +- apps/local/drizzle/0008_normalize_openapi.sql | 153 ++ apps/local/drizzle/meta/0008_snapshot.json | 1903 ++++++++++++++ apps/local/drizzle/meta/_journal.json | 7 + apps/local/src/server/executor-schema.ts | 59 +- apps/local/src/server/migrate-connections.ts | 5 + .../server/migrate-graphql-bindings.test.ts | 52 +- .../server/migrate-openapi-bindings.test.ts | 302 +++ .../plugins/openapi/src/sdk/plugin.test.ts | 106 +- packages/plugins/openapi/src/sdk/plugin.ts | 77 + packages/plugins/openapi/src/sdk/store.ts | 482 +++- 14 files changed, 5513 insertions(+), 87 deletions(-) create mode 100644 apps/cloud/drizzle/0009_normalize_openapi.sql create mode 100644 apps/cloud/drizzle/meta/0009_snapshot.json create mode 100644 apps/local/drizzle/0008_normalize_openapi.sql create mode 100644 apps/local/drizzle/meta/0008_snapshot.json create mode 100644 apps/local/src/server/migrate-openapi-bindings.test.ts diff --git a/apps/cloud/drizzle/0009_normalize_openapi.sql b/apps/cloud/drizzle/0009_normalize_openapi.sql new file mode 100644 index 000000000..5423a019b --- /dev/null +++ b/apps/cloud/drizzle/0009_normalize_openapi.sql @@ -0,0 +1,127 @@ +-- Normalize openapi plugin: move every direct secret/connection ref out +-- of JSON columns into proper relational shape. pg port of +-- apps/local/drizzle/0008_normalize_openapi.sql. + +CREATE TABLE "openapi_source_query_param" ( + "id" text NOT NULL, + "scope_id" text NOT NULL, + "source_id" text NOT NULL, + "name" text NOT NULL, + "kind" text NOT NULL, + "text_value" text, + "secret_id" text, + "secret_prefix" text, + CONSTRAINT "openapi_source_query_param_scope_id_id_pk" PRIMARY KEY("scope_id","id") +); +--> statement-breakpoint +CREATE INDEX "openapi_source_query_param_scope_id_idx" ON "openapi_source_query_param" USING btree ("scope_id");--> statement-breakpoint +CREATE INDEX "openapi_source_query_param_source_id_idx" ON "openapi_source_query_param" USING btree ("source_id");--> statement-breakpoint +CREATE INDEX "openapi_source_query_param_secret_id_idx" ON "openapi_source_query_param" USING btree ("secret_id");--> statement-breakpoint + +CREATE TABLE "openapi_source_spec_fetch_header" ( + "id" text NOT NULL, + "scope_id" text NOT NULL, + "source_id" text NOT NULL, + "name" text NOT NULL, + "kind" text NOT NULL, + "text_value" text, + "secret_id" text, + "secret_prefix" text, + CONSTRAINT "openapi_source_spec_fetch_header_scope_id_id_pk" PRIMARY KEY("scope_id","id") +); +--> statement-breakpoint +CREATE INDEX "openapi_source_spec_fetch_header_scope_id_idx" ON "openapi_source_spec_fetch_header" USING btree ("scope_id");--> statement-breakpoint +CREATE INDEX "openapi_source_spec_fetch_header_source_id_idx" ON "openapi_source_spec_fetch_header" USING btree ("source_id");--> statement-breakpoint +CREATE INDEX "openapi_source_spec_fetch_header_secret_id_idx" ON "openapi_source_spec_fetch_header" USING btree ("secret_id");--> statement-breakpoint + +CREATE TABLE "openapi_source_spec_fetch_query_param" ( + "id" text NOT NULL, + "scope_id" text NOT NULL, + "source_id" text NOT NULL, + "name" text NOT NULL, + "kind" text NOT NULL, + "text_value" text, + "secret_id" text, + "secret_prefix" text, + CONSTRAINT "openapi_source_spec_fetch_query_param_scope_id_id_pk" PRIMARY KEY("scope_id","id") +); +--> statement-breakpoint +CREATE INDEX "openapi_source_spec_fetch_query_param_scope_id_idx" ON "openapi_source_spec_fetch_query_param" USING btree ("scope_id");--> statement-breakpoint +CREATE INDEX "openapi_source_spec_fetch_query_param_source_id_idx" ON "openapi_source_spec_fetch_query_param" USING btree ("source_id");--> statement-breakpoint +CREATE INDEX "openapi_source_spec_fetch_query_param_secret_id_idx" ON "openapi_source_spec_fetch_query_param" USING btree ("secret_id");--> statement-breakpoint + +-- New columns on openapi_source_binding to flatten the value json. +-- `kind` defaults to 'text' so the ALTER works on existing rows; the +-- backfill below stamps the real value. +ALTER TABLE "openapi_source_binding" ADD COLUMN "kind" text DEFAULT 'text' NOT NULL;--> statement-breakpoint +ALTER TABLE "openapi_source_binding" ADD COLUMN "secret_id" text;--> statement-breakpoint +ALTER TABLE "openapi_source_binding" ADD COLUMN "connection_id" text;--> statement-breakpoint +ALTER TABLE "openapi_source_binding" ADD COLUMN "text_value" text;--> statement-breakpoint +CREATE INDEX "openapi_source_binding_secret_id_idx" ON "openapi_source_binding" USING btree ("secret_id");--> statement-breakpoint +CREATE INDEX "openapi_source_binding_connection_id_idx" ON "openapi_source_binding" USING btree ("connection_id");--> statement-breakpoint + +UPDATE "openapi_source_binding" +SET + "kind" = COALESCE("value"->>'kind', 'text'), + "secret_id" = CASE WHEN "value"->>'kind' = 'secret' THEN "value"->>'secretId' ELSE NULL END, + "connection_id" = CASE WHEN "value"->>'kind' = 'connection' THEN "value"->>'connectionId' ELSE NULL END, + "text_value" = CASE WHEN "value"->>'kind' = 'text' THEN "value"->>'text' ELSE NULL END +WHERE "value" IS NOT NULL;--> statement-breakpoint + +INSERT INTO "openapi_source_query_param" + ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") +SELECT + s."scope_id", + '[' || to_jsonb(s."id")::text || ',' || to_jsonb(q.key)::text || ']', + s."id", + q.key, + CASE + WHEN jsonb_typeof(q.value) = 'object' AND q.value ? 'secretId' THEN 'secret' + ELSE 'text' + END, + CASE WHEN jsonb_typeof(q.value) = 'string' THEN q.value #>> '{}' ELSE NULL END, + CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'secretId' ELSE NULL END, + CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'prefix' ELSE NULL END +FROM "openapi_source" s, jsonb_each(s."query_params") q +WHERE s."query_params" IS NOT NULL +ON CONFLICT DO NOTHING;--> statement-breakpoint + +INSERT INTO "openapi_source_spec_fetch_header" + ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") +SELECT + s."scope_id", + '[' || to_jsonb(s."id")::text || ',' || to_jsonb(h.key)::text || ']', + s."id", + h.key, + CASE + WHEN jsonb_typeof(h.value) = 'object' AND h.value ? 'secretId' THEN 'secret' + ELSE 'text' + END, + CASE WHEN jsonb_typeof(h.value) = 'string' THEN h.value #>> '{}' ELSE NULL END, + CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'secretId' ELSE NULL END, + CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'prefix' ELSE NULL END +FROM "openapi_source" s, jsonb_each(s."invocation_config"->'specFetchCredentials'->'headers') h +WHERE s."invocation_config"->'specFetchCredentials'->'headers' IS NOT NULL +ON CONFLICT DO NOTHING;--> statement-breakpoint + +INSERT INTO "openapi_source_spec_fetch_query_param" + ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") +SELECT + s."scope_id", + '[' || to_jsonb(s."id")::text || ',' || to_jsonb(q.key)::text || ']', + s."id", + q.key, + CASE + WHEN jsonb_typeof(q.value) = 'object' AND q.value ? 'secretId' THEN 'secret' + ELSE 'text' + END, + CASE WHEN jsonb_typeof(q.value) = 'string' THEN q.value #>> '{}' ELSE NULL END, + CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'secretId' ELSE NULL END, + CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'prefix' ELSE NULL END +FROM "openapi_source" s, jsonb_each(s."invocation_config"->'specFetchCredentials'->'queryParams') q +WHERE s."invocation_config"->'specFetchCredentials'->'queryParams' IS NOT NULL +ON CONFLICT DO NOTHING;--> statement-breakpoint + +ALTER TABLE "openapi_source_binding" DROP COLUMN "value";--> statement-breakpoint +ALTER TABLE "openapi_source" DROP COLUMN "query_params";--> statement-breakpoint +ALTER TABLE "openapi_source" DROP COLUMN "invocation_config"; diff --git a/apps/cloud/drizzle/meta/0009_snapshot.json b/apps/cloud/drizzle/meta/0009_snapshot.json new file mode 100644 index 000000000..a76525b6d --- /dev/null +++ b/apps/cloud/drizzle/meta/0009_snapshot.json @@ -0,0 +1,2261 @@ +{ + "id": "8bfee8ad-2ac4-4d42-bff7-3330052ce94c", + "prevId": "67f25bbd-69f4-4d0d-af28-72a218b0ed45", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memberships_account_id_organization_id_pk": { + "name": "memberships_account_id_organization_id_pk", + "columns": [ + "account_id", + "organization_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob": { + "name": "blob", + "schema": "", + "columns": { + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "blob_namespace_key_pk": { + "name": "blob_namespace_key_pk", + "columns": [ + "namespace", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection": { + "name": "connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_secret_id": { + "name": "access_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_secret_id": { + "name": "refresh_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_state": { + "name": "provider_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connection_scope_id_idx": { + "name": "connection_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_provider_idx": { + "name": "connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connection_scope_id_id_pk": { + "name": "connection_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.definition": { + "name": "definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "definition_scope_id_idx": { + "name": "definition_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "definition_source_id_idx": { + "name": "definition_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "definition_plugin_id_idx": { + "name": "definition_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "definition_scope_id_id_pk": { + "name": "definition_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_operation": { + "name": "graphql_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "graphql_operation_scope_id_idx": { + "name": "graphql_operation_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_operation_source_id_idx": { + "name": "graphql_operation_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_operation_scope_id_id_pk": { + "name": "graphql_operation_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_source": { + "name": "graphql_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "none" + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "graphql_source_scope_id_idx": { + "name": "graphql_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_auth_connection_id_idx": { + "name": "graphql_source_auth_connection_id_idx", + "columns": [ + { + "expression": "auth_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_scope_id_id_pk": { + "name": "graphql_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_binding": { + "name": "mcp_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_binding_scope_id_idx": { + "name": "mcp_binding_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_binding_source_id_idx": { + "name": "mcp_binding_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_binding_scope_id_id_pk": { + "name": "mcp_binding_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_source": { + "name": "mcp_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_source_scope_id_idx": { + "name": "mcp_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_scope_id_id_pk": { + "name": "mcp_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth2_session": { + "name": "oauth2_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_scope": { + "name": "token_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth2_session_scope_id_idx": { + "name": "oauth2_session_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth2_session_plugin_id_idx": { + "name": "oauth2_session_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth2_session_connection_id_idx": { + "name": "oauth2_session_connection_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauth2_session_scope_id_id_pk": { + "name": "oauth2_session_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_operation": { + "name": "openapi_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "openapi_operation_scope_id_idx": { + "name": "openapi_operation_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_operation_source_id_idx": { + "name": "openapi_operation_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_operation_scope_id_id_pk": { + "name": "openapi_operation_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source": { + "name": "openapi_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "oauth2": { + "name": "oauth2", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_scope_id_idx": { + "name": "openapi_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_scope_id_id_pk": { + "name": "openapi_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_binding": { + "name": "openapi_source_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_scope_id": { + "name": "source_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_scope_id": { + "name": "target_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slot": { + "name": "slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_binding_source_id_idx": { + "name": "openapi_source_binding_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_source_scope_id_idx": { + "name": "openapi_source_binding_source_scope_id_idx", + "columns": [ + { + "expression": "source_scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_target_scope_id_idx": { + "name": "openapi_source_binding_target_scope_id_idx", + "columns": [ + { + "expression": "target_scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_slot_idx": { + "name": "openapi_source_binding_slot_idx", + "columns": [ + { + "expression": "slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_secret_id_idx": { + "name": "openapi_source_binding_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_connection_id_idx": { + "name": "openapi_source_binding_connection_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret": { + "name": "secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_connection_id": { + "name": "owned_by_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "secret_scope_id_idx": { + "name": "secret_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_provider_idx": { + "name": "secret_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_owned_by_connection_id_idx": { + "name": "secret_owned_by_connection_id_idx", + "columns": [ + { + "expression": "owned_by_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "secret_scope_id_id_pk": { + "name": "secret_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source": { + "name": "source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "can_remove": { + "name": "can_remove", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_edit": { + "name": "can_edit", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "source_scope_id_idx": { + "name": "source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "source_plugin_id_idx": { + "name": "source_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "source_scope_id_id_pk": { + "name": "source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool": { + "name": "tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_scope_id_idx": { + "name": "tool_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_source_id_idx": { + "name": "tool_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_plugin_id_idx": { + "name": "tool_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_scope_id_id_pk": { + "name": "tool_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policy": { + "name": "tool_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_policy_scope_id_position_idx": { + "name": "tool_policy_scope_id_position_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_policy_scope_id_id_pk": { + "name": "tool_policy_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workos_vault_metadata": { + "name": "workos_vault_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workos_vault_metadata_scope_id_idx": { + "name": "workos_vault_metadata_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workos_vault_metadata_scope_id_id_pk": { + "name": "workos_vault_metadata_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_source_header": { + "name": "graphql_source_header", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "graphql_source_header_scope_id_idx": { + "name": "graphql_source_header_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_header_source_id_idx": { + "name": "graphql_source_header_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_header_secret_id_idx": { + "name": "graphql_source_header_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_header_scope_id_id_pk": { + "name": "graphql_source_header_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_source_query_param": { + "name": "graphql_source_query_param", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "graphql_source_query_param_scope_id_idx": { + "name": "graphql_source_query_param_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_query_param_source_id_idx": { + "name": "graphql_source_query_param_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_source_query_param_secret_id_idx": { + "name": "graphql_source_query_param_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_query_param_scope_id_id_pk": { + "name": "graphql_source_query_param_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_query_param": { + "name": "openapi_source_query_param", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_query_param_scope_id_idx": { + "name": "openapi_source_query_param_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_query_param_source_id_idx": { + "name": "openapi_source_query_param_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_query_param_secret_id_idx": { + "name": "openapi_source_query_param_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_query_param_scope_id_id_pk": { + "name": "openapi_source_query_param_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_spec_fetch_header": { + "name": "openapi_source_spec_fetch_header", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_spec_fetch_header_scope_id_idx": { + "name": "openapi_source_spec_fetch_header_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_spec_fetch_header_source_id_idx": { + "name": "openapi_source_spec_fetch_header_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_spec_fetch_header_secret_id_idx": { + "name": "openapi_source_spec_fetch_header_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_header_scope_id_id_pk": { + "name": "openapi_source_spec_fetch_header_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_spec_fetch_query_param": { + "name": "openapi_source_spec_fetch_query_param", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "openapi_source_spec_fetch_query_param_scope_id_idx": { + "name": "openapi_source_spec_fetch_query_param_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_spec_fetch_query_param_source_id_idx": { + "name": "openapi_source_spec_fetch_query_param_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_spec_fetch_query_param_secret_id_idx": { + "name": "openapi_source_spec_fetch_query_param_secret_id_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_query_param_scope_id_id_pk": { + "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/cloud/drizzle/meta/_journal.json b/apps/cloud/drizzle/meta/_journal.json index c8c090d91..4e717cfda 100644 --- a/apps/cloud/drizzle/meta/_journal.json +++ b/apps/cloud/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1778004191000, "tag": "0008_normalize_graphql", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1778004434001, + "tag": "0009_normalize_openapi", + "breakpoints": true } ] } diff --git a/apps/cloud/src/services/executor-schema.ts b/apps/cloud/src/services/executor-schema.ts index f07f71d78..1b5d1626d 100644 --- a/apps/cloud/src/services/executor-schema.ts +++ b/apps/cloud/src/services/executor-schema.ts @@ -122,9 +122,7 @@ export const openapi_source = pgTable("openapi_source", { source_url: text('source_url'), base_url: text('base_url'), headers: jsonb('headers'), - query_params: jsonb('query_params'), - oauth2: jsonb('oauth2'), - invocation_config: jsonb('invocation_config').notNull() + oauth2: jsonb('oauth2') }, (table) => [ primaryKey({ columns: [table.scope_id, table.id] }), index("openapi_source_scope_id_idx").on(table.scope_id), @@ -147,7 +145,10 @@ export const openapi_source_binding = pgTable("openapi_source_binding", { source_scope_id: text('source_scope_id').notNull(), target_scope_id: text('target_scope_id').notNull(), slot: text('slot').notNull(), - value: jsonb('value').notNull(), + kind: text('kind', { enum: ['secret', 'connection', 'text'] }).notNull(), + secret_id: text('secret_id'), + connection_id: text('connection_id'), + text_value: text('text_value'), created_at: timestamp('created_at').notNull(), updated_at: timestamp('updated_at').notNull() }, (table) => [ @@ -155,6 +156,56 @@ export const openapi_source_binding = pgTable("openapi_source_binding", { index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id), index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id), index("openapi_source_binding_slot_idx").on(table.slot), + index("openapi_source_binding_secret_id_idx").on(table.secret_id), + index("openapi_source_binding_connection_id_idx").on(table.connection_id), +]); + +export const openapi_source_query_param = pgTable("openapi_source_query_param", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text('kind', { enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("openapi_source_query_param_scope_id_idx").on(table.scope_id), + index("openapi_source_query_param_source_id_idx").on(table.source_id), + index("openapi_source_query_param_secret_id_idx").on(table.secret_id), +]); + +export const openapi_source_spec_fetch_header = pgTable("openapi_source_spec_fetch_header", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text('kind', { enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("openapi_source_spec_fetch_header_scope_id_idx").on(table.scope_id), + index("openapi_source_spec_fetch_header_source_id_idx").on(table.source_id), + index("openapi_source_spec_fetch_header_secret_id_idx").on(table.secret_id), +]); + +export const openapi_source_spec_fetch_query_param = pgTable("openapi_source_spec_fetch_query_param", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text('kind', { enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("openapi_source_spec_fetch_query_param_scope_id_idx").on(table.scope_id), + index("openapi_source_spec_fetch_query_param_source_id_idx").on(table.source_id), + index("openapi_source_spec_fetch_query_param_secret_id_idx").on(table.secret_id), ]); export const mcp_source = pgTable("mcp_source", { diff --git a/apps/local/drizzle/0008_normalize_openapi.sql b/apps/local/drizzle/0008_normalize_openapi.sql new file mode 100644 index 000000000..cdf8779e2 --- /dev/null +++ b/apps/local/drizzle/0008_normalize_openapi.sql @@ -0,0 +1,153 @@ +-- Normalize openapi plugin: move every direct secret/connection ref out +-- of JSON columns into proper relational shape. +-- +-- Old shape: +-- openapi_source.query_params json Record +-- openapi_source.invocation_config json { specFetchCredentials?: { headers, queryParams } } +-- openapi_source_binding.value json discriminated union +-- {kind:"secret",secretId} | {kind:"connection",connectionId} | {kind:"text",text} +-- +-- New shape: +-- openapi_source_binding gains kind/secret_id/connection_id/text_value columns. +-- `headers` / `oauth2` on openapi_source stay JSON because they hold +-- slot names, not direct refs — the actual credentials reach those +-- slots through openapi_source_binding rows, which ARE normalized. +-- openapi_source_query_param: child table, secret-backed entries. +-- openapi_source_spec_fetch_header / spec_fetch_query_param: child +-- tables for the equivalent maps inside specFetchCredentials. + +CREATE TABLE `openapi_source_query_param` ( + `id` text NOT NULL, + `scope_id` text NOT NULL, + `source_id` text NOT NULL, + `name` text NOT NULL, + `kind` text NOT NULL, + `text_value` text, + `secret_id` text, + `secret_prefix` text, + PRIMARY KEY(`scope_id`, `id`) +); +--> statement-breakpoint +CREATE INDEX `openapi_source_query_param_scope_id_idx` ON `openapi_source_query_param` (`scope_id`);--> statement-breakpoint +CREATE INDEX `openapi_source_query_param_source_id_idx` ON `openapi_source_query_param` (`source_id`);--> statement-breakpoint +CREATE INDEX `openapi_source_query_param_secret_id_idx` ON `openapi_source_query_param` (`secret_id`);--> statement-breakpoint + +CREATE TABLE `openapi_source_spec_fetch_header` ( + `id` text NOT NULL, + `scope_id` text NOT NULL, + `source_id` text NOT NULL, + `name` text NOT NULL, + `kind` text NOT NULL, + `text_value` text, + `secret_id` text, + `secret_prefix` text, + PRIMARY KEY(`scope_id`, `id`) +); +--> statement-breakpoint +CREATE INDEX `openapi_source_spec_fetch_header_scope_id_idx` ON `openapi_source_spec_fetch_header` (`scope_id`);--> statement-breakpoint +CREATE INDEX `openapi_source_spec_fetch_header_source_id_idx` ON `openapi_source_spec_fetch_header` (`source_id`);--> statement-breakpoint +CREATE INDEX `openapi_source_spec_fetch_header_secret_id_idx` ON `openapi_source_spec_fetch_header` (`secret_id`);--> statement-breakpoint + +CREATE TABLE `openapi_source_spec_fetch_query_param` ( + `id` text NOT NULL, + `scope_id` text NOT NULL, + `source_id` text NOT NULL, + `name` text NOT NULL, + `kind` text NOT NULL, + `text_value` text, + `secret_id` text, + `secret_prefix` text, + PRIMARY KEY(`scope_id`, `id`) +); +--> statement-breakpoint +CREATE INDEX `openapi_source_spec_fetch_query_param_scope_id_idx` ON `openapi_source_spec_fetch_query_param` (`scope_id`);--> statement-breakpoint +CREATE INDEX `openapi_source_spec_fetch_query_param_source_id_idx` ON `openapi_source_spec_fetch_query_param` (`source_id`);--> statement-breakpoint +CREATE INDEX `openapi_source_spec_fetch_query_param_secret_id_idx` ON `openapi_source_spec_fetch_query_param` (`secret_id`);--> statement-breakpoint + +-- New columns on openapi_source_binding to flatten the value json. +-- `kind` defaults to 'text' so the ALTER works on existing rows; the +-- backfill below stamps the real value. +ALTER TABLE `openapi_source_binding` ADD `kind` text DEFAULT 'text' NOT NULL;--> statement-breakpoint +ALTER TABLE `openapi_source_binding` ADD `secret_id` text;--> statement-breakpoint +ALTER TABLE `openapi_source_binding` ADD `connection_id` text;--> statement-breakpoint +ALTER TABLE `openapi_source_binding` ADD `text_value` text;--> statement-breakpoint +CREATE INDEX `openapi_source_binding_secret_id_idx` ON `openapi_source_binding` (`secret_id`);--> statement-breakpoint +CREATE INDEX `openapi_source_binding_connection_id_idx` ON `openapi_source_binding` (`connection_id`);--> statement-breakpoint + +-- Backfill the binding columns from the legacy `value` JSON. We pull +-- $.kind into `kind` directly; for each kind the matching id field +-- (`secretId` / `connectionId` / `text`) gets copied into the matching +-- column. Rows whose value JSON is malformed or missing $.kind fall +-- through to kind='text' with a NULL text_value — same as a missing +-- text binding, the source will surface "binding not configured" at +-- invoke time rather than crashing the migration. +UPDATE `openapi_source_binding` +SET + `kind` = COALESCE(json_extract(`value`, '$.kind'), 'text'), + `secret_id` = CASE WHEN json_extract(`value`, '$.kind') = 'secret' THEN json_extract(`value`, '$.secretId') ELSE NULL END, + `connection_id` = CASE WHEN json_extract(`value`, '$.kind') = 'connection' THEN json_extract(`value`, '$.connectionId') ELSE NULL END, + `text_value` = CASE WHEN json_extract(`value`, '$.kind') = 'text' THEN json_extract(`value`, '$.text') ELSE NULL END +WHERE `value` IS NOT NULL;--> statement-breakpoint + +-- Backfill openapi_source_query_param from openapi_source.query_params. +-- json_each iterates the keys of the query_params object. For each +-- entry: if the value is an object with .secretId, write a kind=secret +-- row; otherwise write a kind=text row with the literal string. +INSERT OR IGNORE INTO `openapi_source_query_param` + (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) +SELECT + s.`scope_id`, + json_array(s.`id`, q.`key`), + s.`id`, + q.`key`, + CASE + WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' + ELSE 'text' + END, + CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, + CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, + CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END +FROM `openapi_source` s, json_each(s.`query_params`) q +WHERE s.`query_params` IS NOT NULL;--> statement-breakpoint + +-- Backfill openapi_source_spec_fetch_header from +-- openapi_source.invocation_config.specFetchCredentials.headers. Same +-- shape as query_params; the JSON path is one level deeper. +INSERT OR IGNORE INTO `openapi_source_spec_fetch_header` + (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) +SELECT + s.`scope_id`, + json_array(s.`id`, h.`key`), + s.`id`, + h.`key`, + CASE + WHEN h.`type` = 'object' AND json_extract(h.`value`, '$.secretId') IS NOT NULL THEN 'secret' + ELSE 'text' + END, + CASE WHEN h.`type` = 'object' THEN NULL ELSE h.`value` END, + CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.secretId') ELSE NULL END, + CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.prefix') ELSE NULL END +FROM `openapi_source` s, json_each(json_extract(s.`invocation_config`, '$.specFetchCredentials.headers')) h +WHERE json_extract(s.`invocation_config`, '$.specFetchCredentials.headers') IS NOT NULL;--> statement-breakpoint + +INSERT OR IGNORE INTO `openapi_source_spec_fetch_query_param` + (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) +SELECT + s.`scope_id`, + json_array(s.`id`, q.`key`), + s.`id`, + q.`key`, + CASE + WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' + ELSE 'text' + END, + CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, + CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, + CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END +FROM `openapi_source` s, json_each(json_extract(s.`invocation_config`, '$.specFetchCredentials.queryParams')) q +WHERE json_extract(s.`invocation_config`, '$.specFetchCredentials.queryParams') IS NOT NULL;--> statement-breakpoint + +-- Drop the legacy JSON columns now that everything is normalized. +ALTER TABLE `openapi_source_binding` DROP COLUMN `value`;--> statement-breakpoint +ALTER TABLE `openapi_source` DROP COLUMN `query_params`;--> statement-breakpoint +ALTER TABLE `openapi_source` DROP COLUMN `invocation_config`; diff --git a/apps/local/drizzle/meta/0008_snapshot.json b/apps/local/drizzle/meta/0008_snapshot.json new file mode 100644 index 000000000..af1e4943c --- /dev/null +++ b/apps/local/drizzle/meta/0008_snapshot.json @@ -0,0 +1,1903 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "22222222-3333-4444-5555-666666666666", + "prevId": "11111111-2222-3333-4444-555555555555", + "tables": { + "blob": { + "name": "blob", + "columns": { + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "blob_namespace_key_pk": { + "columns": [ + "namespace", + "key" + ], + "name": "blob_namespace_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "connection": { + "name": "connection", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_secret_id": { + "name": "access_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_secret_id": { + "name": "refresh_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_state": { + "name": "provider_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "connection_scope_id_idx": { + "name": "connection_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "connection_provider_idx": { + "name": "connection_provider_idx", + "columns": [ + "provider" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connection_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "connection_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "definition": { + "name": "definition", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "definition_scope_id_idx": { + "name": "definition_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "definition_source_id_idx": { + "name": "definition_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "definition_plugin_id_idx": { + "name": "definition_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "definition_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "definition_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_discovery_binding": { + "name": "google_discovery_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "google_discovery_binding_scope_id_idx": { + "name": "google_discovery_binding_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "google_discovery_binding_source_id_idx": { + "name": "google_discovery_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "google_discovery_binding_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "google_discovery_binding_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_discovery_source": { + "name": "google_discovery_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "google_discovery_source_scope_id_idx": { + "name": "google_discovery_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "google_discovery_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "google_discovery_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_operation": { + "name": "graphql_operation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "graphql_operation_scope_id_idx": { + "name": "graphql_operation_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_operation_source_id_idx": { + "name": "graphql_operation_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_operation_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_operation_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source": { + "name": "graphql_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "auth_connection_id": { + "name": "auth_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_scope_id_idx": { + "name": "graphql_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_auth_connection_id_idx": { + "name": "graphql_source_auth_connection_id_idx", + "columns": [ + "auth_connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_binding": { + "name": "mcp_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_binding_scope_id_idx": { + "name": "mcp_binding_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "mcp_binding_source_id_idx": { + "name": "mcp_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_binding_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_binding_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_source": { + "name": "mcp_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "mcp_source_scope_id_idx": { + "name": "mcp_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "mcp_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth2_session": { + "name": "oauth2_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_scope": { + "name": "token_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth2_session_scope_id_idx": { + "name": "oauth2_session_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "oauth2_session_plugin_id_idx": { + "name": "oauth2_session_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "oauth2_session_connection_id_idx": { + "name": "oauth2_session_connection_id_idx", + "columns": [ + "connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauth2_session_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "oauth2_session_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_operation": { + "name": "openapi_operation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "binding": { + "name": "binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "openapi_operation_scope_id_idx": { + "name": "openapi_operation_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_operation_source_id_idx": { + "name": "openapi_operation_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_operation_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_operation_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source": { + "name": "openapi_source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth2": { + "name": "oauth2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_scope_id_idx": { + "name": "openapi_source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_binding": { + "name": "openapi_source_binding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_scope_id": { + "name": "source_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_scope_id": { + "name": "target_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slot": { + "name": "slot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_binding_source_id_idx": { + "name": "openapi_source_binding_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_binding_source_scope_id_idx": { + "name": "openapi_source_binding_source_scope_id_idx", + "columns": [ + "source_scope_id" + ], + "isUnique": false + }, + "openapi_source_binding_target_scope_id_idx": { + "name": "openapi_source_binding_target_scope_id_idx", + "columns": [ + "target_scope_id" + ], + "isUnique": false + }, + "openapi_source_binding_slot_idx": { + "name": "openapi_source_binding_slot_idx", + "columns": [ + "slot" + ], + "isUnique": false + }, + "openapi_source_binding_secret_id_idx": { + "name": "openapi_source_binding_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + }, + "openapi_source_binding_connection_id_idx": { + "name": "openapi_source_binding_connection_id_idx", + "columns": [ + "connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "secret": { + "name": "secret", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owned_by_connection_id": { + "name": "owned_by_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "secret_scope_id_idx": { + "name": "secret_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "secret_provider_idx": { + "name": "secret_provider_idx", + "columns": [ + "provider" + ], + "isUnique": false + }, + "secret_owned_by_connection_id_idx": { + "name": "secret_owned_by_connection_id_idx", + "columns": [ + "owned_by_connection_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "secret_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "secret_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "source": { + "name": "source", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "can_remove": { + "name": "can_remove", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "can_edit": { + "name": "can_edit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "source_scope_id_idx": { + "name": "source_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "source_plugin_id_idx": { + "name": "source_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "source_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "source_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool": { + "name": "tool", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_schema": { + "name": "input_schema", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_schema": { + "name": "output_schema", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tool_scope_id_idx": { + "name": "tool_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "tool_source_id_idx": { + "name": "tool_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "tool_plugin_id_idx": { + "name": "tool_plugin_id_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "tool_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool_policy": { + "name": "tool_policy", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tool_policy_scope_id_position_idx": { + "name": "tool_policy_scope_id_position_idx", + "columns": [ + "scope_id", + "position" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_policy_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "tool_policy_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source_header": { + "name": "graphql_source_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_header_scope_id_idx": { + "name": "graphql_source_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_header_source_id_idx": { + "name": "graphql_source_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "graphql_source_header_secret_id_idx": { + "name": "graphql_source_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "graphql_source_query_param": { + "name": "graphql_source_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "graphql_source_query_param_scope_id_idx": { + "name": "graphql_source_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "graphql_source_query_param_source_id_idx": { + "name": "graphql_source_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "graphql_source_query_param_secret_id_idx": { + "name": "graphql_source_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "graphql_source_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_query_param": { + "name": "openapi_source_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_query_param_scope_id_idx": { + "name": "openapi_source_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_query_param_source_id_idx": { + "name": "openapi_source_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_query_param_secret_id_idx": { + "name": "openapi_source_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_spec_fetch_header": { + "name": "openapi_source_spec_fetch_header", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_spec_fetch_header_scope_id_idx": { + "name": "openapi_source_spec_fetch_header_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_header_source_id_idx": { + "name": "openapi_source_spec_fetch_header_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_header_secret_id_idx": { + "name": "openapi_source_spec_fetch_header_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_header_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_spec_fetch_header_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "openapi_source_spec_fetch_query_param": { + "name": "openapi_source_spec_fetch_query_param", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text_value": { + "name": "text_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_id": { + "name": "secret_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "openapi_source_spec_fetch_query_param_scope_id_idx": { + "name": "openapi_source_spec_fetch_query_param_scope_id_idx", + "columns": [ + "scope_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_query_param_source_id_idx": { + "name": "openapi_source_spec_fetch_query_param_source_id_idx", + "columns": [ + "source_id" + ], + "isUnique": false + }, + "openapi_source_spec_fetch_query_param_secret_id_idx": { + "name": "openapi_source_spec_fetch_query_param_secret_id_idx", + "columns": [ + "secret_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_spec_fetch_query_param_scope_id_id_pk": { + "columns": [ + "scope_id", + "id" + ], + "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/local/drizzle/meta/_journal.json b/apps/local/drizzle/meta/_journal.json index 804d7dd62..b77702d02 100644 --- a/apps/local/drizzle/meta/_journal.json +++ b/apps/local/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1778100000000, "tag": "0007_normalize_graphql", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1778100000001, + "tag": "0008_normalize_openapi", + "breakpoints": true } ] } diff --git a/apps/local/src/server/executor-schema.ts b/apps/local/src/server/executor-schema.ts index f24dc621e..fa05adfb7 100644 --- a/apps/local/src/server/executor-schema.ts +++ b/apps/local/src/server/executor-schema.ts @@ -122,9 +122,7 @@ export const openapi_source = sqliteTable("openapi_source", { source_url: text('source_url'), base_url: text('base_url'), headers: text('headers', { mode: "json" }), - query_params: text('query_params', { mode: "json" }), - oauth2: text('oauth2', { mode: "json" }), - invocation_config: text('invocation_config', { mode: "json" }).notNull() + oauth2: text('oauth2', { mode: "json" }) }, (table) => [ primaryKey({ columns: [table.scope_id, table.id] }), index("openapi_source_scope_id_idx").on(table.scope_id), @@ -147,7 +145,10 @@ export const openapi_source_binding = sqliteTable("openapi_source_binding", { source_scope_id: text('source_scope_id').notNull(), target_scope_id: text('target_scope_id').notNull(), slot: text('slot').notNull(), - value: text('value', { mode: "json" }).notNull(), + kind: text({ enum: ['secret', 'connection', 'text'] }).notNull(), + secret_id: text('secret_id'), + connection_id: text('connection_id'), + text_value: text('text_value'), created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(), updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull() }, (table) => [ @@ -155,6 +156,56 @@ export const openapi_source_binding = sqliteTable("openapi_source_binding", { index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id), index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id), index("openapi_source_binding_slot_idx").on(table.slot), + index("openapi_source_binding_secret_id_idx").on(table.secret_id), + index("openapi_source_binding_connection_id_idx").on(table.connection_id), +]); + +export const openapi_source_query_param = sqliteTable("openapi_source_query_param", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text({ enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("openapi_source_query_param_scope_id_idx").on(table.scope_id), + index("openapi_source_query_param_source_id_idx").on(table.source_id), + index("openapi_source_query_param_secret_id_idx").on(table.secret_id), +]); + +export const openapi_source_spec_fetch_header = sqliteTable("openapi_source_spec_fetch_header", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text({ enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("openapi_source_spec_fetch_header_scope_id_idx").on(table.scope_id), + index("openapi_source_spec_fetch_header_source_id_idx").on(table.source_id), + index("openapi_source_spec_fetch_header_secret_id_idx").on(table.secret_id), +]); + +export const openapi_source_spec_fetch_query_param = sqliteTable("openapi_source_spec_fetch_query_param", { + id: text('id').notNull(), + scope_id: text('scope_id').notNull(), + source_id: text('source_id').notNull(), + name: text('name').notNull(), + kind: text({ enum: ['text', 'secret'] }).notNull(), + text_value: text('text_value'), + secret_id: text('secret_id'), + secret_prefix: text('secret_prefix') +}, (table) => [ + primaryKey({ columns: [table.scope_id, table.id] }), + index("openapi_source_spec_fetch_query_param_scope_id_idx").on(table.scope_id), + index("openapi_source_spec_fetch_query_param_source_id_idx").on(table.source_id), + index("openapi_source_spec_fetch_query_param_secret_id_idx").on(table.secret_id), ]); export const mcp_source = sqliteTable("mcp_source", { diff --git a/apps/local/src/server/migrate-connections.ts b/apps/local/src/server/migrate-connections.ts index 6e7ac65fa..ecd17bdfd 100644 --- a/apps/local/src/server/migrate-connections.ts +++ b/apps/local/src/server/migrate-connections.ts @@ -250,6 +250,11 @@ type OpenApiRow = { const migrateOpenApi = async (sqlite: Database): Promise => { if (!tableExists(sqlite, "openapi_source")) return; + // After 0008 normalized openapi, the legacy `invocation_config` JSON + // column is gone (specFetchCredentials moved to child tables). + // There's nothing for this legacy backfill to do at that point; skip + // cleanly. `oauth2` stays JSON because it holds slot names, not refs. + if (!columnExists(sqlite, "openapi_source", "invocation_config")) return; const rows = sqlite .prepare( "SELECT scope_id, id, name, spec, invocation_config, oauth2 FROM openapi_source", diff --git a/apps/local/src/server/migrate-graphql-bindings.test.ts b/apps/local/src/server/migrate-graphql-bindings.test.ts index 69c9235a4..b244dbbb2 100644 --- a/apps/local/src/server/migrate-graphql-bindings.test.ts +++ b/apps/local/src/server/migrate-graphql-bindings.test.ts @@ -13,11 +13,12 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator"; const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); -// Minimal pre-migration shape — only the graphql tables we care about, -// plus the drizzle bookkeeping `__drizzle_migrations` table that the -// runner uses to skip already-applied migrations. Stamping all -// migrations 0000..0006 as applied lets us run only 0007 against this -// hand-crafted DB. +// Minimal pre-migration shape — the graphql tables we care about, +// plus the openapi tables that 0008 (which also runs after our stamp) +// needs to touch, plus the drizzle bookkeeping `__drizzle_migrations` +// table that the runner uses to skip already-applied migrations. Both +// 0007 and 0008 will run sequentially against this DB; the test only +// asserts on the graphql side. const PRE_0007_SQL = ` CREATE TABLE __drizzle_migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -43,18 +44,43 @@ const PRE_0007_SQL = ` binding TEXT NOT NULL, PRIMARY KEY (scope_id, id) ); + + CREATE TABLE openapi_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + spec TEXT NOT NULL, + source_url TEXT, + base_url TEXT, + headers TEXT, + query_params TEXT, + oauth2 TEXT, + invocation_config TEXT NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE openapi_source_binding ( + id TEXT PRIMARY KEY NOT NULL, + source_id TEXT NOT NULL, + source_scope_id TEXT NOT NULL, + target_scope_id TEXT NOT NULL, + slot TEXT NOT NULL, + value TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); `; +// drizzle's sqlite migrator picks the latest `created_at` from +// __drizzle_migrations and skips any migration whose folderMillis (from +// the journal) is <= that timestamp. Stamping a row with 0006's +// folderMillis lets the runner skip 0000..0006 and only execute 0007. +const STAMP_BEFORE = 1777850000001; // 0006_neat_terror.when + const stampPriorMigrationsApplied = (db: Database) => { - // drizzle's migration runner reads this table and skips any hashes - // already present. Insert one fixed-shape row per prior migration so - // only 0007 actually runs. - const stmt = db.prepare( + db.prepare( "INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)", - ); - for (let i = 0; i <= 6; i += 1) { - stmt.run(`stub-${i.toString().padStart(4, "0")}`, Date.now()); - } + ).run("pre-0007-marker", STAMP_BEFORE); }; describe("0007_normalize_graphql backfill", () => { diff --git a/apps/local/src/server/migrate-openapi-bindings.test.ts b/apps/local/src/server/migrate-openapi-bindings.test.ts new file mode 100644 index 000000000..a44ee054b --- /dev/null +++ b/apps/local/src/server/migrate-openapi-bindings.test.ts @@ -0,0 +1,302 @@ +// End-to-end test for `0008_normalize_openapi.sql`. Seeds the +// pre-migration shape (json blobs on openapi_source.query_params, +// openapi_source.invocation_config.specFetchCredentials.*, and +// openapi_source_binding.value), runs the migration runner, asserts +// the new flat columns + child tables match. + +import { describe, expect, it } from "@effect/vitest"; +import { Database } from "bun:sqlite"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; + +const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); + +// Pre-0008 shape — only the openapi tables we touch, plus the drizzle +// bookkeeping table so the runner can stamp earlier migrations as +// applied and only run 0008. +const PRE_0008_SQL = ` + CREATE TABLE __drizzle_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT NOT NULL, + created_at NUMERIC + ); + + CREATE TABLE openapi_source ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + name TEXT NOT NULL, + spec TEXT NOT NULL, + source_url TEXT, + base_url TEXT, + headers TEXT, + query_params TEXT, + oauth2 TEXT, + invocation_config TEXT NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE openapi_operation ( + id TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_id TEXT NOT NULL, + binding TEXT NOT NULL, + PRIMARY KEY (scope_id, id) + ); + + CREATE TABLE openapi_source_binding ( + id TEXT PRIMARY KEY NOT NULL, + source_id TEXT NOT NULL, + source_scope_id TEXT NOT NULL, + target_scope_id TEXT NOT NULL, + slot TEXT NOT NULL, + value TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); +`; + +// Stamp 0007's folderMillis from the journal so drizzle's runner skips +// 0000..0007 and only executes 0008 against this hand-seeded DB. +const STAMP_BEFORE = 1778100000000; // 0007_normalize_graphql.when + +const stampPriorMigrationsApplied = (db: Database) => { + db.prepare( + "INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)", + ).run("pre-0008-marker", STAMP_BEFORE); +}; + +describe("0008_normalize_openapi backfill", () => { + it("flattens openapi_source_binding.value into kind/secret_id/connection_id/text_value", () => { + const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0008_SQL); + stampPriorMigrationsApplied(db); + + // Seed three bindings, one per kind. + const insert = db.prepare( + "INSERT INTO openapi_source_binding (id, source_id, source_scope_id, target_scope_id, slot, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + ); + const now = Date.now(); + insert.run( + "b1", + "src", + "default-scope", + "default-scope", + "header:authorization", + JSON.stringify({ kind: "secret", secretId: "tok-secret" }), + now, + now, + ); + insert.run( + "b2", + "src", + "default-scope", + "default-scope", + "oauth2:default:connection", + JSON.stringify({ kind: "connection", connectionId: "conn-1" }), + now, + now, + ); + insert.run( + "b3", + "src", + "default-scope", + "default-scope", + "header:x-static", + JSON.stringify({ kind: "text", text: "literal" }), + now, + now, + ); + + // Need the parent openapi_source row so the source_id FK ergonomics + // are satisfied for any cascading delete logic — though the binding + // table has no DB-level FK, code paths assume the parent exists. + db.prepare( + "INSERT INTO openapi_source (scope_id, id, name, spec, invocation_config) VALUES (?, ?, ?, ?, ?)", + ).run("default-scope", "src", "Source", "{}", "{}"); + + db.close(); + + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + const rows = after + .prepare( + "SELECT id, kind, secret_id, connection_id, text_value FROM openapi_source_binding ORDER BY id", + ) + .all() as ReadonlyArray<{ + id: string; + kind: string; + secret_id: string | null; + connection_id: string | null; + text_value: string | null; + }>; + expect(rows).toHaveLength(3); + expect(rows[0]).toMatchObject({ + id: "b1", + kind: "secret", + secret_id: "tok-secret", + connection_id: null, + text_value: null, + }); + expect(rows[1]).toMatchObject({ + id: "b2", + kind: "connection", + secret_id: null, + connection_id: "conn-1", + text_value: null, + }); + expect(rows[2]).toMatchObject({ + id: "b3", + kind: "text", + secret_id: null, + connection_id: null, + text_value: "literal", + }); + // value json column dropped. + const cols = after + .prepare("PRAGMA table_info('openapi_source_binding')") + .all() as ReadonlyArray<{ name: string }>; + expect(cols.some((c) => c.name === "value")).toBe(false); + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("explodes query_params and specFetchCredentials json into child rows", () => { + const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0008_SQL); + stampPriorMigrationsApplied(db); + + const queryParams = { + api_key: { secretId: "qp-secret" }, + flag: "true", + }; + const invocationConfig = { + specFetchCredentials: { + headers: { + Authorization: { secretId: "fetch-tok", prefix: "Bearer " }, + }, + queryParams: { token: { secretId: "fetch-qp" } }, + }, + }; + + db.prepare( + "INSERT INTO openapi_source (scope_id, id, name, spec, query_params, invocation_config) VALUES (?, ?, ?, ?, ?, ?)", + ).run( + "default-scope", + "src", + "Source", + "{}", + JSON.stringify(queryParams), + JSON.stringify(invocationConfig), + ); + + db.close(); + + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + + const qpRows = after + .prepare( + "SELECT name, kind, text_value, secret_id FROM openapi_source_query_param WHERE source_id = ? ORDER BY name", + ) + .all("src") as ReadonlyArray<{ + name: string; + kind: string; + text_value: string | null; + secret_id: string | null; + }>; + expect(qpRows).toHaveLength(2); + const byName = new Map(qpRows.map((r) => [r.name, r])); + expect(byName.get("api_key")).toMatchObject({ + kind: "secret", + secret_id: "qp-secret", + }); + expect(byName.get("flag")).toMatchObject({ + kind: "text", + text_value: "true", + }); + + const fetchHeaders = after + .prepare( + "SELECT name, kind, secret_id, secret_prefix FROM openapi_source_spec_fetch_header WHERE source_id = ?", + ) + .all("src") as ReadonlyArray<{ + name: string; + kind: string; + secret_id: string | null; + secret_prefix: string | null; + }>; + expect(fetchHeaders).toHaveLength(1); + expect(fetchHeaders[0]).toMatchObject({ + name: "Authorization", + kind: "secret", + secret_id: "fetch-tok", + secret_prefix: "Bearer ", + }); + + const fetchQp = after + .prepare( + "SELECT name, secret_id FROM openapi_source_spec_fetch_query_param WHERE source_id = ?", + ) + .all("src") as ReadonlyArray<{ name: string; secret_id: string }>; + expect(fetchQp).toHaveLength(1); + expect(fetchQp[0]).toMatchObject({ name: "token", secret_id: "fetch-qp" }); + + // Old json columns dropped. + const cols = after + .prepare("PRAGMA table_info('openapi_source')") + .all() as ReadonlyArray<{ name: string }>; + expect(cols.some((c) => c.name === "query_params")).toBe(false); + expect(cols.some((c) => c.name === "invocation_config")).toBe(false); + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("survives empty / missing json on bindings and sources", () => { + const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); + const dbPath = join(dir, "test.sqlite"); + try { + const db = new Database(dbPath); + db.exec(PRE_0008_SQL); + stampPriorMigrationsApplied(db); + + // Source with empty invocation_config and no query_params. + db.prepare( + "INSERT INTO openapi_source (scope_id, id, name, spec, invocation_config) VALUES (?, ?, ?, ?, ?)", + ).run("default-scope", "bare", "Bare", "{}", JSON.stringify({})); + + db.close(); + const drizzleDb = drizzle(new Database(dbPath)); + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); + + const after = new Database(dbPath, { readonly: true }); + const qpCount = ( + after + .prepare( + "SELECT count(*) as n FROM openapi_source_query_param WHERE source_id = ?", + ) + .get("bare") as { n: number } + ).n; + expect(qpCount).toBe(0); + after.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/plugins/openapi/src/sdk/plugin.test.ts b/packages/plugins/openapi/src/sdk/plugin.test.ts index 6b5e7ca2c..670605876 100644 --- a/packages/plugins/openapi/src/sdk/plugin.test.ts +++ b/packages/plugins/openapi/src/sdk/plugin.test.ts @@ -20,7 +20,7 @@ import { const TEST_SCOPE = "test-scope"; import { openApiPlugin } from "./plugin"; -import { OAuth2Auth } from "./types"; +import { OAuth2Auth, OpenApiSourceBindingInput } from "./types"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; @@ -847,4 +847,108 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { expect(tools.some((t) => t.id.startsWith("deferred."))).toBe(true); }), ); + + // ------------------------------------------------------------------------- + // Usage tracking — once openapi normalizes its bindings, query_params, + // and specFetchCredentials, `usagesForSecret` and `usagesForConnection` + // should surface every reference and `secrets.remove` should RESTRICT. + // ------------------------------------------------------------------------- + + it.effect("usagesForSecret aggregates bindings and child rows", () => + Effect.gen(function* () { + const executor = yield* createExecutor( + makeTestConfig({ + plugins: [memorySecretsPlugin(), openApiPlugin()] as const, + }), + ); + + yield* executor.secrets.set( + new SetSecretInput({ + id: SecretId.make("api-key"), + scope: ScopeId.make(TEST_SCOPE), + name: "API Key", + value: "abc123", + provider: "memory", + }), + ); + + // Add a source whose query_params reference the secret directly. + yield* executor.openapi.addSpec({ + spec: specJson, + scope: TEST_SCOPE, + namespace: "with_secret", + baseUrl: "http://example.com", + queryParams: { token: { secretId: "api-key" } }, + }); + + // Configure a slot binding pointing at the same secret. + yield* executor.openapi.setSourceBinding( + new OpenApiSourceBindingInput({ + sourceId: "with_secret", + sourceScope: ScopeId.make(TEST_SCOPE), + scope: ScopeId.make(TEST_SCOPE), + slot: "header:authorization", + value: { kind: "secret", secretId: SecretId.make("api-key") }, + }), + ); + + const usages = yield* executor.secrets.usages(SecretId.make("api-key")); + expect(usages.length).toBe(2); + const slots = usages.map((u) => u.slot).sort(); + expect(slots).toEqual([ + "binding:header:authorization", + "query_param:token", + ]); + expect(usages.every((u) => u.pluginId === "openapi")).toBe(true); + }), + ); + + it.effect("secrets.remove refuses while an openapi binding still uses it", () => + Effect.gen(function* () { + const executor = yield* createExecutor( + makeTestConfig({ + plugins: [memorySecretsPlugin(), openApiPlugin()] as const, + }), + ); + yield* executor.secrets.set( + new SetSecretInput({ + id: SecretId.make("locked"), + scope: ScopeId.make(TEST_SCOPE), + name: "Locked", + value: "v", + provider: "memory", + }), + ); + + yield* executor.openapi.addSpec({ + spec: specJson, + scope: TEST_SCOPE, + namespace: "ref", + baseUrl: "http://example.com", + }); + yield* executor.openapi.setSourceBinding( + new OpenApiSourceBindingInput({ + sourceId: "ref", + sourceScope: ScopeId.make(TEST_SCOPE), + scope: ScopeId.make(TEST_SCOPE), + slot: "header:authorization", + value: { kind: "secret", secretId: SecretId.make("locked") }, + }), + ); + + const failure = yield* executor.secrets + .remove(SecretId.make("locked")) + .pipe(Effect.flip); + expect((failure as { _tag: string })._tag).toBe("SecretInUseError"); + + // Detach the binding, then remove succeeds. + yield* executor.openapi.removeSourceBinding( + "ref", + ScopeId.make(TEST_SCOPE), + "header:authorization", + ScopeId.make(TEST_SCOPE), + ); + yield* executor.secrets.remove(SecretId.make("locked")); + }), + ); }); diff --git a/packages/plugins/openapi/src/sdk/plugin.ts b/packages/plugins/openapi/src/sdk/plugin.ts index dddab6c22..70d5130c8 100644 --- a/packages/plugins/openapi/src/sdk/plugin.ts +++ b/packages/plugins/openapi/src/sdk/plugin.ts @@ -10,6 +10,7 @@ import { ScopeId, SecretId, SourceDetectionResult, + Usage, definePlugin, resolveSecretBackedMap, type PluginCtx, @@ -1030,6 +1031,82 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { removeSource: ({ ctx, sourceId, scope }) => ctx.storage.removeSource(sourceId, scope), + // Aggregate usages across the four places openapi can hold a direct + // secret ref: + // - openapi_source_binding.secret_id (kind=secret slot bindings) + // - openapi_source_query_param + // - openapi_source_spec_fetch_header / spec_fetch_query_param + // Each is one indexed SELECT in the store; the merge plus a single + // source-name JOIN happens here. + usagesForSecret: ({ ctx, args }) => + Effect.gen(function* () { + const bindings = yield* ctx.storage.findBindingsBySecret(args.secretId); + const childRows = yield* ctx.storage.findChildRowsBySecret( + args.secretId, + ); + + const sourceKeys = new Set(); + for (const b of bindings) { + sourceKeys.add(`${b.sourceScopeId}:${b.sourceId}`); + } + for (const r of childRows) { + sourceKeys.add(`${r.scope_id}:${r.source_id}`); + } + const sources = yield* ctx.storage.lookupSourceNames([...sourceKeys]); + + const out: Usage[] = []; + for (const b of bindings) { + out.push( + new Usage({ + pluginId: "openapi", + scopeId: ScopeId.make(b.scopeId), + ownerKind: "openapi-source-binding", + ownerId: b.sourceId, + ownerName: + sources.get(`${b.sourceScopeId}:${b.sourceId}`) ?? null, + slot: `binding:${b.slot}`, + }), + ); + } + for (const r of childRows) { + out.push( + new Usage({ + pluginId: "openapi", + scopeId: ScopeId.make(r.scope_id), + ownerKind: `openapi-source-${r.kind.replace(/_/g, "-")}`, + ownerId: r.source_id, + ownerName: sources.get(`${r.scope_id}:${r.source_id}`) ?? null, + slot: `${r.kind}:${r.name}`, + }), + ); + } + return out; + }), + + usagesForConnection: ({ ctx, args }) => + Effect.gen(function* () { + const bindings = yield* ctx.storage.findBindingsByConnection( + args.connectionId, + ); + const sourceKeys = new Set(); + for (const b of bindings) { + sourceKeys.add(`${b.sourceScopeId}:${b.sourceId}`); + } + const sources = yield* ctx.storage.lookupSourceNames([...sourceKeys]); + return bindings.map( + (b) => + new Usage({ + pluginId: "openapi", + scopeId: ScopeId.make(b.scopeId), + ownerKind: "openapi-source-binding", + ownerId: b.sourceId, + ownerName: + sources.get(`${b.sourceScopeId}:${b.sourceId}`) ?? null, + slot: `binding:${b.slot}`, + }), + ); + }), + // Re-fetch the spec from its origin URL (captured at addSpec time) // and replay the same parse → extract → upsertSource → register // path used by addSpec. Sources without a stored URL surface a diff --git a/packages/plugins/openapi/src/sdk/store.ts b/packages/plugins/openapi/src/sdk/store.ts index 4231eb4d6..75407e2ef 100644 --- a/packages/plugins/openapi/src/sdk/store.ts +++ b/packages/plugins/openapi/src/sdk/store.ts @@ -1,8 +1,10 @@ import { Effect, Schema } from "effect"; import { + ConnectionId, defineSchema, ScopeId, + SecretId, StorageError, type StorageDeps, type StorageFailure, @@ -27,6 +29,15 @@ import { // - openapi_source_binding: credential bindings for shared sources // --------------------------------------------------------------------------- +// Each of the secret-backed child tables (`openapi_source_query_param`, +// `openapi_source_spec_fetch_header`, +// `openapi_source_spec_fetch_query_param`) shares the same column shape: +// id/scope_id/source_id/name plus a `kind` enum that discriminates a +// literal text value from a secret reference (with optional prefix). +// The fields are inlined per-table because `defineSchema`'s type +// narrowing relies on the literal types staying on the original +// declaration site. + export const openapiSchema = defineSchema({ openapi_source: { fields: { @@ -40,10 +51,14 @@ export const openapiSchema = defineSchema({ // is the address re-fetched on `refreshSource`. source_url: { type: "string", required: false }, base_url: { type: "string", required: false }, + // `headers` and `oauth2` stay JSON: these carry slot names, not + // direct secret/connection ids. The secrets/connections that + // actually power them live one level of indirection deeper, in + // `openapi_source_binding` rows keyed by slot — and those ARE + // normalized below. Headers and oauth2 are plugin-private + // structural data, not cross-cutting refs. headers: { type: "json", required: false }, - query_params: { type: "json", required: false }, oauth2: { type: "json", required: false }, - invocation_config: { type: "json", required: true }, }, }, openapi_operation: { @@ -66,11 +81,54 @@ export const openapiSchema = defineSchema({ // bindings when a shared source is removed. target_scope_id: { type: "string", required: true, index: true }, slot: { type: "string", required: true, index: true }, - value: { type: "json", required: true }, + // Discriminated union, flattened. Exactly one of secret_id / + // connection_id / text_value is populated based on `kind`. + // `secret_id` and `connection_id` are indexed so usages queries + // are one-hop SELECTs. + kind: { type: ["secret", "connection", "text"], required: true }, + secret_id: { type: "string", required: false, index: true }, + connection_id: { type: "string", required: false, index: true }, + text_value: { type: "string", required: false }, created_at: { type: "date", required: true }, updated_at: { type: "date", required: true }, }, }, + openapi_source_query_param: { + fields: { + id: { type: "string", required: true }, + scope_id: { type: "string", required: true, index: true }, + source_id: { type: "string", required: true, index: true }, + name: { type: "string", required: true }, + kind: { type: ["text", "secret"], required: true }, + text_value: { type: "string", required: false }, + secret_id: { type: "string", required: false, index: true }, + secret_prefix: { type: "string", required: false }, + }, + }, + openapi_source_spec_fetch_header: { + fields: { + id: { type: "string", required: true }, + scope_id: { type: "string", required: true, index: true }, + source_id: { type: "string", required: true, index: true }, + name: { type: "string", required: true }, + kind: { type: ["text", "secret"], required: true }, + text_value: { type: "string", required: false }, + secret_id: { type: "string", required: false, index: true }, + secret_prefix: { type: "string", required: false }, + }, + }, + openapi_source_spec_fetch_query_param: { + fields: { + id: { type: "string", required: true }, + scope_id: { type: "string", required: true, index: true }, + source_id: { type: "string", required: true, index: true }, + name: { type: "string", required: true }, + kind: { type: ["text", "secret"], required: true }, + text_value: { type: "string", required: false }, + secret_id: { type: "string", required: false, index: true }, + secret_prefix: { type: "string", required: false }, + }, + }, }); export type OpenapiSchema = typeof openapiSchema; @@ -166,16 +224,70 @@ const decodeBinding = Schema.decodeUnknownSync(OperationBinding); const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth); const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig); -const encodeSourceBindingValue = Schema.encodeSync(OpenApiSourceBindingValue); -const decodeSourceBindingValue = Schema.decodeUnknownSync( - OpenApiSourceBindingValue, -); -const asJsonObject = (value: unknown): Record => { - if (value == null) return {}; - if (typeof value === "string") - return JSON.parse(value) as Record; - return value as Record; +interface ChildRow { + readonly id: string; + readonly scope_id: string; + readonly source_id: string; + readonly name: string; + readonly kind: "text" | "secret"; + readonly text_value?: string; + readonly secret_id?: string; + readonly secret_prefix?: string; + // Index signature to satisfy adapter's `RowInput` shape (the typed + // adapter exposes its row shape with one). + readonly [k: string]: unknown; +} + +// Collapse a SecretBackedValue map into the flat child-table column +// shape used by openapi_source_query_param and the two +// openapi_source_spec_fetch_* tables. Returns one record per entry. +const valueMapToChildRows = ( + sourceId: string, + scope: string, + values: Record | undefined, +): readonly ChildRow[] => { + if (!values) return []; + return Object.entries(values).map(([name, value]) => { + const id = JSON.stringify([sourceId, name]); + if (typeof value === "string") { + return { + id, + scope_id: scope, + source_id: sourceId, + name, + kind: "text", + text_value: value, + }; + } + return { + id, + scope_id: scope, + source_id: sourceId, + name, + kind: "secret", + secret_id: value.secretId, + secret_prefix: value.prefix, + }; + }); +}; + +const childRowsToValueMap = ( + rows: readonly Record[], +): Record => { + const out: Record = {}; + for (const row of rows) { + const name = row.name as string; + if (row.kind === "secret" && typeof row.secret_id === "string") { + const prefix = row.secret_prefix as string | undefined | null; + out[name] = prefix + ? { secretId: row.secret_id, prefix } + : { secretId: row.secret_id }; + } else if (row.kind === "text" && typeof row.text_value === "string") { + out[name] = row.text_value; + } + } + return out; }; const toJsonRecord = (value: unknown): Record => @@ -367,6 +479,44 @@ export interface OpenapiStore { slot: string, scope: string, ) => Effect.Effect; + + // --------------------------------------------------------------------- + // Usage lookups — back `usagesForSecret` / `usagesForConnection`. + // Each is one indexed SELECT against the new normalized columns. + // --------------------------------------------------------------------- + + /** Source-binding rows that point at the given secret id. */ + readonly findBindingsBySecret: ( + secretId: string, + ) => Effect.Effect; + + /** Source-binding rows that point at the given connection id. */ + readonly findBindingsByConnection: ( + connectionId: string, + ) => Effect.Effect; + + /** Child rows from query_params / specFetch tables that reference the + * given secret id, tagged with the table they came from so the + * caller can produce a readable `slot` like + * `query_param:foo` or `spec_fetch_header:Authorization`. */ + readonly findChildRowsBySecret: (secretId: string) => Effect.Effect< + readonly { + readonly kind: + | "query_param" + | "spec_fetch_header" + | "spec_fetch_query_param"; + readonly source_id: string; + readonly scope_id: string; + readonly name: string; + }[], + StorageFailure + >; + + /** Resolve display names for one or more `(scope_id, source_id)` pairs + * in a single round trip, keyed by `${scope_id}:${source_id}`. */ + readonly lookupSourceNames: ( + keys: readonly string[], + ) => Effect.Effect, StorageFailure>; } // --------------------------------------------------------------------------- @@ -400,6 +550,25 @@ export const makeDefaultOpenapiStore = ({ encodeSyntheticRowIdPart(scopeId), ].join("::"); + const rowToSourceBindingValue = ( + row: Record, + ): OpenApiSourceBindingValue => { + const kind = row.kind as string; + if (kind === "secret" && typeof row.secret_id === "string") { + return { kind: "secret", secretId: SecretId.make(row.secret_id) }; + } + if (kind === "connection" && typeof row.connection_id === "string") { + return { + kind: "connection", + connectionId: ConnectionId.make(row.connection_id), + }; + } + // text fallback covers both well-formed text rows and any + // partial/null row that survived a malformed write — `text_value` + // defaults to "" so the type stays satisfied without a throw. + return { kind: "text", text: (row.text_value as string | null) ?? "" }; + }; + const rowToSourceBinding = ( row: Record, ): OpenApiSourceBindingRef => @@ -408,7 +577,7 @@ export const makeDefaultOpenapiStore = ({ sourceScopeId: ScopeId.make(row.source_scope_id as string), scopeId: ScopeId.make(row.target_scope_id as string), slot: row.slot as string, - value: decodeSourceBindingValue(asJsonObject(row.value)), + value: rowToSourceBindingValue(row), createdAt: row.created_at instanceof Date ? row.created_at @@ -419,6 +588,18 @@ export const makeDefaultOpenapiStore = ({ : new Date(row.updated_at as string), }); + const sourceBindingValueColumns = ( + value: OpenApiSourceBindingValue, + ): { kind: string; secret_id?: string; connection_id?: string; text_value?: string } => { + if (value.kind === "secret") { + return { kind: "secret", secret_id: value.secretId }; + } + if (value.kind === "connection") { + return { kind: "connection", connection_id: value.connectionId }; + } + return { kind: "text", text_value: value.text }; + }; + const validateBindingScopes = (params: { readonly sourceScope: string; readonly targetScope: string; @@ -485,39 +666,88 @@ export const makeDefaultOpenapiStore = ({ return source; }); - const rowToSource = (row: Record): StoredSource => { - const normalizedHeaders = normalizeStoredHeaders(row.headers); - const normalizedOAuth2 = normalizeStoredOAuth2(row.oauth2); - const invocationConfig = asJsonObject(row.invocation_config); - return { - namespace: row.id as string, - scope: row.scope_id as string, - name: row.name as string, - config: { - spec: row.spec as string, - sourceUrl: (row.source_url as string | null | undefined) ?? undefined, - baseUrl: (row.base_url as string | null | undefined) ?? undefined, - headers: normalizedHeaders.headers, - queryParams: decodeHeaders(row.query_params), - specFetchCredentials: invocationConfig.specFetchCredentials as - | OpenApiSpecFetchCredentials - | undefined, - oauth2: normalizedOAuth2.oauth2, - }, - legacy: - Object.keys(normalizedHeaders.legacy).length > 0 || - normalizedOAuth2.legacy - ? { - ...(Object.keys(normalizedHeaders.legacy).length > 0 - ? { headers: normalizedHeaders.legacy } + const loadChildValueMap = ( + model: + | "openapi_source_query_param" + | "openapi_source_spec_fetch_header" + | "openapi_source_spec_fetch_query_param", + sourceId: string, + scope: string, + ) => + adapter + .findMany({ + model, + where: [ + { field: "source_id", value: sourceId }, + { field: "scope_id", value: scope }, + ], + }) + .pipe(Effect.map(childRowsToValueMap)); + + const rowToSource = ( + row: Record, + ): Effect.Effect => + Effect.gen(function* () { + const sourceId = row.id as string; + const scope = row.scope_id as string; + const normalizedHeaders = normalizeStoredHeaders(row.headers); + const normalizedOAuth2 = normalizeStoredOAuth2(row.oauth2); + + const queryParams = yield* loadChildValueMap( + "openapi_source_query_param", + sourceId, + scope, + ); + const specFetchHeaders = yield* loadChildValueMap( + "openapi_source_spec_fetch_header", + sourceId, + scope, + ); + const specFetchQueryParams = yield* loadChildValueMap( + "openapi_source_spec_fetch_query_param", + sourceId, + scope, + ); + const specFetchCredentials: OpenApiSpecFetchCredentials | undefined = + Object.keys(specFetchHeaders).length === 0 && + Object.keys(specFetchQueryParams).length === 0 + ? undefined + : { + ...(Object.keys(specFetchHeaders).length > 0 + ? { headers: specFetchHeaders } : {}), - ...(normalizedOAuth2.legacy - ? { oauth2: normalizedOAuth2.legacy } + ...(Object.keys(specFetchQueryParams).length > 0 + ? { queryParams: specFetchQueryParams } : {}), - } - : undefined, - }; - }; + }; + + return { + namespace: sourceId, + scope, + name: row.name as string, + config: { + spec: row.spec as string, + sourceUrl: (row.source_url as string | null | undefined) ?? undefined, + baseUrl: (row.base_url as string | null | undefined) ?? undefined, + headers: normalizedHeaders.headers, + queryParams, + specFetchCredentials, + oauth2: normalizedOAuth2.oauth2, + }, + legacy: + Object.keys(normalizedHeaders.legacy).length > 0 || + normalizedOAuth2.legacy + ? { + ...(Object.keys(normalizedHeaders.legacy).length > 0 + ? { headers: normalizedHeaders.legacy } + : {}), + ...(normalizedOAuth2.legacy + ? { oauth2: normalizedOAuth2.legacy } + : {}), + } + : undefined, + }; + }); const rowToOperation = (row: Record): StoredOperation => ({ toolId: row.id as string, @@ -527,6 +757,35 @@ export const makeDefaultOpenapiStore = ({ ), }); + // Replace the rows of one child table for a source: delete then bulk + // insert. Single helper so upsertSource and updateSourceMeta both + // funnel through the same write path. + const replaceChildRows = ( + model: + | "openapi_source_query_param" + | "openapi_source_spec_fetch_header" + | "openapi_source_spec_fetch_query_param", + sourceId: string, + scope: string, + values: Record | undefined, + ) => + Effect.gen(function* () { + yield* adapter.deleteMany({ + model, + where: [ + { field: "source_id", value: sourceId }, + { field: "scope_id", value: scope }, + ], + }); + const rows = valueMapToChildRows(sourceId, scope, values); + if (rows.length === 0) return; + yield* adapter.createMany({ + model, + data: rows, + forceAllowId: true, + }); + }); + const deleteSource = ( namespace: string, scope: string, @@ -540,6 +799,20 @@ export const makeDefaultOpenapiStore = ({ { field: "scope_id", value: scope }, ], }); + // Drop every child table's rows for this source/scope. + for (const model of [ + "openapi_source_query_param", + "openapi_source_spec_fetch_header", + "openapi_source_spec_fetch_query_param", + ] as const) { + yield* adapter.deleteMany({ + model, + where: [ + { field: "source_id", value: namespace }, + { field: "scope_id", value: scope }, + ], + }); + } yield* adapter.delete({ model: "openapi_source", where: [ @@ -587,18 +860,30 @@ export const makeDefaultOpenapiStore = ({ ], ), ) as Record, - query_params: input.config.queryParams, oauth2: input.config.oauth2 ? toJsonRecord(encodeOAuth2SourceConfig(input.config.oauth2)) : undefined, - invocation_config: { - ...(input.config.specFetchCredentials - ? { specFetchCredentials: input.config.specFetchCredentials } - : {}), - }, }, forceAllowId: true, }); + yield* replaceChildRows( + "openapi_source_query_param", + input.namespace, + input.scope, + input.config.queryParams, + ); + yield* replaceChildRows( + "openapi_source_spec_fetch_header", + input.namespace, + input.scope, + input.config.specFetchCredentials?.headers, + ); + yield* replaceChildRows( + "openapi_source_spec_fetch_query_param", + input.namespace, + input.scope, + input.config.specFetchCredentials?.queryParams, + ); if (operations.length > 0) { yield* adapter.createMany({ model: "openapi_operation", @@ -623,7 +908,7 @@ export const makeDefaultOpenapiStore = ({ ], }); if (!existingRow) return; - const existing = rowToSource(existingRow); + const existing = yield* rowToSource(existingRow); const nextName = patch.name?.trim() || existing.name; const nextBaseUrl = @@ -632,10 +917,6 @@ export const makeDefaultOpenapiStore = ({ patch.headers !== undefined ? patch.headers : (existing.config.headers ?? {}); - const nextQueryParams = - patch.queryParams !== undefined - ? patch.queryParams - : (existing.config.queryParams ?? {}); const nextOAuth2 = patch.oauth2 !== undefined ? patch.oauth2 : existing.config.oauth2; @@ -660,30 +941,41 @@ export const makeDefaultOpenapiStore = ({ }, ]), ) as Record, - query_params: nextQueryParams, oauth2: nextOAuth2 ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2)) : undefined, - invocation_config: asJsonObject(existingRow.invocation_config), }, }); + if (patch.queryParams !== undefined) { + yield* replaceChildRows( + "openapi_source_query_param", + namespace, + scope, + patch.queryParams, + ); + } }), getSource: (namespace, scope) => - adapter - .findOne({ + Effect.gen(function* () { + const row = yield* adapter.findOne({ model: "openapi_source", where: [ { field: "id", value: namespace }, { field: "scope_id", value: scope }, ], - }) - .pipe(Effect.map((row) => (row ? rowToSource(row) : null))), + }); + if (!row) return null; + return yield* rowToSource(row); + }), listSources: () => - adapter - .findMany({ model: "openapi_source" }) - .pipe(Effect.map((rows) => rows.map(rowToSource))), + Effect.gen(function* () { + const rows = yield* adapter.findMany({ model: "openapi_source" }); + return yield* Effect.forEach(rows, rowToSource, { + concurrency: "unbounded", + }); + }), getOperationByToolId: (toolId, scope) => adapter @@ -779,6 +1071,7 @@ export const makeDefaultOpenapiStore = ({ input.scope as string, ); const now = new Date(); + const valueColumns = sourceBindingValueColumns(input.value); yield* adapter.delete({ model: "openapi_source_binding", where: [{ field: "id", value: id }], @@ -791,7 +1084,7 @@ export const makeDefaultOpenapiStore = ({ source_scope_id: input.sourceScope as string, target_scope_id: input.scope as string, slot: input.slot, - value: toJsonRecord(encodeSourceBindingValue(input.value)), + ...valueColumns, created_at: now, updated_at: now, }, @@ -825,5 +1118,64 @@ export const makeDefaultOpenapiStore = ({ ], }); }), + + findBindingsBySecret: (secretId) => + adapter + .findMany({ + model: "openapi_source_binding", + where: [{ field: "secret_id", value: secretId }], + }) + .pipe(Effect.map((rows) => rows.map(rowToSourceBinding))), + + findBindingsByConnection: (connectionId) => + adapter + .findMany({ + model: "openapi_source_binding", + where: [{ field: "connection_id", value: connectionId }], + }) + .pipe(Effect.map((rows) => rows.map(rowToSourceBinding))), + + findChildRowsBySecret: (secretId) => + Effect.gen(function* () { + const tables = [ + { model: "openapi_source_query_param" as const, kind: "query_param" as const }, + { model: "openapi_source_spec_fetch_header" as const, kind: "spec_fetch_header" as const }, + { model: "openapi_source_spec_fetch_query_param" as const, kind: "spec_fetch_query_param" as const }, + ]; + const perTable = yield* Effect.forEach( + tables, + (t) => + adapter + .findMany({ + model: t.model, + where: [{ field: "secret_id", value: secretId }], + }) + .pipe( + Effect.map((rows) => + rows.map((r) => ({ + kind: t.kind, + source_id: r.source_id as string, + scope_id: r.scope_id as string, + name: r.name as string, + })), + ), + ), + { concurrency: "unbounded" }, + ); + return perTable.flat(); + }), + + lookupSourceNames: (keys) => + Effect.gen(function* () { + if (keys.length === 0) return new Map(); + const rows = yield* adapter.findMany({ model: "openapi_source" }); + const requested = new Set(keys); + const out = new Map(); + for (const r of rows) { + const key = `${r.scope_id as string}:${r.id as string}`; + if (requested.has(key)) out.set(key, r.name as string); + } + return out; + }), }; };