* Add secret/connection usages SDK surface + UI placeholder Plugins gain optional `usagesForSecret` / `usagesForConnection` callbacks that the executor fans out across via `executor.secrets.usages(id)` and `executor.connections.usages(id)`. Secret/connection removal is now RESTRICT — refuses with `SecretInUseError` / `ConnectionInUseError` when any plugin reports the id as in use, surfacing the count to the caller. API gains GET `/secrets/:id/usages` and `/connections/:id/usages` endpoints. React atoms and a small "Used by …" footer on each row in the Secrets and Connections tabs round out the surface. No plugin implements `usagesForSecret`/`usagesForConnection` yet, so the footer renders nothing and remove still succeeds for everything. Plugin migration lands in follow-up commits. * Normalize graphql plugin secret/connection refs Move secret/connection references out of the JSON columns on graphql_source (auth, headers, query_params) into: - two new flat columns auth_kind + auth_connection_id (indexed) - two child tables graphql_source_header / graphql_source_query_param, each with kind/text_value/secret_id/secret_prefix and an indexed secret_id This lets usagesForSecret and usagesForConnection answer "where is X referenced?" with a single indexed SELECT instead of scanning JSON, and secrets.remove / connections.remove now correctly RESTRICT when a graphql source still references the id. Migration 0007 backfills existing JSON via json_each + json_extract, then drops the old columns. Three tests cover the backfill against a hand-seeded pre-migration DB; three plugin-level tests cover usage fan-out and remove RESTRICT. * Normalize openapi plugin secret/connection refs 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. * Normalize mcp plugin secret/connection refs Move secret/connection references out of the JSON `mcp_source.config` column into: - Flat columns on mcp_source: auth_kind, auth_header_name, auth_secret_id, auth_secret_prefix, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id (each indexed where it carries a ref). Replaces the McpConnectionAuth json blob. - mcp_source_header / mcp_source_query_param: child tables for the remote source's SecretBackedMap entries (same shape as graphql / openapi child tables). The remaining structural fields (transport, endpoint, command, args, etc.) stay as JSON in `config` because they're plugin-private and vary by transport. mcp_binding.binding stays JSON too — McpToolBinding carries no refs and inputSchema/outputSchema are arbitrary user JSON. Plugin gains usagesForSecret / usagesForConnection. Migration 0009 backfills via json_extract / json_each, then strips the extracted fields with json_remove. Three migration tests cover backfill from a hand-seeded pre-migration DB; two plugin-level tests cover usage fan-out across header-auth and oauth2 sources. * Normalize google-discovery plugin secret/connection refs Move secret/connection references out of the JSON google_discovery_source.config column into: - Flat columns on google_discovery_source: auth_kind, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id, auth_scopes (each indexed where it carries a ref). Replaces the GoogleDiscoveryAuth json. - google_discovery_source_credential_header / google_discovery_source_credential_query_param: child tables for the optional GoogleDiscoveryFetchCredentials SecretBackedMap entries. The remaining structural fields (discoveryUrl, service, version, rootUrl, servicePath) stay in `config` as JSON — they're plugin-private and never carry refs. google_discovery_binding stays JSON too: the GoogleDiscoveryMethodBinding has no refs, and method input/output schemas are arbitrary user data. Plugin gains usagesForSecret / usagesForConnection. Migration 0010 backfills via json_extract / json_each, then strips the extracted fields with json_remove. Three migration tests cover backfill from a hand-seeded pre-migration DB; one plugin-level test covers usage fan-out across auth_*_secret_id and auth_connection_id. * Consolidate plugin normalization into single migration Merge graphql/openapi/mcp/google-discovery normalization into one migration per app: 0007_normalize_plugin_secret_refs (local sqlite) and 0008_normalize_plugin_secret_refs (cloud pg, mcp-only since cloud doesn't ship google-discovery). Each test now imports a shared PRE_0007_SQL helper that seeds all four plugins' parent tables at the pre-migration shape — needed because the single consolidated migration touches every plugin in one transaction. * Address review: legacy openapi OAuth + strict usage gate + shadow-aware delete Three issues from review: 1. migrateOpenApi was early-returning when invocation_config column was missing (post-0007). Rows with legacy OAuth in oauth2 column silently lost their migration to a Connection. Fixed: - migrate-connections now reads oauth2 column even without invocation_config, and adapts its UPDATE statement accordingly. - Both 0007 (sqlite) and 0008 (pg) migrations COALESCE invocation_config.oauth2 into the oauth2 column before dropping invocation_config, so legacy data isn't lost mid-migration. 2. secrets.remove / connections.remove gated on a lenient usage helper that swallowed plugin failures into empty arrays — a transient plugin error could silently allow a delete that creates dangling refs. Added secretsUsagesStrict / connectionsUsagesStrict that propagate plugin failures as StorageFailure and wired them into the delete RESTRICT path. Display path keeps the lenient variant. 3. The RESTRICT check was global by id, conflicting with the documented shadowing model: deleting a user-scope override should reveal the outer fallback, but the global check blocked any delete whose id is referenced anywhere. Now scoped: only enforce RESTRICT when the delete would leave no row with that id (i.e., the reference would actually dangle).
Unified · Split
| @@ -0,0 +1,388 @@ | |||
| 1 | -- Normalize all plugin secret/connection refs out of JSON columns | ||
| 2 | -- into proper relational shape: graphql, openapi, mcp. | ||
| 3 | -- pg port of apps/local/drizzle/0007_normalize_plugin_secret_refs.sql. | ||
| 4 | -- (google-discovery is local-only — not in cloud's plugin list.) | ||
| 5 | |||
| 6 | -- ============================================================ | ||
| 7 | -- graphql | ||
| 8 | -- ============================================================ | ||
| 9 | |||
| 10 | -- Normalize graphql plugin: move secret/connection refs out of JSON | ||
| 11 | -- columns into proper relational shape so usagesForSecret / | ||
| 12 | -- usagesForConnection are one indexed SELECT instead of a JSON scan. | ||
| 13 | -- pg port of apps/local/drizzle/0007_normalize_graphql.sql. | ||
| 14 | |||
| 15 | CREATE TABLE "graphql_source_header" ( | ||
| 16 | "id" text NOT NULL, | ||
| 17 | "scope_id" text NOT NULL, | ||
| 18 | "source_id" text NOT NULL, | ||
| 19 | "name" text NOT NULL, | ||
| 20 | "kind" text NOT NULL, | ||
| 21 | "text_value" text, | ||
| 22 | "secret_id" text, | ||
| 23 | "secret_prefix" text, | ||
| 24 | CONSTRAINT "graphql_source_header_scope_id_id_pk" PRIMARY KEY("scope_id","id") | ||
| 25 | ); | ||
| 26 | --> statement-breakpoint | ||
| 27 | CREATE INDEX "graphql_source_header_scope_id_idx" ON "graphql_source_header" USING btree ("scope_id");--> statement-breakpoint | ||
| 28 | CREATE INDEX "graphql_source_header_source_id_idx" ON "graphql_source_header" USING btree ("source_id");--> statement-breakpoint | ||
| 29 | CREATE INDEX "graphql_source_header_secret_id_idx" ON "graphql_source_header" USING btree ("secret_id");--> statement-breakpoint | ||
| 30 | |||
| 31 | CREATE TABLE "graphql_source_query_param" ( | ||
| 32 | "id" text NOT NULL, | ||
| 33 | "scope_id" text NOT NULL, | ||
| 34 | "source_id" text NOT NULL, | ||
| 35 | "name" text NOT NULL, | ||
| 36 | "kind" text NOT NULL, | ||
| 37 | "text_value" text, | ||
| 38 | "secret_id" text, | ||
| 39 | "secret_prefix" text, | ||
| 40 | CONSTRAINT "graphql_source_query_param_scope_id_id_pk" PRIMARY KEY("scope_id","id") | ||
| 41 | ); | ||
| 42 | --> statement-breakpoint | ||
| 43 | CREATE INDEX "graphql_source_query_param_scope_id_idx" ON "graphql_source_query_param" USING btree ("scope_id");--> statement-breakpoint | ||
| 44 | CREATE INDEX "graphql_source_query_param_source_id_idx" ON "graphql_source_query_param" USING btree ("source_id");--> statement-breakpoint | ||
| 45 | CREATE INDEX "graphql_source_query_param_secret_id_idx" ON "graphql_source_query_param" USING btree ("secret_id");--> statement-breakpoint | ||
| 46 | |||
| 47 | -- New auth columns. `auth_kind` defaults to "none" so existing rows that | ||
| 48 | -- predate this migration are valid even if the json was null. | ||
| 49 | ALTER TABLE "graphql_source" ADD COLUMN "auth_kind" text DEFAULT 'none' NOT NULL;--> statement-breakpoint | ||
| 50 | ALTER TABLE "graphql_source" ADD COLUMN "auth_connection_id" text;--> statement-breakpoint | ||
| 51 | CREATE INDEX "graphql_source_auth_connection_id_idx" ON "graphql_source" USING btree ("auth_connection_id");--> statement-breakpoint | ||
| 52 | |||
| 53 | -- Backfill auth from the JSON column. Missing keys yield NULL, so a row | ||
| 54 | -- with auth=NULL or kind="none" leaves auth_connection_id NULL and | ||
| 55 | -- auth_kind defaulted to "none". | ||
| 56 | UPDATE "graphql_source" | ||
| 57 | SET | ||
| 58 | "auth_kind" = COALESCE("auth"->>'kind', 'none'), | ||
| 59 | "auth_connection_id" = "auth"->>'connectionId' | ||
| 60 | WHERE "auth" IS NOT NULL;--> statement-breakpoint | ||
| 61 | |||
| 62 | -- Backfill headers. For each (source, header_name) pair: if the value | ||
| 63 | -- is a json object with .secretId, write a kind=secret row; otherwise | ||
| 64 | -- write a kind=text row with the literal string. jsonb_each iterates | ||
| 65 | -- the keys of the headers object. | ||
| 66 | INSERT INTO "graphql_source_header" | ||
| 67 | ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") | ||
| 68 | SELECT | ||
| 69 | s."scope_id", | ||
| 70 | '[' || to_jsonb(s."id")::text || ',' || to_jsonb(h.key)::text || ']', | ||
| 71 | s."id", | ||
| 72 | h.key, | ||
| 73 | CASE | ||
| 74 | WHEN jsonb_typeof(h.value) = 'object' AND h.value ? 'secretId' THEN 'secret' | ||
| 75 | ELSE 'text' | ||
| 76 | END, | ||
| 77 | CASE WHEN jsonb_typeof(h.value) = 'string' THEN h.value #>> '{}' ELSE NULL END, | ||
| 78 | CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'secretId' ELSE NULL END, | ||
| 79 | CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'prefix' ELSE NULL END | ||
| 80 | FROM "graphql_source" s, jsonb_each(s."headers") h | ||
| 81 | WHERE s."headers" IS NOT NULL | ||
| 82 | ON CONFLICT DO NOTHING;--> statement-breakpoint | ||
| 83 | |||
| 84 | -- Same for query_params. | ||
| 85 | INSERT INTO "graphql_source_query_param" | ||
| 86 | ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") | ||
| 87 | SELECT | ||
| 88 | s."scope_id", | ||
| 89 | '[' || to_jsonb(s."id")::text || ',' || to_jsonb(q.key)::text || ']', | ||
| 90 | s."id", | ||
| 91 | q.key, | ||
| 92 | CASE | ||
| 93 | WHEN jsonb_typeof(q.value) = 'object' AND q.value ? 'secretId' THEN 'secret' | ||
| 94 | ELSE 'text' | ||
| 95 | END, | ||
| 96 | CASE WHEN jsonb_typeof(q.value) = 'string' THEN q.value #>> '{}' ELSE NULL END, | ||
| 97 | CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'secretId' ELSE NULL END, | ||
| 98 | CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'prefix' ELSE NULL END | ||
| 99 | FROM "graphql_source" s, jsonb_each(s."query_params") q | ||
| 100 | WHERE s."query_params" IS NOT NULL | ||
| 101 | ON CONFLICT DO NOTHING;--> statement-breakpoint | ||
| 102 | |||
| 103 | ALTER TABLE "graphql_source" DROP COLUMN "headers";--> statement-breakpoint | ||
| 104 | ALTER TABLE "graphql_source" DROP COLUMN "query_params";--> statement-breakpoint | ||
| 105 | ALTER TABLE "graphql_source" DROP COLUMN "auth"; | ||
| 106 | |||
| 107 | --> statement-breakpoint | ||
| 108 | |||
| 109 | -- ============================================================ | ||
| 110 | -- openapi | ||
| 111 | -- ============================================================ | ||
| 112 | |||
| 113 | -- Normalize openapi plugin: move every direct secret/connection ref out | ||
| 114 | -- of JSON columns into proper relational shape. pg port of | ||
| 115 | -- apps/local/drizzle/0008_normalize_openapi.sql. | ||
| 116 | |||
| 117 | CREATE TABLE "openapi_source_query_param" ( | ||
| 118 | "id" text NOT NULL, | ||
| 119 | "scope_id" text NOT NULL, | ||
| 120 | "source_id" text NOT NULL, | ||
| 121 | "name" text NOT NULL, | ||
| 122 | "kind" text NOT NULL, | ||
| 123 | "text_value" text, | ||
| 124 | "secret_id" text, | ||
| 125 | "secret_prefix" text, | ||
| 126 | CONSTRAINT "openapi_source_query_param_scope_id_id_pk" PRIMARY KEY("scope_id","id") | ||
| 127 | ); | ||
| 128 | --> statement-breakpoint | ||
| 129 | CREATE INDEX "openapi_source_query_param_scope_id_idx" ON "openapi_source_query_param" USING btree ("scope_id");--> statement-breakpoint | ||
| 130 | CREATE INDEX "openapi_source_query_param_source_id_idx" ON "openapi_source_query_param" USING btree ("source_id");--> statement-breakpoint | ||
| 131 | CREATE INDEX "openapi_source_query_param_secret_id_idx" ON "openapi_source_query_param" USING btree ("secret_id");--> statement-breakpoint | ||
| 132 | |||
| 133 | CREATE TABLE "openapi_source_spec_fetch_header" ( | ||
| 134 | "id" text NOT NULL, | ||
| 135 | "scope_id" text NOT NULL, | ||
| 136 | "source_id" text NOT NULL, | ||
| 137 | "name" text NOT NULL, | ||
| 138 | "kind" text NOT NULL, | ||
| 139 | "text_value" text, | ||
| 140 | "secret_id" text, | ||
| 141 | "secret_prefix" text, | ||
| 142 | CONSTRAINT "openapi_source_spec_fetch_header_scope_id_id_pk" PRIMARY KEY("scope_id","id") | ||
| 143 | ); | ||
| 144 | --> statement-breakpoint | ||
| 145 | CREATE INDEX "openapi_source_spec_fetch_header_scope_id_idx" ON "openapi_source_spec_fetch_header" USING btree ("scope_id");--> statement-breakpoint | ||
| 146 | CREATE INDEX "openapi_source_spec_fetch_header_source_id_idx" ON "openapi_source_spec_fetch_header" USING btree ("source_id");--> statement-breakpoint | ||
| 147 | CREATE INDEX "openapi_source_spec_fetch_header_secret_id_idx" ON "openapi_source_spec_fetch_header" USING btree ("secret_id");--> statement-breakpoint | ||
| 148 | |||
| 149 | CREATE TABLE "openapi_source_spec_fetch_query_param" ( | ||
| 150 | "id" text NOT NULL, | ||
| 151 | "scope_id" text NOT NULL, | ||
| 152 | "source_id" text NOT NULL, | ||
| 153 | "name" text NOT NULL, | ||
| 154 | "kind" text NOT NULL, | ||
| 155 | "text_value" text, | ||
| 156 | "secret_id" text, | ||
| 157 | "secret_prefix" text, | ||
| 158 | CONSTRAINT "openapi_source_spec_fetch_query_param_scope_id_id_pk" PRIMARY KEY("scope_id","id") | ||
| 159 | ); | ||
| 160 | --> statement-breakpoint | ||
| 161 | CREATE INDEX "openapi_source_spec_fetch_query_param_scope_id_idx" ON "openapi_source_spec_fetch_query_param" USING btree ("scope_id");--> statement-breakpoint | ||
| 162 | CREATE INDEX "openapi_source_spec_fetch_query_param_source_id_idx" ON "openapi_source_spec_fetch_query_param" USING btree ("source_id");--> statement-breakpoint | ||
| 163 | CREATE INDEX "openapi_source_spec_fetch_query_param_secret_id_idx" ON "openapi_source_spec_fetch_query_param" USING btree ("secret_id");--> statement-breakpoint | ||
| 164 | |||
| 165 | -- New columns on openapi_source_binding to flatten the value json. | ||
| 166 | -- `kind` defaults to 'text' so the ALTER works on existing rows; the | ||
| 167 | -- backfill below stamps the real value. | ||
| 168 | ALTER TABLE "openapi_source_binding" ADD COLUMN "kind" text DEFAULT 'text' NOT NULL;--> statement-breakpoint | ||
| 169 | ALTER TABLE "openapi_source_binding" ADD COLUMN "secret_id" text;--> statement-breakpoint | ||
| 170 | ALTER TABLE "openapi_source_binding" ADD COLUMN "connection_id" text;--> statement-breakpoint | ||
| 171 | ALTER TABLE "openapi_source_binding" ADD COLUMN "text_value" text;--> statement-breakpoint | ||
| 172 | CREATE INDEX "openapi_source_binding_secret_id_idx" ON "openapi_source_binding" USING btree ("secret_id");--> statement-breakpoint | ||
| 173 | CREATE INDEX "openapi_source_binding_connection_id_idx" ON "openapi_source_binding" USING btree ("connection_id");--> statement-breakpoint | ||
| 174 | |||
| 175 | UPDATE "openapi_source_binding" | ||
| 176 | SET | ||
| 177 | "kind" = COALESCE("value"->>'kind', 'text'), | ||
| 178 | "secret_id" = CASE WHEN "value"->>'kind' = 'secret' THEN "value"->>'secretId' ELSE NULL END, | ||
| 179 | "connection_id" = CASE WHEN "value"->>'kind' = 'connection' THEN "value"->>'connectionId' ELSE NULL END, | ||
| 180 | "text_value" = CASE WHEN "value"->>'kind' = 'text' THEN "value"->>'text' ELSE NULL END | ||
| 181 | WHERE "value" IS NOT NULL;--> statement-breakpoint | ||
| 182 | |||
| 183 | INSERT INTO "openapi_source_query_param" | ||
| 184 | ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") | ||
| 185 | SELECT | ||
| 186 | s."scope_id", | ||
| 187 | '[' || to_jsonb(s."id")::text || ',' || to_jsonb(q.key)::text || ']', | ||
| 188 | s."id", | ||
| 189 | q.key, | ||
| 190 | CASE | ||
| 191 | WHEN jsonb_typeof(q.value) = 'object' AND q.value ? 'secretId' THEN 'secret' | ||
| 192 | ELSE 'text' | ||
| 193 | END, | ||
| 194 | CASE WHEN jsonb_typeof(q.value) = 'string' THEN q.value #>> '{}' ELSE NULL END, | ||
| 195 | CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'secretId' ELSE NULL END, | ||
| 196 | CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'prefix' ELSE NULL END | ||
| 197 | FROM "openapi_source" s, jsonb_each(s."query_params") q | ||
| 198 | WHERE s."query_params" IS NOT NULL | ||
| 199 | ON CONFLICT DO NOTHING;--> statement-breakpoint | ||
| 200 | |||
| 201 | INSERT INTO "openapi_source_spec_fetch_header" | ||
| 202 | ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") | ||
| 203 | SELECT | ||
| 204 | s."scope_id", | ||
| 205 | '[' || to_jsonb(s."id")::text || ',' || to_jsonb(h.key)::text || ']', | ||
| 206 | s."id", | ||
| 207 | h.key, | ||
| 208 | CASE | ||
| 209 | WHEN jsonb_typeof(h.value) = 'object' AND h.value ? 'secretId' THEN 'secret' | ||
| 210 | ELSE 'text' | ||
| 211 | END, | ||
| 212 | CASE WHEN jsonb_typeof(h.value) = 'string' THEN h.value #>> '{}' ELSE NULL END, | ||
| 213 | CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'secretId' ELSE NULL END, | ||
| 214 | CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'prefix' ELSE NULL END | ||
| 215 | FROM "openapi_source" s, jsonb_each(s."invocation_config"->'specFetchCredentials'->'headers') h | ||
| 216 | WHERE s."invocation_config"->'specFetchCredentials'->'headers' IS NOT NULL | ||
| 217 | ON CONFLICT DO NOTHING;--> statement-breakpoint | ||
| 218 | |||
| 219 | INSERT INTO "openapi_source_spec_fetch_query_param" | ||
| 220 | ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") | ||
| 221 | SELECT | ||
| 222 | s."scope_id", | ||
| 223 | '[' || to_jsonb(s."id")::text || ',' || to_jsonb(q.key)::text || ']', | ||
| 224 | s."id", | ||
| 225 | q.key, | ||
| 226 | CASE | ||
| 227 | WHEN jsonb_typeof(q.value) = 'object' AND q.value ? 'secretId' THEN 'secret' | ||
| 228 | ELSE 'text' | ||
| 229 | END, | ||
| 230 | CASE WHEN jsonb_typeof(q.value) = 'string' THEN q.value #>> '{}' ELSE NULL END, | ||
| 231 | CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'secretId' ELSE NULL END, | ||
| 232 | CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'prefix' ELSE NULL END | ||
| 233 | FROM "openapi_source" s, jsonb_each(s."invocation_config"->'specFetchCredentials'->'queryParams') q | ||
| 234 | WHERE s."invocation_config"->'specFetchCredentials'->'queryParams' IS NOT NULL | ||
| 235 | ON CONFLICT DO NOTHING;--> statement-breakpoint | ||
| 236 | |||
| 237 | -- Preserve any legacy OAuth payload from invocation_config.oauth2 into | ||
| 238 | -- the still-existing oauth2 column before we drop invocation_config. | ||
| 239 | -- migrateLegacyConnections runs after drizzle migrations and reads | ||
| 240 | -- oauth2 to detect the legacy shape; without this, rows that only had | ||
| 241 | -- their OAuth payload under invocation_config.oauth2 would lose it. | ||
| 242 | UPDATE "openapi_source" | ||
| 243 | SET "oauth2" = "invocation_config"->'oauth2' | ||
| 244 | WHERE "oauth2" IS NULL | ||
| 245 | AND "invocation_config"->'oauth2' IS NOT NULL;--> statement-breakpoint | ||
| 246 | |||
| 247 | ALTER TABLE "openapi_source_binding" DROP COLUMN "value";--> statement-breakpoint | ||
| 248 | ALTER TABLE "openapi_source" DROP COLUMN "query_params";--> statement-breakpoint | ||
| 249 | ALTER TABLE "openapi_source" DROP COLUMN "invocation_config"; | ||
| 250 | |||
| 251 | --> statement-breakpoint | ||
| 252 | |||
| 253 | -- ============================================================ | ||
| 254 | -- mcp | ||
| 255 | -- ============================================================ | ||
| 256 | |||
| 257 | -- Normalize mcp plugin: lift the McpConnectionAuth secret/connection | ||
| 258 | -- refs and the SecretBackedMap headers/query_params out of | ||
| 259 | -- mcp_source.config JSON into proper columns / child tables. pg port | ||
| 260 | -- of apps/local/drizzle/0009_normalize_mcp.sql. | ||
| 261 | |||
| 262 | CREATE TABLE "mcp_source_header" ( | ||
| 263 | "id" text NOT NULL, | ||
| 264 | "scope_id" text NOT NULL, | ||
| 265 | "source_id" text NOT NULL, | ||
| 266 | "name" text NOT NULL, | ||
| 267 | "kind" text NOT NULL, | ||
| 268 | "text_value" text, | ||
| 269 | "secret_id" text, | ||
| 270 | "secret_prefix" text, | ||
| 271 | CONSTRAINT "mcp_source_header_scope_id_id_pk" PRIMARY KEY("scope_id","id") | ||
| 272 | ); | ||
| 273 | --> statement-breakpoint | ||
| 274 | CREATE INDEX "mcp_source_header_scope_id_idx" ON "mcp_source_header" USING btree ("scope_id");--> statement-breakpoint | ||
| 275 | CREATE INDEX "mcp_source_header_source_id_idx" ON "mcp_source_header" USING btree ("source_id");--> statement-breakpoint | ||
| 276 | CREATE INDEX "mcp_source_header_secret_id_idx" ON "mcp_source_header" USING btree ("secret_id");--> statement-breakpoint | ||
| 277 | |||
| 278 | CREATE TABLE "mcp_source_query_param" ( | ||
| 279 | "id" text NOT NULL, | ||
| 280 | "scope_id" text NOT NULL, | ||
| 281 | "source_id" text NOT NULL, | ||
| 282 | "name" text NOT NULL, | ||
| 283 | "kind" text NOT NULL, | ||
| 284 | "text_value" text, | ||
| 285 | "secret_id" text, | ||
| 286 | "secret_prefix" text, | ||
| 287 | CONSTRAINT "mcp_source_query_param_scope_id_id_pk" PRIMARY KEY("scope_id","id") | ||
| 288 | ); | ||
| 289 | --> statement-breakpoint | ||
| 290 | CREATE INDEX "mcp_source_query_param_scope_id_idx" ON "mcp_source_query_param" USING btree ("scope_id");--> statement-breakpoint | ||
| 291 | CREATE INDEX "mcp_source_query_param_source_id_idx" ON "mcp_source_query_param" USING btree ("source_id");--> statement-breakpoint | ||
| 292 | CREATE INDEX "mcp_source_query_param_secret_id_idx" ON "mcp_source_query_param" USING btree ("secret_id");--> statement-breakpoint | ||
| 293 | |||
| 294 | ALTER TABLE "mcp_source" ADD COLUMN "auth_kind" text DEFAULT 'none' NOT NULL;--> statement-breakpoint | ||
| 295 | ALTER TABLE "mcp_source" ADD COLUMN "auth_header_name" text;--> statement-breakpoint | ||
| 296 | ALTER TABLE "mcp_source" ADD COLUMN "auth_secret_id" text;--> statement-breakpoint | ||
| 297 | ALTER TABLE "mcp_source" ADD COLUMN "auth_secret_prefix" text;--> statement-breakpoint | ||
| 298 | ALTER TABLE "mcp_source" ADD COLUMN "auth_connection_id" text;--> statement-breakpoint | ||
| 299 | ALTER TABLE "mcp_source" ADD COLUMN "auth_client_id_secret_id" text;--> statement-breakpoint | ||
| 300 | ALTER TABLE "mcp_source" ADD COLUMN "auth_client_secret_secret_id" text;--> statement-breakpoint | ||
| 301 | CREATE INDEX "mcp_source_auth_secret_id_idx" ON "mcp_source" USING btree ("auth_secret_id");--> statement-breakpoint | ||
| 302 | CREATE INDEX "mcp_source_auth_connection_id_idx" ON "mcp_source" USING btree ("auth_connection_id");--> statement-breakpoint | ||
| 303 | CREATE INDEX "mcp_source_auth_client_id_secret_id_idx" ON "mcp_source" USING btree ("auth_client_id_secret_id");--> statement-breakpoint | ||
| 304 | CREATE INDEX "mcp_source_auth_client_secret_secret_id_idx" ON "mcp_source" USING btree ("auth_client_secret_secret_id");--> statement-breakpoint | ||
| 305 | |||
| 306 | -- Only update rows with explicitly current-shape auth (kind=header w/ | ||
| 307 | -- secretId, or kind=oauth2 w/ connectionId). Legacy inline-OAuth rows | ||
| 308 | -- are left untouched so the post-migrate migrateLegacyConnections | ||
| 309 | -- script can convert them to a Connection. | ||
| 310 | UPDATE "mcp_source" | ||
| 311 | SET | ||
| 312 | "auth_kind" = "config"#>>'{auth,kind}', | ||
| 313 | "auth_header_name" = "config"#>>'{auth,headerName}', | ||
| 314 | "auth_secret_id" = "config"#>>'{auth,secretId}', | ||
| 315 | "auth_secret_prefix" = "config"#>>'{auth,prefix}', | ||
| 316 | "auth_connection_id" = "config"#>>'{auth,connectionId}', | ||
| 317 | "auth_client_id_secret_id" = "config"#>>'{auth,clientIdSecretId}', | ||
| 318 | "auth_client_secret_secret_id" = "config"#>>'{auth,clientSecretSecretId}' | ||
| 319 | WHERE "config" IS NOT NULL | ||
| 320 | AND ( | ||
| 321 | ( | ||
| 322 | "config"#>>'{auth,kind}' = 'header' | ||
| 323 | AND "config"#>>'{auth,secretId}' IS NOT NULL | ||
| 324 | ) | ||
| 325 | OR ( | ||
| 326 | "config"#>>'{auth,kind}' = 'oauth2' | ||
| 327 | AND "config"#>>'{auth,connectionId}' IS NOT NULL | ||
| 328 | ) | ||
| 329 | );--> statement-breakpoint | ||
| 330 | |||
| 331 | INSERT INTO "mcp_source_header" | ||
| 332 | ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") | ||
| 333 | SELECT | ||
| 334 | s."scope_id", | ||
| 335 | '[' || to_jsonb(s."id")::text || ',' || to_jsonb(h.key)::text || ']', | ||
| 336 | s."id", | ||
| 337 | h.key, | ||
| 338 | CASE | ||
| 339 | WHEN jsonb_typeof(h.value) = 'object' AND h.value ? 'secretId' THEN 'secret' | ||
| 340 | ELSE 'text' | ||
| 341 | END, | ||
| 342 | CASE WHEN jsonb_typeof(h.value) = 'string' THEN h.value #>> '{}' ELSE NULL END, | ||
| 343 | CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'secretId' ELSE NULL END, | ||
| 344 | CASE WHEN jsonb_typeof(h.value) = 'object' THEN h.value->>'prefix' ELSE NULL END | ||
| 345 | FROM "mcp_source" s, jsonb_each(s."config"->'headers') h | ||
| 346 | WHERE s."config"->'headers' IS NOT NULL | ||
| 347 | ON CONFLICT DO NOTHING;--> statement-breakpoint | ||
| 348 | |||
| 349 | INSERT INTO "mcp_source_query_param" | ||
| 350 | ("scope_id", "id", "source_id", "name", "kind", "text_value", "secret_id", "secret_prefix") | ||
| 351 | SELECT | ||
| 352 | s."scope_id", | ||
| 353 | '[' || to_jsonb(s."id")::text || ',' || to_jsonb(q.key)::text || ']', | ||
| 354 | s."id", | ||
| 355 | q.key, | ||
| 356 | CASE | ||
| 357 | WHEN jsonb_typeof(q.value) = 'object' AND q.value ? 'secretId' THEN 'secret' | ||
| 358 | ELSE 'text' | ||
| 359 | END, | ||
| 360 | CASE WHEN jsonb_typeof(q.value) = 'string' THEN q.value #>> '{}' ELSE NULL END, | ||
| 361 | CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'secretId' ELSE NULL END, | ||
| 362 | CASE WHEN jsonb_typeof(q.value) = 'object' THEN q.value->>'prefix' ELSE NULL END | ||
| 363 | FROM "mcp_source" s, jsonb_each(s."config"->'queryParams') q | ||
| 364 | WHERE s."config"->'queryParams' IS NOT NULL | ||
| 365 | ON CONFLICT DO NOTHING;--> statement-breakpoint | ||
| 366 | |||
| 367 | -- Strip already-copied fields from config JSON. headers/queryParams | ||
| 368 | -- are always safe; auth is only stripped on rows whose auth was the | ||
| 369 | -- current shape (legacy inline-OAuth rows keep config.auth so | ||
| 370 | -- migrateLegacyConnections can mint a Connection from it). | ||
| 371 | UPDATE "mcp_source" | ||
| 372 | SET "config" = "config" - 'headers' - 'queryParams' | ||
| 373 | WHERE "config" IS NOT NULL;--> statement-breakpoint | ||
| 374 | |||
| 375 | UPDATE "mcp_source" | ||
| 376 | SET "config" = "config" - 'auth' | ||
| 377 | WHERE "config" IS NOT NULL | ||
| 378 | AND ( | ||
| 379 | "config"#>>'{auth,kind}' = 'none' | ||
| 380 | OR ( | ||
| 381 | "config"#>>'{auth,kind}' = 'header' | ||
| 382 | AND "config"#>>'{auth,secretId}' IS NOT NULL | ||
| 383 | ) | ||
| 384 | OR ( | ||
| 385 | "config"#>>'{auth,kind}' = 'oauth2' | ||
| 386 | AND "config"#>>'{auth,connectionId}' IS NOT NULL | ||
| 387 | ) | ||
| 388 | ); | ||
| @@ -0,0 +1,2594 @@ | |||
| 1 | { | ||
| 2 | "id": "b8d89563-58e1-4e6b-8674-f502338978e2", | ||
| 3 | "prevId": "f521e4d8-1eb4-4f84-8110-38fb5157aaca", | ||
| 4 | "version": "7", | ||
| 5 | "dialect": "postgresql", | ||
| 6 | "tables": { | ||
| 7 | "public.accounts": { | ||
| 8 | "name": "accounts", | ||
| 9 | "schema": "", | ||
| 10 | "columns": { | ||
| 11 | "id": { | ||
| 12 | "name": "id", | ||
| 13 | "type": "text", | ||
| 14 | "primaryKey": true, | ||
| 15 | "notNull": true | ||
| 16 | }, | ||
| 17 | "created_at": { | ||
| 18 | "name": "created_at", | ||
| 19 | "type": "timestamp with time zone", | ||
| 20 | "primaryKey": false, | ||
| 21 | "notNull": true, | ||
| 22 | "default": "now()" | ||
| 23 | } | ||
| 24 | }, | ||
| 25 | "indexes": {}, | ||
| 26 | "foreignKeys": {}, | ||
| 27 | "compositePrimaryKeys": {}, | ||
| 28 | "uniqueConstraints": {}, | ||
| 29 | "policies": {}, | ||
| 30 | "checkConstraints": {}, | ||
| 31 | "isRLSEnabled": false | ||
| 32 | }, | ||
| 33 | "public.memberships": { | ||
| 34 | "name": "memberships", | ||
| 35 | "schema": "", | ||
| 36 | "columns": { | ||
| 37 | "account_id": { | ||
| 38 | "name": "account_id", | ||
| 39 | "type": "text", | ||
| 40 | "primaryKey": false, | ||
| 41 | "notNull": true | ||
| 42 | }, | ||
| 43 | "organization_id": { | ||
| 44 | "name": "organization_id", | ||
| 45 | "type": "text", | ||
| 46 | "primaryKey": false, | ||
| 47 | "notNull": true | ||
| 48 | }, | ||
| 49 | "created_at": { | ||
| 50 | "name": "created_at", | ||
| 51 | "type": "timestamp with time zone", | ||
| 52 | "primaryKey": false, | ||
| 53 | "notNull": true, | ||
| 54 | "default": "now()" | ||
| 55 | } | ||
| 56 | }, | ||
| 57 | "indexes": {}, | ||
| 58 | "foreignKeys": { | ||
| 59 | "memberships_account_id_accounts_id_fk": { | ||
| 60 | "name": "memberships_account_id_accounts_id_fk", | ||
| 61 | "tableFrom": "memberships", | ||
| 62 | "tableTo": "accounts", | ||
| 63 | "columnsFrom": [ | ||
| 64 | "account_id" | ||
| 65 | ], | ||
| 66 | "columnsTo": [ | ||
| 67 | "id" | ||
| 68 | ], | ||
| 69 | "onDelete": "cascade", | ||
| 70 | "onUpdate": "no action" | ||
| 71 | }, | ||
| 72 | "memberships_organization_id_organizations_id_fk": { | ||
| 73 | "name": "memberships_organization_id_organizations_id_fk", | ||
| 74 | "tableFrom": "memberships", | ||
| 75 | "tableTo": "organizations", | ||
| 76 | "columnsFrom": [ | ||
| 77 | "organization_id" | ||
| 78 | ], | ||
| 79 | "columnsTo": [ | ||
| 80 | "id" | ||
| 81 | ], | ||
| 82 | "onDelete": "cascade", | ||
| 83 | "onUpdate": "no action" | ||
| 84 | } | ||
| 85 | }, | ||
| 86 | "compositePrimaryKeys": { | ||
| 87 | "memberships_account_id_organization_id_pk": { | ||
| 88 | "name": "memberships_account_id_organization_id_pk", | ||
| 89 | "columns": [ | ||
| 90 | "account_id", | ||
| 91 | "organization_id" | ||
| 92 | ] | ||
| 93 | } | ||
| 94 | }, | ||
| 95 | "uniqueConstraints": {}, | ||
| 96 | "policies": {}, | ||
| 97 | "checkConstraints": {}, | ||
| 98 | "isRLSEnabled": false | ||
| 99 | }, | ||
| 100 | "public.organizations": { | ||
| 101 | "name": "organizations", | ||
| 102 | "schema": "", | ||
| 103 | "columns": { | ||
| 104 | "id": { | ||
| 105 | "name": "id", | ||
| 106 | "type": "text", | ||
| 107 | "primaryKey": true, | ||
| 108 | "notNull": true | ||
| 109 | }, | ||
| 110 | "name": { | ||
| 111 | "name": "name", | ||
| 112 | "type": "text", | ||
| 113 | "primaryKey": false, | ||
| 114 | "notNull": true | ||
| 115 | }, | ||
| 116 | "created_at": { | ||
| 117 | "name": "created_at", | ||
| 118 | "type": "timestamp with time zone", | ||
| 119 | "primaryKey": false, | ||
| 120 | "notNull": true, | ||
| 121 | "default": "now()" | ||
| 122 | } | ||
| 123 | }, | ||
| 124 | "indexes": {}, | ||
| 125 | "foreignKeys": {}, | ||
| 126 | "compositePrimaryKeys": {}, | ||
| 127 | "uniqueConstraints": {}, | ||
| 128 | "policies": {}, | ||
| 129 | "checkConstraints": {}, | ||
| 130 | "isRLSEnabled": false | ||
| 131 | }, | ||
| 132 | "public.blob": { | ||
| 133 | "name": "blob", | ||
| 134 | "schema": "", | ||
| 135 | "columns": { | ||
| 136 | "namespace": { | ||
| 137 | "name": "namespace", | ||
| 138 | "type": "text", | ||
| 139 | "primaryKey": false, | ||
| 140 | "notNull": true | ||
| 141 | }, | ||
| 142 | "key": { | ||
| 143 | "name": "key", | ||
| 144 | "type": "text", | ||
| 145 | "primaryKey": false, | ||
| 146 | "notNull": true | ||
| 147 | }, | ||
| 148 | "value": { | ||
| 149 | "name": "value", | ||
| 150 | "type": "text", | ||
| 151 | "primaryKey": false, | ||
| 152 | "notNull": true | ||
| 153 | } | ||
| 154 | }, | ||
| 155 | "indexes": {}, | ||
| 156 | "foreignKeys": {}, | ||
| 157 | "compositePrimaryKeys": { | ||
| 158 | "blob_namespace_key_pk": { | ||
| 159 | "name": "blob_namespace_key_pk", | ||
| 160 | "columns": [ | ||
| 161 | "namespace", | ||
| 162 | "key" | ||
| 163 | ] | ||
| 164 | } | ||
| 165 | }, | ||
| 166 | "uniqueConstraints": {}, | ||
| 167 | "policies": {}, | ||
| 168 | "checkConstraints": {}, | ||
| 169 | "isRLSEnabled": false | ||
| 170 | }, | ||
| 171 | "public.connection": { | ||
| 172 | "name": "connection", | ||
| 173 | "schema": "", | ||
| 174 | "columns": { | ||
| 175 | "id": { | ||
| 176 | "name": "id", | ||
| 177 | "type": "text", | ||
| 178 | "primaryKey": false, | ||
| 179 | "notNull": true | ||
| 180 | }, | ||
| 181 | "scope_id": { | ||
| 182 | "name": "scope_id", | ||
| 183 | "type": "text", | ||
| 184 | "primaryKey": false, | ||
| 185 | "notNull": true | ||
| 186 | }, | ||
| 187 | "provider": { | ||
| 188 | "name": "provider", | ||
| 189 | "type": "text", | ||
| 190 | "primaryKey": false, | ||
| 191 | "notNull": true | ||
| 192 | }, | ||
| 193 | "identity_label": { | ||
| 194 | "name": "identity_label", | ||
| 195 | "type": "text", | ||
| 196 | "primaryKey": false, | ||
| 197 | "notNull": false | ||
| 198 | }, | ||
| 199 | "access_token_secret_id": { | ||
| 200 | "name": "access_token_secret_id", | ||
| 201 | "type": "text", | ||
| 202 | "primaryKey": false, | ||
| 203 | "notNull": true | ||
| 204 | }, | ||
| 205 | "refresh_token_secret_id": { | ||
| 206 | "name": "refresh_token_secret_id", | ||
| 207 | "type": "text", | ||
| 208 | "primaryKey": false, | ||
| 209 | "notNull": false | ||
| 210 | }, | ||
| 211 | "expires_at": { | ||
| 212 | "name": "expires_at", | ||
| 213 | "type": "bigint", | ||
| 214 | "primaryKey": false, | ||
| 215 | "notNull": false | ||
| 216 | }, | ||
| 217 | "scope": { | ||
| 218 | "name": "scope", | ||
| 219 | "type": "text", | ||
| 220 | "primaryKey": false, | ||
| 221 | "notNull": false | ||
| 222 | }, | ||
| 223 | "provider_state": { | ||
| 224 | "name": "provider_state", | ||
| 225 | "type": "jsonb", | ||
| 226 | "primaryKey": false, | ||
| 227 | "notNull": false | ||
| 228 | }, | ||
| 229 | "created_at": { | ||
| 230 | "name": "created_at", | ||
| 231 | "type": "timestamp", | ||
| 232 | "primaryKey": false, | ||
| 233 | "notNull": true | ||
| 234 | }, | ||
| 235 | "updated_at": { | ||
| 236 | "name": "updated_at", | ||
| 237 | "type": "timestamp", | ||
| 238 | "primaryKey": false, | ||
| 239 | "notNull": true | ||
| 240 | } | ||
| 241 | }, | ||
| 242 | "indexes": { | ||
| 243 | "connection_scope_id_idx": { | ||
| 244 | "name": "connection_scope_id_idx", | ||
| 245 | "columns": [ | ||
| 246 | { | ||
| 247 | "expression": "scope_id", | ||
| 248 | "isExpression": false, | ||
| 249 | "asc": true, | ||
| 250 | "nulls": "last" | ||
| 251 | } | ||
| 252 | ], | ||
| 253 | "isUnique": false, | ||
| 254 | "concurrently": false, | ||
| 255 | "method": "btree", | ||
| 256 | "with": {} | ||
| 257 | }, | ||
| 258 | "connection_provider_idx": { | ||
| 259 | "name": "connection_provider_idx", | ||
| 260 | "columns": [ | ||
| 261 | { | ||
| 262 | "expression": "provider", | ||
| 263 | "isExpression": false, | ||
| 264 | "asc": true, | ||
| 265 | "nulls": "last" | ||
| 266 | } | ||
| 267 | ], | ||
| 268 | "isUnique": false, | ||
| 269 | "concurrently": false, | ||
| 270 | "method": "btree", | ||
| 271 | "with": {} | ||
| 272 | } | ||
| 273 | }, | ||
| 274 | "foreignKeys": {}, | ||
| 275 | "compositePrimaryKeys": { | ||
| 276 | "connection_scope_id_id_pk": { | ||
| 277 | "name": "connection_scope_id_id_pk", | ||
| 278 | "columns": [ | ||
| 279 | "scope_id", | ||
| 280 | "id" | ||
| 281 | ] | ||
| 282 | } | ||
| 283 | }, | ||
| 284 | "uniqueConstraints": {}, | ||
| 285 | "policies": {}, | ||
| 286 | "checkConstraints": {}, | ||
| 287 | "isRLSEnabled": false | ||
| 288 | }, | ||
| 289 | "public.definition": { | ||
| 290 | "name": "definition", | ||
| 291 | "schema": "", | ||
| 292 | "columns": { | ||
| 293 | "id": { | ||
| 294 | "name": "id", | ||
| 295 | "type": "text", | ||
| 296 | "primaryKey": false, | ||
| 297 | "notNull": true | ||
| 298 | }, | ||
| 299 | "scope_id": { | ||
| 300 | "name": "scope_id", | ||
| 301 | "type": "text", | ||
| 302 | "primaryKey": false, | ||
| 303 | "notNull": true | ||
| 304 | }, | ||
| 305 | "source_id": { | ||
| 306 | "name": "source_id", | ||
| 307 | "type": "text", | ||
| 308 | "primaryKey": false, | ||
| 309 | "notNull": true | ||
| 310 | }, | ||
| 311 | "plugin_id": { | ||
| 312 | "name": "plugin_id", | ||
| 313 | "type": "text", | ||
| 314 | "primaryKey": false, | ||
| 315 | "notNull": true | ||
| 316 | }, | ||
| 317 | "name": { | ||
| 318 | "name": "name", | ||
| 319 | "type": "text", | ||
| 320 | "primaryKey": false, | ||
| 321 | "notNull": true | ||
| 322 | }, | ||
| 323 | "schema": { | ||
| 324 | "name": "schema", | ||
| 325 | "type": "jsonb", | ||
| 326 | "primaryKey": false, | ||
| 327 | "notNull": true | ||
| 328 | }, | ||
| 329 | "created_at": { | ||
| 330 | "name": "created_at", | ||
| 331 | "type": "timestamp", | ||
| 332 | "primaryKey": false, | ||
| 333 | "notNull": true | ||
| 334 | } | ||
| 335 | }, | ||
| 336 | "indexes": { | ||
| 337 | "definition_scope_id_idx": { | ||
| 338 | "name": "definition_scope_id_idx", | ||
| 339 | "columns": [ | ||
| 340 | { | ||
| 341 | "expression": "scope_id", | ||
| 342 | "isExpression": false, | ||
| 343 | "asc": true, | ||
| 344 | "nulls": "last" | ||
| 345 | } | ||
| 346 | ], | ||
| 347 | "isUnique": false, | ||
| 348 | "concurrently": false, | ||
| 349 | "method": "btree", | ||
| 350 | "with": {} | ||
| 351 | }, | ||
| 352 | "definition_source_id_idx": { | ||
| 353 | "name": "definition_source_id_idx", | ||
| 354 | "columns": [ | ||
| 355 | { | ||
| 356 | "expression": "source_id", | ||
| 357 | "isExpression": false, | ||
| 358 | "asc": true, | ||
| 359 | "nulls": "last" | ||
| 360 | } | ||
| 361 | ], | ||
| 362 | "isUnique": false, | ||
| 363 | "concurrently": false, | ||
| 364 | "method": "btree", | ||
| 365 | "with": {} | ||
| 366 | }, | ||
| 367 | "definition_plugin_id_idx": { | ||
| 368 | "name": "definition_plugin_id_idx", | ||
| 369 | "columns": [ | ||
| 370 | { | ||
| 371 | "expression": "plugin_id", | ||
| 372 | "isExpression": false, | ||
| 373 | "asc": true, | ||
| 374 | "nulls": "last" | ||
| 375 | } | ||
| 376 | ], | ||
| 377 | "isUnique": false, | ||
| 378 | "concurrently": false, | ||
| 379 | "method": "btree", | ||
| 380 | "with": {} | ||
| 381 | } | ||
| 382 | }, | ||
| 383 | "foreignKeys": {}, | ||
| 384 | "compositePrimaryKeys": { | ||
| 385 | "definition_scope_id_id_pk": { | ||
| 386 | "name": "definition_scope_id_id_pk", | ||
| 387 | "columns": [ | ||
| 388 | "scope_id", | ||
| 389 | "id" | ||
| 390 | ] | ||
| 391 | } | ||
| 392 | }, | ||
| 393 | "uniqueConstraints": {}, | ||
| 394 | "policies": {}, | ||
| 395 | "checkConstraints": {}, | ||
| 396 | "isRLSEnabled": false | ||
| 397 | }, | ||
| 398 | "public.graphql_operation": { | ||
| 399 | "name": "graphql_operation", | ||
| 400 | "schema": "", | ||
| 401 | "columns": { | ||
| 402 | "id": { | ||
| 403 | "name": "id", | ||
| 404 | "type": "text", | ||
| 405 | "primaryKey": false, | ||
| 406 | "notNull": true | ||
| 407 | }, | ||
| 408 | "scope_id": { | ||
| 409 | "name": "scope_id", | ||
| 410 | "type": "text", | ||
| 411 | "primaryKey": false, | ||
| 412 | "notNull": true | ||
| 413 | }, | ||
| 414 | "source_id": { | ||
| 415 | "name": "source_id", | ||
| 416 | "type": "text", | ||
| 417 | "primaryKey": false, | ||
| 418 | "notNull": true | ||
| 419 | }, | ||
| 420 | "binding": { | ||
| 421 | "name": "binding", | ||
| 422 | "type": "jsonb", | ||
| 423 | "primaryKey": false, | ||
| 424 | "notNull": true | ||
| 425 | } | ||
| 426 | }, | ||
| 427 | "indexes": { | ||
| 428 | "graphql_operation_scope_id_idx": { | ||
| 429 | "name": "graphql_operation_scope_id_idx", | ||
| 430 | "columns": [ | ||
| 431 | { | ||
| 432 | "expression": "scope_id", | ||
| 433 | "isExpression": false, | ||
| 434 | "asc": true, | ||
| 435 | "nulls": "last" | ||
| 436 | } | ||
| 437 | ], | ||
| 438 | "isUnique": false, | ||
| 439 | "concurrently": false, | ||
| 440 | "method": "btree", | ||
| 441 | "with": {} | ||
| 442 | }, | ||
| 443 | "graphql_operation_source_id_idx": { | ||
| 444 | "name": "graphql_operation_source_id_idx", | ||
| 445 | "columns": [ | ||
| 446 | { | ||
| 447 | "expression": "source_id", | ||
| 448 | "isExpression": false, | ||
| 449 | "asc": true, | ||
| 450 | "nulls": "last" | ||
| 451 | } | ||
| 452 | ], | ||
| 453 | "isUnique": false, | ||
| 454 | "concurrently": false, | ||
| 455 | "method": "btree", | ||
| 456 | "with": {} | ||
| 457 | } | ||
| 458 | }, | ||
| 459 | "foreignKeys": {}, | ||
| 460 | "compositePrimaryKeys": { | ||
| 461 | "graphql_operation_scope_id_id_pk": { | ||
| 462 | "name": "graphql_operation_scope_id_id_pk", | ||
| 463 | "columns": [ | ||
| 464 | "scope_id", | ||
| 465 | "id" | ||
| 466 | ] | ||
| 467 | } | ||
| 468 | }, | ||
| 469 | "uniqueConstraints": {}, | ||
| 470 | "policies": {}, | ||
| 471 | "checkConstraints": {}, | ||
| 472 | "isRLSEnabled": false | ||
| 473 | }, | ||
| 474 | "public.graphql_source": { | ||
| 475 | "name": "graphql_source", | ||
| 476 | "schema": "", | ||
| 477 | "columns": { | ||
| 478 | "id": { | ||
| 479 | "name": "id", | ||
| 480 | "type": "text", | ||
| 481 | "primaryKey": false, | ||
| 482 | "notNull": true | ||
| 483 | }, | ||
| 484 | "scope_id": { | ||
| 485 | "name": "scope_id", | ||
| 486 | "type": "text", | ||
| 487 | "primaryKey": false, | ||
| 488 | "notNull": true | ||
| 489 | }, | ||
| 490 | "name": { | ||
| 491 | "name": "name", | ||
| 492 | "type": "text", | ||
| 493 | "primaryKey": false, | ||
| 494 | "notNull": true | ||
| 495 | }, | ||
| 496 | "endpoint": { | ||
| 497 | "name": "endpoint", | ||
| 498 | "type": "text", | ||
| 499 | "primaryKey": false, | ||
| 500 | "notNull": true | ||
| 501 | }, | ||
| 502 | "auth_kind": { | ||
| 503 | "name": "auth_kind", | ||
| 504 | "type": "text", | ||
| 505 | "primaryKey": false, | ||
| 506 | "notNull": true, | ||
| 507 | "default": "none" | ||
| 508 | }, | ||
| 509 | "auth_connection_id": { | ||
| 510 | "name": "auth_connection_id", | ||
| 511 | "type": "text", | ||
| 512 | "primaryKey": false, | ||
| 513 | "notNull": false | ||
| 514 | } | ||
| 515 | }, | ||
| 516 | "indexes": { | ||
| 517 | "graphql_source_scope_id_idx": { | ||
| 518 | "name": "graphql_source_scope_id_idx", | ||
| 519 | "columns": [ | ||
| 520 | { | ||
| 521 | "expression": "scope_id", | ||
| 522 | "isExpression": false, | ||
| 523 | "asc": true, | ||
| 524 | "nulls": "last" | ||
| 525 | } | ||
| 526 | ], | ||
| 527 | "isUnique": false, | ||
| 528 | "concurrently": false, | ||
| 529 | "method": "btree", | ||
| 530 | "with": {} | ||
| 531 | }, | ||
| 532 | "graphql_source_auth_connection_id_idx": { | ||
| 533 | "name": "graphql_source_auth_connection_id_idx", | ||
| 534 | "columns": [ | ||
| 535 | { | ||
| 536 | "expression": "auth_connection_id", | ||
| 537 | "isExpression": false, | ||
| 538 | "asc": true, | ||
| 539 | "nulls": "last" | ||
| 540 | } | ||
| 541 | ], | ||
| 542 | "isUnique": false, | ||
| 543 | "concurrently": false, | ||
| 544 | "method": "btree", | ||
| 545 | "with": {} | ||
| 546 | } | ||
| 547 | }, | ||
| 548 | "foreignKeys": {}, | ||
| 549 | "compositePrimaryKeys": { | ||
| 550 | "graphql_source_scope_id_id_pk": { | ||
| 551 | "name": "graphql_source_scope_id_id_pk", | ||
| 552 | "columns": [ | ||
| 553 | "scope_id", | ||
| 554 | "id" | ||
| 555 | ] | ||
| 556 | } | ||
| 557 | }, | ||
| 558 | "uniqueConstraints": {}, | ||
| 559 | "policies": {}, | ||
| 560 | "checkConstraints": {}, | ||
| 561 | "isRLSEnabled": false | ||
| 562 | }, | ||
| 563 | "public.mcp_binding": { | ||
| 564 | "name": "mcp_binding", | ||
| 565 | "schema": "", | ||
| 566 | "columns": { | ||
| 567 | "id": { | ||
| 568 | "name": "id", | ||
| 569 | "type": "text", | ||
| 570 | "primaryKey": false, | ||
| 571 | "notNull": true | ||
| 572 | }, | ||
| 573 | "scope_id": { | ||
| 574 | "name": "scope_id", | ||
| 575 | "type": "text", | ||
| 576 | "primaryKey": false, | ||
| 577 | "notNull": true | ||
| 578 | }, | ||
| 579 | "source_id": { | ||
| 580 | "name": "source_id", | ||
| 581 | "type": "text", | ||
| 582 | "primaryKey": false, | ||
| 583 | "notNull": true | ||
| 584 | }, | ||
| 585 | "binding": { | ||
| 586 | "name": "binding", | ||
| 587 | "type": "jsonb", | ||
| 588 | "primaryKey": false, | ||
| 589 | "notNull": true | ||
| 590 | }, | ||
| 591 | "created_at": { | ||
| 592 | "name": "created_at", | ||
| 593 | "type": "timestamp", | ||
| 594 | "primaryKey": false, | ||
| 595 | "notNull": true | ||
| 596 | } | ||
| 597 | }, | ||
| 598 | "indexes": { | ||
| 599 | "mcp_binding_scope_id_idx": { | ||
| 600 | "name": "mcp_binding_scope_id_idx", | ||
| 601 | "columns": [ | ||
| 602 | { | ||
| 603 | "expression": "scope_id", | ||
| 604 | "isExpression": false, | ||
| 605 | "asc": true, | ||
| 606 | "nulls": "last" | ||
| 607 | } | ||
| 608 | ], | ||
| 609 | "isUnique": false, | ||
| 610 | "concurrently": false, | ||
| 611 | "method": "btree", | ||
| 612 | "with": {} | ||
| 613 | }, | ||
| 614 | "mcp_binding_source_id_idx": { | ||
| 615 | "name": "mcp_binding_source_id_idx", | ||
| 616 | "columns": [ | ||
| 617 | { | ||
| 618 | "expression": "source_id", | ||
| 619 | "isExpression": false, | ||
| 620 | "asc": true, | ||
| 621 | "nulls": "last" | ||
| 622 | } | ||
| 623 | ], | ||
| 624 | "isUnique": false, | ||
| 625 | "concurrently": false, | ||
| 626 | "method": "btree", | ||
| 627 | "with": {} | ||
| 628 | } | ||
| 629 | }, | ||
| 630 | "foreignKeys": {}, | ||
| 631 | "compositePrimaryKeys": { | ||
| 632 | "mcp_binding_scope_id_id_pk": { | ||
| 633 | "name": "mcp_binding_scope_id_id_pk", | ||
| 634 | "columns": [ | ||
| 635 | "scope_id", | ||
| 636 | "id" | ||
| 637 | ] | ||
| 638 | } | ||
| 639 | }, | ||
| 640 | "uniqueConstraints": {}, | ||
| 641 | "policies": {}, | ||
| 642 | "checkConstraints": {}, | ||
| 643 | "isRLSEnabled": false | ||
| 644 | }, | ||
| 645 | "public.mcp_source": { | ||
| 646 | "name": "mcp_source", | ||
| 647 | "schema": "", | ||
| 648 | "columns": { | ||
| 649 | "id": { | ||
| 650 | "name": "id", | ||
| 651 | "type": "text", | ||
| 652 | "primaryKey": false, | ||
| 653 | "notNull": true | ||
| 654 | }, | ||
| 655 | "scope_id": { | ||
| 656 | "name": "scope_id", | ||
| 657 | "type": "text", | ||
| 658 | "primaryKey": false, | ||
| 659 | "notNull": true | ||
| 660 | }, | ||
| 661 | "name": { | ||
| 662 | "name": "name", | ||
| 663 | "type": "text", | ||
| 664 | "primaryKey": false, | ||
| 665 | "notNull": true | ||
| 666 | }, | ||
| 667 | "config": { | ||
| 668 | "name": "config", | ||
| 669 | "type": "jsonb", | ||
| 670 | "primaryKey": false, | ||
| 671 | "notNull": true | ||
| 672 | }, | ||
| 673 | "created_at": { | ||
| 674 | "name": "created_at", | ||
| 675 | "type": "timestamp", | ||
| 676 | "primaryKey": false, | ||
| 677 | "notNull": true | ||
| 678 | }, | ||
| 679 | "auth_kind": { | ||
| 680 | "name": "auth_kind", | ||
| 681 | "type": "text", | ||
| 682 | "primaryKey": false, | ||
| 683 | "notNull": true, | ||
| 684 | "default": "'none'" | ||
| 685 | }, | ||
| 686 | "auth_header_name": { | ||
| 687 | "name": "auth_header_name", | ||
| 688 | "type": "text", | ||
| 689 | "primaryKey": false, | ||
| 690 | "notNull": false | ||
| 691 | }, | ||
| 692 | "auth_secret_id": { | ||
| 693 | "name": "auth_secret_id", | ||
| 694 | "type": "text", | ||
| 695 | "primaryKey": false, | ||
| 696 | "notNull": false | ||
| 697 | }, | ||
| 698 | "auth_secret_prefix": { | ||
| 699 | "name": "auth_secret_prefix", | ||
| 700 | "type": "text", | ||
| 701 | "primaryKey": false, | ||
| 702 | "notNull": false | ||
| 703 | }, | ||
| 704 | "auth_connection_id": { | ||
| 705 | "name": "auth_connection_id", | ||
| 706 | "type": "text", | ||
| 707 | "primaryKey": false, | ||
| 708 | "notNull": false | ||
| 709 | }, | ||
| 710 | "auth_client_id_secret_id": { | ||
| 711 | "name": "auth_client_id_secret_id", | ||
| 712 | "type": "text", | ||
| 713 | "primaryKey": false, | ||
| 714 | "notNull": false | ||
| 715 | }, | ||
| 716 | "auth_client_secret_secret_id": { | ||
| 717 | "name": "auth_client_secret_secret_id", | ||
| 718 | "type": "text", | ||
| 719 | "primaryKey": false, | ||
| 720 | "notNull": false | ||
| 721 | } | ||
| 722 | }, | ||
| 723 | "indexes": { | ||
| 724 | "mcp_source_scope_id_idx": { | ||
| 725 | "name": "mcp_source_scope_id_idx", | ||
| 726 | "columns": [ | ||
| 727 | { | ||
| 728 | "expression": "scope_id", | ||
| 729 | "isExpression": false, | ||
| 730 | "asc": true, | ||
| 731 | "nulls": "last" | ||
| 732 | } | ||
| 733 | ], | ||
| 734 | "isUnique": false, | ||
| 735 | "concurrently": false, | ||
| 736 | "method": "btree", | ||
| 737 | "with": {} | ||
| 738 | }, | ||
| 739 | "mcp_source_auth_secret_id_idx": { | ||
| 740 | "name": "mcp_source_auth_secret_id_idx", | ||
| 741 | "columns": [ | ||
| 742 | { | ||
| 743 | "expression": "auth_secret_id", | ||
| 744 | "isExpression": false, | ||
| 745 | "asc": true, | ||
| 746 | "nulls": "last" | ||
| 747 | } | ||
| 748 | ], | ||
| 749 | "isUnique": false, | ||
| 750 | "concurrently": false, | ||
| 751 | "method": "btree", | ||
| 752 | "with": {} | ||
| 753 | }, | ||
| 754 | "mcp_source_auth_connection_id_idx": { | ||
| 755 | "name": "mcp_source_auth_connection_id_idx", | ||
| 756 | "columns": [ | ||
| 757 | { | ||
| 758 | "expression": "auth_connection_id", | ||
| 759 | "isExpression": false, | ||
| 760 | "asc": true, | ||
| 761 | "nulls": "last" | ||
| 762 | } | ||
| 763 | ], | ||
| 764 | "isUnique": false, | ||
| 765 | "concurrently": false, | ||
| 766 | "method": "btree", | ||
| 767 | "with": {} | ||
| 768 | }, | ||
| 769 | "mcp_source_auth_client_id_secret_id_idx": { | ||
| 770 | "name": "mcp_source_auth_client_id_secret_id_idx", | ||
| 771 | "columns": [ | ||
| 772 | { | ||
| 773 | "expression": "auth_client_id_secret_id", | ||
| 774 | "isExpression": false, | ||
| 775 | "asc": true, | ||
| 776 | "nulls": "last" | ||
| 777 | } | ||
| 778 | ], | ||
| 779 | "isUnique": false, | ||
| 780 | "concurrently": false, | ||
| 781 | "method": "btree", | ||
| 782 | "with": {} | ||
| 783 | }, | ||
| 784 | "mcp_source_auth_client_secret_secret_id_idx": { | ||
| 785 | "name": "mcp_source_auth_client_secret_secret_id_idx", | ||
| 786 | "columns": [ | ||
| 787 | { | ||
| 788 | "expression": "auth_client_secret_secret_id", | ||
| 789 | "isExpression": false, | ||
| 790 | "asc": true, | ||
| 791 | "nulls": "last" | ||
| 792 | } | ||
| 793 | ], | ||
| 794 | "isUnique": false, | ||
| 795 | "concurrently": false, | ||
| 796 | "method": "btree", | ||
| 797 | "with": {} | ||
| 798 | } | ||
| 799 | }, | ||
| 800 | "foreignKeys": {}, | ||
| 801 | "compositePrimaryKeys": { | ||
| 802 | "mcp_source_scope_id_id_pk": { | ||
| 803 | "name": "mcp_source_scope_id_id_pk", | ||
| 804 | "columns": [ | ||
| 805 | "scope_id", | ||
| 806 | "id" | ||
| 807 | ] | ||
| 808 | } | ||
| 809 | }, | ||
| 810 | "uniqueConstraints": {}, | ||
| 811 | "policies": {}, | ||
| 812 | "checkConstraints": {}, | ||
| 813 | "isRLSEnabled": false | ||
| 814 | }, | ||
| 815 | "public.oauth2_session": { | ||
| 816 | "name": "oauth2_session", | ||
| 817 | "schema": "", | ||
| 818 | "columns": { | ||
| 819 | "id": { | ||
| 820 | "name": "id", | ||
| 821 | "type": "text", | ||
| 822 | "primaryKey": false, | ||
| 823 | "notNull": true | ||
| 824 | }, | ||
| 825 | "scope_id": { | ||
| 826 | "name": "scope_id", | ||
| 827 | "type": "text", | ||
| 828 | "primaryKey": false, | ||
| 829 | "notNull": true | ||
| 830 | }, | ||
| 831 | "plugin_id": { | ||
| 832 | "name": "plugin_id", | ||
| 833 | "type": "text", | ||
| 834 | "primaryKey": false, | ||
| 835 | "notNull": true | ||
| 836 | }, | ||
| 837 | "strategy": { | ||
| 838 | "name": "strategy", | ||
| 839 | "type": "text", | ||
| 840 | "primaryKey": false, | ||
| 841 | "notNull": true | ||
| 842 | }, | ||
| 843 | "connection_id": { | ||
| 844 | "name": "connection_id", | ||
| 845 | "type": "text", | ||
| 846 | "primaryKey": false, | ||
| 847 | "notNull": true | ||
| 848 | }, | ||
| 849 | "token_scope": { | ||
| 850 | "name": "token_scope", | ||
| 851 | "type": "text", | ||
| 852 | "primaryKey": false, | ||
| 853 | "notNull": true | ||
| 854 | }, | ||
| 855 | "redirect_url": { | ||
| 856 | "name": "redirect_url", | ||
| 857 | "type": "text", | ||
| 858 | "primaryKey": false, | ||
| 859 | "notNull": true | ||
| 860 | }, | ||
| 861 | "payload": { | ||
| 862 | "name": "payload", | ||
| 863 | "type": "jsonb", | ||
| 864 | "primaryKey": false, | ||
| 865 | "notNull": true | ||
| 866 | }, | ||
| 867 | "expires_at": { | ||
| 868 | "name": "expires_at", | ||
| 869 | "type": "bigint", | ||
| 870 | "primaryKey": false, | ||
| 871 | "notNull": true | ||
| 872 | }, | ||
| 873 | "created_at": { | ||
| 874 | "name": "created_at", | ||
| 875 | "type": "timestamp", | ||
| 876 | "primaryKey": false, | ||
| 877 | "notNull": true | ||
| 878 | } | ||
| 879 | }, | ||
| 880 | "indexes": { | ||
| 881 | "oauth2_session_scope_id_idx": { | ||
| 882 | "name": "oauth2_session_scope_id_idx", | ||
| 883 | "columns": [ | ||
| 884 | { | ||
| 885 | "expression": "scope_id", | ||
| 886 | "isExpression": false, | ||
| 887 | "asc": true, | ||
| 888 | "nulls": "last" | ||
| 889 | } | ||
| 890 | ], | ||
| 891 | "isUnique": false, | ||
| 892 | "concurrently": false, | ||
| 893 | "method": "btree", | ||
| 894 | "with": {} | ||
| 895 | }, | ||
| 896 | "oauth2_session_plugin_id_idx": { | ||
| 897 | "name": "oauth2_session_plugin_id_idx", | ||
| 898 | "columns": [ | ||
| 899 | { | ||
| 900 | "expression": "plugin_id", | ||
| 901 | "isExpression": false, | ||
| 902 | "asc": true, | ||
| 903 | "nulls": "last" | ||
| 904 | } | ||
| 905 | ], | ||
| 906 | "isUnique": false, | ||
| 907 | "concurrently": false, | ||
| 908 | "method": "btree", | ||
| 909 | "with": {} | ||
| 910 | }, | ||
| 911 | "oauth2_session_connection_id_idx": { | ||
| 912 | "name": "oauth2_session_connection_id_idx", | ||
| 913 | "columns": [ | ||
| 914 | { | ||
| 915 | "expression": "connection_id", | ||
| 916 | "isExpression": false, | ||
| 917 | "asc": true, | ||
| 918 | "nulls": "last" | ||
| 919 | } | ||
| 920 | ], | ||
| 921 | "isUnique": false, | ||
| 922 | "concurrently": false, | ||
| 923 | "method": "btree", | ||
| 924 | "with": {} | ||
| 925 | } | ||
| 926 | }, | ||
| 927 | "foreignKeys": {}, | ||
| 928 | "compositePrimaryKeys": { | ||
| 929 | "oauth2_session_scope_id_id_pk": { | ||
| 930 | "name": "oauth2_session_scope_id_id_pk", | ||
| 931 | "columns": [ | ||
| 932 | "scope_id", | ||
| 933 | "id" | ||
| 934 | ] | ||
| 935 | } | ||
| 936 | }, | ||
| 937 | "uniqueConstraints": {}, | ||
| 938 | "policies": {}, | ||
| 939 | "checkConstraints": {}, | ||
| 940 | "isRLSEnabled": false | ||
| 941 | }, | ||
| 942 | "public.openapi_operation": { | ||
| 943 | "name": "openapi_operation", | ||
| 944 | "schema": "", | ||
| 945 | "columns": { | ||
| 946 | "id": { | ||
| 947 | "name": "id", | ||
| 948 | "type": "text", | ||
| 949 | "primaryKey": false, | ||
| 950 | "notNull": true | ||
| 951 | }, | ||
| 952 | "scope_id": { | ||
| 953 | "name": "scope_id", | ||
| 954 | "type": "text", | ||
| 955 | "primaryKey": false, | ||
| 956 | "notNull": true | ||
| 957 | }, | ||
| 958 | "source_id": { | ||
| 959 | "name": "source_id", | ||
| 960 | "type": "text", | ||
| 961 | "primaryKey": false, | ||
| 962 | "notNull": true | ||
| 963 | }, | ||
| 964 | "binding": { | ||
| 965 | "name": "binding", | ||
| 966 | "type": "jsonb", | ||
| 967 | "primaryKey": false, | ||
| 968 | "notNull": true | ||
| 969 | } | ||
| 970 | }, | ||
| 971 | "indexes": { | ||
| 972 | "openapi_operation_scope_id_idx": { | ||
| 973 | "name": "openapi_operation_scope_id_idx", | ||
| 974 | "columns": [ | ||
| 975 | { | ||
| 976 | "expression": "scope_id", | ||
| 977 | "isExpression": false, | ||
| 978 | "asc": true, | ||
| 979 | "nulls": "last" | ||
| 980 | } | ||
| 981 | ], | ||
| 982 | "isUnique": false, | ||
| 983 | "concurrently": false, | ||
| 984 | "method": "btree", | ||
| 985 | "with": {} | ||
| 986 | }, | ||
| 987 | "openapi_operation_source_id_idx": { | ||
| 988 | "name": "openapi_operation_source_id_idx", | ||
| 989 | "columns": [ | ||
| 990 | { | ||
| 991 | "expression": "source_id", | ||
| 992 | "isExpression": false, | ||
| 993 | "asc": true, | ||
| 994 | "nulls": "last" | ||
| 995 | } | ||
| 996 | ], | ||
| 997 | "isUnique": false, | ||
| 998 | "concurrently": false, | ||
| 999 | "method": "btree", | ||
| 1000 | "with": {} | ||
| 1001 | } | ||
| 1002 | }, | ||
| 1003 | "foreignKeys": {}, | ||
| 1004 | "compositePrimaryKeys": { | ||
| 1005 | "openapi_operation_scope_id_id_pk": { | ||
| 1006 | "name": "openapi_operation_scope_id_id_pk", | ||
| 1007 | "columns": [ | ||
| 1008 | "scope_id", | ||
| 1009 | "id" | ||
| 1010 | ] | ||
| 1011 | } | ||
| 1012 | }, | ||
| 1013 | "uniqueConstraints": {}, | ||
| 1014 | "policies": {}, | ||
| 1015 | "checkConstraints": {}, | ||
| 1016 | "isRLSEnabled": false | ||
| 1017 | }, | ||
| 1018 | "public.openapi_source": { | ||
| 1019 | "name": "openapi_source", | ||
| 1020 | "schema": "", | ||
| 1021 | "columns": { | ||
| 1022 | "id": { | ||
| 1023 | "name": "id", | ||
| 1024 | "type": "text", | ||
| 1025 | "primaryKey": false, | ||
| 1026 | "notNull": true | ||
| 1027 | }, | ||
| 1028 | "scope_id": { | ||
| 1029 | "name": "scope_id", | ||
| 1030 | "type": "text", | ||
| 1031 | "primaryKey": false, | ||
| 1032 | "notNull": true | ||
| 1033 | }, | ||
| 1034 | "name": { | ||
| 1035 | "name": "name", | ||
| 1036 | "type": "text", | ||
| 1037 | "primaryKey": false, | ||
| 1038 | "notNull": true | ||
| 1039 | }, | ||
| 1040 | "spec": { | ||
| 1041 | "name": "spec", | ||
| 1042 | "type": "text", | ||
| 1043 | "primaryKey": false, | ||
| 1044 | "notNull": true | ||
| 1045 | }, | ||
| 1046 | "source_url": { | ||
| 1047 | "name": "source_url", | ||
| 1048 | "type": "text", | ||
| 1049 | "primaryKey": false, | ||
| 1050 | "notNull": false | ||
| 1051 | }, | ||
| 1052 | "base_url": { | ||
| 1053 | "name": "base_url", | ||
| 1054 | "type": "text", | ||
| 1055 | "primaryKey": false, | ||
| 1056 | "notNull": false | ||
| 1057 | }, | ||
| 1058 | "headers": { | ||
| 1059 | "name": "headers", | ||
| 1060 | "type": "jsonb", | ||
| 1061 | "primaryKey": false, | ||
| 1062 | "notNull": false | ||
| 1063 | }, | ||
| 1064 | "oauth2": { | ||
| 1065 | "name": "oauth2", | ||
| 1066 | "type": "jsonb", | ||
| 1067 | "primaryKey": false, | ||
| 1068 | "notNull": false | ||
| 1069 | } | ||
| 1070 | }, | ||
| 1071 | "indexes": { | ||
| 1072 | "openapi_source_scope_id_idx": { | ||
| 1073 | "name": "openapi_source_scope_id_idx", | ||
| 1074 | "columns": [ | ||
| 1075 | { | ||
| 1076 | "expression": "scope_id", | ||
| 1077 | "isExpression": false, | ||
| 1078 | "asc": true, | ||
| 1079 | "nulls": "last" | ||
| 1080 | } | ||
| 1081 | ], | ||
| 1082 | "isUnique": false, | ||
| 1083 | "concurrently": false, | ||
| 1084 | "method": "btree", | ||
| 1085 | "with": {} | ||
| 1086 | } | ||
| 1087 | }, | ||
| 1088 | "foreignKeys": {}, | ||
| 1089 | "compositePrimaryKeys": { | ||
| 1090 | "openapi_source_scope_id_id_pk": { | ||
| 1091 | "name": "openapi_source_scope_id_id_pk", | ||
| 1092 | "columns": [ | ||
| 1093 | "scope_id", | ||
| 1094 | "id" | ||
| 1095 | ] | ||
| 1096 | } | ||
| 1097 | }, | ||
| 1098 | "uniqueConstraints": {}, | ||
| 1099 | "policies": {}, | ||
| 1100 | "checkConstraints": {}, | ||
| 1101 | "isRLSEnabled": false | ||
| 1102 | }, | ||
| 1103 | "public.openapi_source_binding": { | ||
| 1104 | "name": "openapi_source_binding", | ||
| 1105 | "schema": "", | ||
| 1106 | "columns": { | ||
| 1107 | "id": { | ||
| 1108 | "name": "id", | ||
| 1109 | "type": "text", | ||
| 1110 | "primaryKey": true, | ||
| 1111 | "notNull": true | ||
| 1112 | }, | ||
| 1113 | "source_id": { | ||
| 1114 | "name": "source_id", | ||
| 1115 | "type": "text", | ||
| 1116 | "primaryKey": false, | ||
| 1117 | "notNull": true | ||
| 1118 | }, | ||
| 1119 | "source_scope_id": { | ||
| 1120 | "name": "source_scope_id", | ||
| 1121 | "type": "text", | ||
| 1122 | "primaryKey": false, | ||
| 1123 | "notNull": true | ||
| 1124 | }, | ||
| 1125 | "target_scope_id": { | ||
| 1126 | "name": "target_scope_id", | ||
| 1127 | "type": "text", | ||
| 1128 | "primaryKey": false, | ||
| 1129 | "notNull": true | ||
| 1130 | }, | ||
| 1131 | "slot": { | ||
| 1132 | "name": "slot", | ||
| 1133 | "type": "text", | ||
| 1134 | "primaryKey": false, | ||
| 1135 | "notNull": true | ||
| 1136 | }, | ||
| 1137 | "created_at": { | ||
| 1138 | "name": "created_at", | ||
| 1139 | "type": "timestamp", | ||
| 1140 | "primaryKey": false, | ||
| 1141 | "notNull": true | ||
| 1142 | }, | ||
| 1143 | "updated_at": { | ||
| 1144 | "name": "updated_at", | ||
| 1145 | "type": "timestamp", | ||
| 1146 | "primaryKey": false, | ||
| 1147 | "notNull": true | ||
| 1148 | }, | ||
| 1149 | "kind": { | ||
| 1150 | "name": "kind", | ||
| 1151 | "type": "text", | ||
| 1152 | "primaryKey": false, | ||
| 1153 | "notNull": true, | ||
| 1154 | "default": "'text'" | ||
| 1155 | }, | ||
| 1156 | "secret_id": { | ||
| 1157 | "name": "secret_id", | ||
| 1158 | "type": "text", | ||
| 1159 | "primaryKey": false, | ||
| 1160 | "notNull": false | ||
| 1161 | }, | ||
| 1162 | "connection_id": { | ||
| 1163 | "name": "connection_id", | ||
| 1164 | "type": "text", | ||
| 1165 | "primaryKey": false, | ||
| 1166 | "notNull": false | ||
| 1167 | }, | ||
| 1168 | "text_value": { | ||
| 1169 | "name": "text_value", | ||
| 1170 | "type": "text", | ||
| 1171 | "primaryKey": false, | ||
| 1172 | "notNull": false | ||
| 1173 | } | ||
| 1174 | }, | ||
| 1175 | "indexes": { | ||
| 1176 | "openapi_source_binding_source_id_idx": { | ||
| 1177 | "name": "openapi_source_binding_source_id_idx", | ||
| 1178 | "columns": [ | ||
| 1179 | { | ||
| 1180 | "expression": "source_id", | ||
| 1181 | "isExpression": false, | ||
| 1182 | "asc": true, | ||
| 1183 | "nulls": "last" | ||
| 1184 | } | ||
| 1185 | ], | ||
| 1186 | "isUnique": false, | ||
| 1187 | "concurrently": false, | ||
| 1188 | "method": "btree", | ||
| 1189 | "with": {} | ||
| 1190 | }, | ||
| 1191 | "openapi_source_binding_source_scope_id_idx": { | ||
| 1192 | "name": "openapi_source_binding_source_scope_id_idx", | ||
| 1193 | "columns": [ | ||
| 1194 | { | ||
| 1195 | "expression": "source_scope_id", | ||
| 1196 | "isExpression": false, | ||
| 1197 | "asc": true, | ||
| 1198 | "nulls": "last" | ||
| 1199 | } | ||
| 1200 | ], | ||
| 1201 | "isUnique": false, | ||
| 1202 | "concurrently": false, | ||
| 1203 | "method": "btree", | ||
| 1204 | "with": {} | ||
| 1205 | }, | ||
| 1206 | "openapi_source_binding_target_scope_id_idx": { | ||
| 1207 | "name": "openapi_source_binding_target_scope_id_idx", | ||
| 1208 | "columns": [ | ||
| 1209 | { | ||
| 1210 | "expression": "target_scope_id", | ||
| 1211 | "isExpression": false, | ||
| 1212 | "asc": true, | ||
| 1213 | "nulls": "last" | ||
| 1214 | } | ||
| 1215 | ], | ||
| 1216 | "isUnique": false, | ||
| 1217 | "concurrently": false, | ||
| 1218 | "method": "btree", | ||
| 1219 | "with": {} | ||
| 1220 | }, | ||
| 1221 | "openapi_source_binding_slot_idx": { | ||
| 1222 | "name": "openapi_source_binding_slot_idx", | ||
| 1223 | "columns": [ | ||
| 1224 | { | ||
| 1225 | "expression": "slot", | ||
| 1226 | "isExpression": false, | ||
| 1227 | "asc": true, | ||
| 1228 | "nulls": "last" | ||
| 1229 | } | ||
| 1230 | ], | ||
| 1231 | "isUnique": false, | ||
| 1232 | "concurrently": false, | ||
| 1233 | "method": "btree", | ||
| 1234 | "with": {} | ||
| 1235 | }, | ||
| 1236 | "openapi_source_binding_secret_id_idx": { | ||
| 1237 | "name": "openapi_source_binding_secret_id_idx", | ||
| 1238 | "columns": [ | ||
| 1239 | { | ||
| 1240 | "expression": "secret_id", | ||
| 1241 | "isExpression": false, | ||
| 1242 | "asc": true, | ||
| 1243 | "nulls": "last" | ||
| 1244 | } | ||
| 1245 | ], | ||
| 1246 | "isUnique": false, | ||
| 1247 | "concurrently": false, | ||
| 1248 | "method": "btree", | ||
| 1249 | "with": {} | ||
| 1250 | }, | ||
| 1251 | "openapi_source_binding_connection_id_idx": { | ||
| 1252 | "name": "openapi_source_binding_connection_id_idx", | ||
| 1253 | "columns": [ | ||
| 1254 | { | ||
| 1255 | "expression": "connection_id", | ||
| 1256 | "isExpression": false, | ||
| 1257 | "asc": true, | ||
| 1258 | "nulls": "last" | ||
| 1259 | } | ||
| 1260 | ], | ||
| 1261 | "isUnique": false, | ||
| 1262 | "concurrently": false, | ||
| 1263 | "method": "btree", | ||
| 1264 | "with": {} | ||
| 1265 | } | ||
| 1266 | }, | ||
| 1267 | "foreignKeys": {}, | ||
| 1268 | "compositePrimaryKeys": {}, | ||
| 1269 | "uniqueConstraints": {}, | ||
| 1270 | "policies": {}, | ||
| 1271 | "checkConstraints": {}, | ||
| 1272 | "isRLSEnabled": false | ||
| 1273 | }, | ||
| 1274 | "public.secret": { | ||
| 1275 | "name": "secret", | ||
| 1276 | "schema": "", | ||
| 1277 | "columns": { | ||
| 1278 | "id": { | ||
| 1279 | "name": "id", | ||
| 1280 | "type": "text", | ||
| 1281 | "primaryKey": false, | ||
| 1282 | "notNull": true | ||
| 1283 | }, | ||
| 1284 | "scope_id": { | ||
| 1285 | "name": "scope_id", | ||
| 1286 | "type": "text", | ||
| 1287 | "primaryKey": false, | ||
| 1288 | "notNull": true | ||
| 1289 | }, | ||
| 1290 | "name": { | ||
| 1291 | "name": "name", | ||
| 1292 | "type": "text", | ||
| 1293 | "primaryKey": false, | ||
| 1294 | "notNull": true | ||
| 1295 | }, | ||
| 1296 | "provider": { | ||
| 1297 | "name": "provider", | ||
| 1298 | "type": "text", | ||
| 1299 | "primaryKey": false, | ||
| 1300 | "notNull": true | ||
| 1301 | }, | ||
| 1302 | "owned_by_connection_id": { | ||
| 1303 | "name": "owned_by_connection_id", | ||
| 1304 | "type": "text", | ||
| 1305 | "primaryKey": false, | ||
| 1306 | "notNull": false | ||
| 1307 | }, | ||
| 1308 | "created_at": { | ||
| 1309 | "name": "created_at", | ||
| 1310 | "type": "timestamp", | ||
| 1311 | "primaryKey": false, | ||
| 1312 | "notNull": true | ||
| 1313 | } | ||
| 1314 | }, | ||
| 1315 | "indexes": { | ||
| 1316 | "secret_scope_id_idx": { | ||
| 1317 | "name": "secret_scope_id_idx", | ||
| 1318 | "columns": [ | ||
| 1319 | { | ||
| 1320 | "expression": "scope_id", | ||
| 1321 | "isExpression": false, | ||
| 1322 | "asc": true, | ||
| 1323 | "nulls": "last" | ||
| 1324 | } | ||
| 1325 | ], | ||
| 1326 | "isUnique": false, | ||
| 1327 | "concurrently": false, | ||
| 1328 | "method": "btree", | ||
| 1329 | "with": {} | ||
| 1330 | }, | ||
| 1331 | "secret_provider_idx": { | ||
| 1332 | "name": "secret_provider_idx", | ||
| 1333 | "columns": [ | ||
| 1334 | { | ||
| 1335 | "expression": "provider", | ||
| 1336 | "isExpression": false, | ||
| 1337 | "asc": true, | ||
| 1338 | "nulls": "last" | ||
| 1339 | } | ||
| 1340 | ], | ||
| 1341 | "isUnique": false, | ||
| 1342 | "concurrently": false, | ||
| 1343 | "method": "btree", | ||
| 1344 | "with": {} | ||
| 1345 | }, | ||
| 1346 | "secret_owned_by_connection_id_idx": { | ||
| 1347 | "name": "secret_owned_by_connection_id_idx", | ||
| 1348 | "columns": [ | ||
| 1349 | { | ||
| 1350 | "expression": "owned_by_connection_id", | ||
| 1351 | "isExpression": false, | ||
| 1352 | "asc": true, | ||
| 1353 | "nulls": "last" | ||
| 1354 | } | ||
| 1355 | ], | ||
| 1356 | "isUnique": false, | ||
| 1357 | "concurrently": false, | ||
| 1358 | "method": "btree", | ||
| 1359 | "with": {} | ||
| 1360 | } | ||
| 1361 | }, | ||
| 1362 | "foreignKeys": {}, | ||
| 1363 | "compositePrimaryKeys": { | ||
| 1364 | "secret_scope_id_id_pk": { | ||
| 1365 | "name": "secret_scope_id_id_pk", | ||
| 1366 | "columns": [ | ||
| 1367 | "scope_id", | ||
| 1368 | "id" | ||
| 1369 | ] | ||
| 1370 | } | ||
| 1371 | }, | ||
| 1372 | "uniqueConstraints": {}, | ||
| 1373 | "policies": {}, | ||
| 1374 | "checkConstraints": {}, | ||
| 1375 | "isRLSEnabled": false | ||
| 1376 | }, | ||
| 1377 | "public.source": { | ||
| 1378 | "name": "source", | ||
| 1379 | "schema": "", | ||
| 1380 | "columns": { | ||
| 1381 | "id": { | ||
| 1382 | "name": "id", | ||
| 1383 | "type": "text", | ||
| 1384 | "primaryKey": false, | ||
| 1385 | "notNull": true | ||
| 1386 | }, | ||
| 1387 | "scope_id": { | ||
| 1388 | "name": "scope_id", | ||
| 1389 | "type": "text", | ||
| 1390 | "primaryKey": false, | ||
| 1391 | "notNull": true | ||
| 1392 | }, | ||
| 1393 | "plugin_id": { | ||
| 1394 | "name": "plugin_id", | ||
| 1395 | "type": "text", | ||
| 1396 | "primaryKey": false, | ||
| 1397 | "notNull": true | ||
| 1398 | }, | ||
| 1399 | "kind": { | ||
| 1400 | "name": "kind", | ||
| 1401 | "type": "text", | ||
| 1402 | "primaryKey": false, | ||
| 1403 | "notNull": true | ||
| 1404 | }, | ||
| 1405 | "name": { | ||
| 1406 | "name": "name", | ||
| 1407 | "type": "text", | ||
| 1408 | "primaryKey": false, | ||
| 1409 | "notNull": true | ||
| 1410 | }, | ||
| 1411 | "url": { | ||
| 1412 | "name": "url", | ||
| 1413 | "type": "text", | ||
| 1414 | "primaryKey": false, | ||
| 1415 | "notNull": false | ||
| 1416 | }, | ||
| 1417 | "can_remove": { | ||
| 1418 | "name": "can_remove", | ||
| 1419 | "type": "boolean", | ||
| 1420 | "primaryKey": false, | ||
| 1421 | "notNull": true, | ||
| 1422 | "default": true | ||
| 1423 | }, | ||
| 1424 | "can_refresh": { | ||
| 1425 | "name": "can_refresh", | ||
| 1426 | "type": "boolean", | ||
| 1427 | "primaryKey": false, | ||
| 1428 | "notNull": true, | ||
| 1429 | "default": false | ||
| 1430 | }, | ||
| 1431 | "can_edit": { | ||
| 1432 | "name": "can_edit", | ||
| 1433 | "type": "boolean", | ||
| 1434 | "primaryKey": false, | ||
| 1435 | "notNull": true, | ||
| 1436 | "default": false | ||
| 1437 | }, | ||
| 1438 | "created_at": { | ||
| 1439 | "name": "created_at", | ||
| 1440 | "type": "timestamp", | ||
| 1441 | "primaryKey": false, | ||
| 1442 | "notNull": true | ||
| 1443 | }, | ||
| 1444 | "updated_at": { | ||
| 1445 | "name": "updated_at", | ||
| 1446 | "type": "timestamp", | ||
| 1447 | "primaryKey": false, | ||
| 1448 | "notNull": true | ||
| 1449 | } | ||
| 1450 | }, | ||
| 1451 | "indexes": { | ||
| 1452 | "source_scope_id_idx": { | ||
| 1453 | "name": "source_scope_id_idx", | ||
| 1454 | "columns": [ | ||
| 1455 | { | ||
| 1456 | "expression": "scope_id", | ||
| 1457 | "isExpression": false, | ||
| 1458 | "asc": true, | ||
| 1459 | "nulls": "last" | ||
| 1460 | } | ||
| 1461 | ], | ||
| 1462 | "isUnique": false, | ||
| 1463 | "concurrently": false, | ||
| 1464 | "method": "btree", | ||
| 1465 | "with": {} | ||
| 1466 | }, | ||
| 1467 | "source_plugin_id_idx": { | ||
| 1468 | "name": "source_plugin_id_idx", | ||
| 1469 | "columns": [ | ||
| 1470 | { | ||
| 1471 | "expression": "plugin_id", | ||
| 1472 | "isExpression": false, | ||
| 1473 | "asc": true, | ||
| 1474 | "nulls": "last" | ||
| 1475 | } | ||
| 1476 | ], | ||
| 1477 | "isUnique": false, | ||
| 1478 | "concurrently": false, | ||
| 1479 | "method": "btree", | ||
| 1480 | "with": {} | ||
| 1481 | } | ||
| 1482 | }, | ||
| 1483 | "foreignKeys": {}, | ||
| 1484 | "compositePrimaryKeys": { | ||
| 1485 | "source_scope_id_id_pk": { | ||
| 1486 | "name": "source_scope_id_id_pk", | ||
| 1487 | "columns": [ | ||
| 1488 | "scope_id", | ||
| 1489 | "id" | ||
| 1490 | ] | ||
| 1491 | } | ||
| 1492 | }, | ||
| 1493 | "uniqueConstraints": {}, | ||
| 1494 | "policies": {}, | ||
| 1495 | "checkConstraints": {}, | ||
| 1496 | "isRLSEnabled": false | ||
| 1497 | }, | ||
| 1498 | "public.tool": { | ||
| 1499 | "name": "tool", | ||
| 1500 | "schema": "", | ||
| 1501 | "columns": { | ||
| 1502 | "id": { | ||
| 1503 | "name": "id", | ||
| 1504 | "type": "text", | ||
| 1505 | "primaryKey": false, | ||
| 1506 | "notNull": true | ||
| 1507 | }, | ||
| 1508 | "scope_id": { | ||
| 1509 | "name": "scope_id", | ||
| 1510 | "type": "text", | ||
| 1511 | "primaryKey": false, | ||
| 1512 | "notNull": true | ||
| 1513 | }, | ||
| 1514 | "source_id": { | ||
| 1515 | "name": "source_id", | ||
| 1516 | "type": "text", | ||
| 1517 | "primaryKey": false, | ||
| 1518 | "notNull": true | ||
| 1519 | }, | ||
| 1520 | "plugin_id": { | ||
| 1521 | "name": "plugin_id", | ||
| 1522 | "type": "text", | ||
| 1523 | "primaryKey": false, | ||
| 1524 | "notNull": true | ||
| 1525 | }, | ||
| 1526 | "name": { | ||
| 1527 | "name": "name", | ||
| 1528 | "type": "text", | ||
| 1529 | "primaryKey": false, | ||
| 1530 | "notNull": true | ||
| 1531 | }, | ||
| 1532 | "description": { | ||
| 1533 | "name": "description", | ||
| 1534 | "type": "text", | ||
| 1535 | "primaryKey": false, | ||
| 1536 | "notNull": true | ||
| 1537 | }, | ||
| 1538 | "input_schema": { | ||
| 1539 | "name": "input_schema", | ||
| 1540 | "type": "jsonb", | ||
| 1541 | "primaryKey": false, | ||
| 1542 | "notNull": false | ||
| 1543 | }, | ||
| 1544 | "output_schema": { | ||
| 1545 | "name": "output_schema", | ||
| 1546 | "type": "jsonb", | ||
| 1547 | "primaryKey": false, | ||
| 1548 | "notNull": false | ||
| 1549 | }, | ||
| 1550 | "created_at": { | ||
| 1551 | "name": "created_at", | ||
| 1552 | "type": "timestamp", | ||
| 1553 | "primaryKey": false, | ||
| 1554 | "notNull": true | ||
| 1555 | }, | ||
| 1556 | "updated_at": { | ||
| 1557 | "name": "updated_at", | ||
| 1558 | "type": "timestamp", | ||
| 1559 | "primaryKey": false, | ||
| 1560 | "notNull": true | ||
| 1561 | } | ||
| 1562 | }, | ||
| 1563 | "indexes": { | ||
| 1564 | "tool_scope_id_idx": { | ||
| 1565 | "name": "tool_scope_id_idx", | ||
| 1566 | "columns": [ | ||
| 1567 | { | ||
| 1568 | "expression": "scope_id", | ||
| 1569 | "isExpression": false, | ||
| 1570 | "asc": true, | ||
| 1571 | "nulls": "last" | ||
| 1572 | } | ||
| 1573 | ], | ||
| 1574 | "isUnique": false, | ||
| 1575 | "concurrently": false, | ||
| 1576 | "method": "btree", | ||
| 1577 | "with": {} | ||
| 1578 | }, | ||
| 1579 | "tool_source_id_idx": { | ||
| 1580 | "name": "tool_source_id_idx", | ||
| 1581 | "columns": [ | ||
| 1582 | { | ||
| 1583 | "expression": "source_id", | ||
| 1584 | "isExpression": false, | ||
| 1585 | "asc": true, | ||
| 1586 | "nulls": "last" | ||
| 1587 | } | ||
| 1588 | ], | ||
| 1589 | "isUnique": false, | ||
| 1590 | "concurrently": false, | ||
| 1591 | "method": "btree", | ||
| 1592 | "with": {} | ||
| 1593 | }, | ||
| 1594 | "tool_plugin_id_idx": { | ||
| 1595 | "name": "tool_plugin_id_idx", | ||
| 1596 | "columns": [ | ||
| 1597 | { | ||
| 1598 | "expression": "plugin_id", | ||
| 1599 | "isExpression": false, | ||
| 1600 | "asc": true, | ||
| 1601 | "nulls": "last" | ||
| 1602 | } | ||
| 1603 | ], | ||
| 1604 | "isUnique": false, | ||
| 1605 | "concurrently": false, | ||
| 1606 | "method": "btree", | ||
| 1607 | "with": {} | ||
| 1608 | } | ||
| 1609 | }, | ||
| 1610 | "foreignKeys": {}, | ||
| 1611 | "compositePrimaryKeys": { | ||
| 1612 | "tool_scope_id_id_pk": { | ||
| 1613 | "name": "tool_scope_id_id_pk", | ||
| 1614 | "columns": [ | ||
| 1615 | "scope_id", | ||
| 1616 | "id" | ||
| 1617 | ] | ||
| 1618 | } | ||
| 1619 | }, | ||
| 1620 | "uniqueConstraints": {}, | ||
| 1621 | "policies": {}, | ||
| 1622 | "checkConstraints": {}, | ||
| 1623 | "isRLSEnabled": false | ||
| 1624 | }, | ||
| 1625 | "public.tool_policy": { | ||
| 1626 | "name": "tool_policy", | ||
| 1627 | "schema": "", | ||
| 1628 | "columns": { | ||
| 1629 | "id": { | ||
| 1630 | "name": "id", | ||
| 1631 | "type": "text", | ||
| 1632 | "primaryKey": false, | ||
| 1633 | "notNull": true | ||
| 1634 | }, | ||
| 1635 | "scope_id": { | ||
| 1636 | "name": "scope_id", | ||
| 1637 | "type": "text", | ||
| 1638 | "primaryKey": false, | ||
| 1639 | "notNull": true | ||
| 1640 | }, | ||
| 1641 | "pattern": { | ||
| 1642 | "name": "pattern", | ||
| 1643 | "type": "text", | ||
| 1644 | "primaryKey": false, | ||
| 1645 | "notNull": true | ||
| 1646 | }, | ||
| 1647 | "action": { | ||
| 1648 | "name": "action", | ||
| 1649 | "type": "text", | ||
| 1650 | "primaryKey": false, | ||
| 1651 | "notNull": true | ||
| 1652 | }, | ||
| 1653 | "position": { | ||
| 1654 | "name": "position", | ||
| 1655 | "type": "text", | ||
| 1656 | "primaryKey": false, | ||
| 1657 | "notNull": true | ||
| 1658 | }, | ||
| 1659 | "created_at": { | ||
| 1660 | "name": "created_at", | ||
| 1661 | "type": "timestamp", | ||
| 1662 | "primaryKey": false, | ||
| 1663 | "notNull": true | ||
| 1664 | }, | ||
| 1665 | "updated_at": { | ||
| 1666 | "name": "updated_at", | ||
| 1667 | "type": "timestamp", | ||
| 1668 | "primaryKey": false, | ||
| 1669 | "notNull": true | ||
| 1670 | } | ||
| 1671 | }, | ||
| 1672 | "indexes": { | ||
| 1673 | "tool_policy_scope_id_position_idx": { | ||
| 1674 | "name": "tool_policy_scope_id_position_idx", | ||
| 1675 | "columns": [ | ||
| 1676 | { | ||
| 1677 | "expression": "scope_id", | ||
| 1678 | "isExpression": false, | ||
| 1679 | "asc": true, | ||
| 1680 | "nulls": "last" | ||
| 1681 | }, | ||
| 1682 | { | ||
| 1683 | "expression": "position", | ||
| 1684 | "isExpression": false, | ||
| 1685 | "asc": true, | ||
| 1686 | "nulls": "last" | ||
| 1687 | } | ||
| 1688 | ], | ||
| 1689 | "isUnique": false, | ||
| 1690 | "concurrently": false, | ||
| 1691 | "method": "btree", | ||
| 1692 | "with": {} | ||
| 1693 | } | ||
| 1694 | }, | ||
| 1695 | "foreignKeys": {}, | ||
| 1696 | "compositePrimaryKeys": { | ||
| 1697 | "tool_policy_scope_id_id_pk": { | ||
| 1698 | "name": "tool_policy_scope_id_id_pk", | ||
| 1699 | "columns": [ | ||
| 1700 | "scope_id", | ||
| 1701 | "id" | ||
| 1702 | ] | ||
| 1703 | } | ||
| 1704 | }, | ||
| 1705 | "uniqueConstraints": {}, | ||
| 1706 | "policies": {}, | ||
| 1707 | "checkConstraints": {}, | ||
| 1708 | "isRLSEnabled": false | ||
| 1709 | }, | ||
| 1710 | "public.workos_vault_metadata": { | ||
| 1711 | "name": "workos_vault_metadata", | ||
| 1712 | "schema": "", | ||
| 1713 | "columns": { | ||
| 1714 | "id": { | ||
| 1715 | "name": "id", | ||
| 1716 | "type": "text", | ||
| 1717 | "primaryKey": false, | ||
| 1718 | "notNull": true | ||
| 1719 | }, | ||
| 1720 | "scope_id": { | ||
| 1721 | "name": "scope_id", | ||
| 1722 | "type": "text", | ||
| 1723 | "primaryKey": false, | ||
| 1724 | "notNull": true | ||
| 1725 | }, | ||
| 1726 | "name": { | ||
| 1727 | "name": "name", | ||
| 1728 | "type": "text", | ||
| 1729 | "primaryKey": false, | ||
| 1730 | "notNull": true | ||
| 1731 | }, | ||
| 1732 | "purpose": { | ||
| 1733 | "name": "purpose", | ||
| 1734 | "type": "text", | ||
| 1735 | "primaryKey": false, | ||
| 1736 | "notNull": false | ||
| 1737 | }, | ||
| 1738 | "created_at": { | ||
| 1739 | "name": "created_at", | ||
| 1740 | "type": "timestamp", | ||
| 1741 | "primaryKey": false, | ||
| 1742 | "notNull": true | ||
| 1743 | } | ||
| 1744 | }, | ||
| 1745 | "indexes": { | ||
| 1746 | "workos_vault_metadata_scope_id_idx": { | ||
| 1747 | "name": "workos_vault_metadata_scope_id_idx", | ||
| 1748 | "columns": [ | ||
| 1749 | { | ||
| 1750 | "expression": "scope_id", | ||
| 1751 | "isExpression": false, | ||
| 1752 | "asc": true, | ||
| 1753 | "nulls": "last" | ||
| 1754 | } | ||
| 1755 | ], | ||
| 1756 | "isUnique": false, | ||
| 1757 | "concurrently": false, | ||
| 1758 | "method": "btree", | ||
| 1759 | "with": {} | ||
| 1760 | } | ||
| 1761 | }, | ||
| 1762 | "foreignKeys": {}, | ||
| 1763 | "compositePrimaryKeys": { | ||
| 1764 | "workos_vault_metadata_scope_id_id_pk": { | ||
| 1765 | "name": "workos_vault_metadata_scope_id_id_pk", | ||
| 1766 | "columns": [ | ||
| 1767 | "scope_id", | ||
| 1768 | "id" | ||
| 1769 | ] | ||
| 1770 | } | ||
| 1771 | }, | ||
| 1772 | "uniqueConstraints": {}, | ||
| 1773 | "policies": {}, | ||
| 1774 | "checkConstraints": {}, | ||
| 1775 | "isRLSEnabled": false | ||
| 1776 | }, | ||
| 1777 | "public.graphql_source_header": { | ||
| 1778 | "name": "graphql_source_header", | ||
| 1779 | "schema": "", | ||
| 1780 | "columns": { | ||
| 1781 | "id": { | ||
| 1782 | "name": "id", | ||
| 1783 | "type": "text", | ||
| 1784 | "primaryKey": false, | ||
| 1785 | "notNull": true | ||
| 1786 | }, | ||
| 1787 | "scope_id": { | ||
| 1788 | "name": "scope_id", | ||
| 1789 | "type": "text", | ||
| 1790 | "primaryKey": false, | ||
| 1791 | "notNull": true | ||
| 1792 | }, | ||
| 1793 | "source_id": { | ||
| 1794 | "name": "source_id", | ||
| 1795 | "type": "text", | ||
| 1796 | "primaryKey": false, | ||
| 1797 | "notNull": true | ||
| 1798 | }, | ||
| 1799 | "name": { | ||
| 1800 | "name": "name", | ||
| 1801 | "type": "text", | ||
| 1802 | "primaryKey": false, | ||
| 1803 | "notNull": true | ||
| 1804 | }, | ||
| 1805 | "kind": { | ||
| 1806 | "name": "kind", | ||
| 1807 | "type": "text", | ||
| 1808 | "primaryKey": false, | ||
| 1809 | "notNull": true | ||
| 1810 | }, | ||
| 1811 | "text_value": { | ||
| 1812 | "name": "text_value", | ||
| 1813 | "type": "text", | ||
| 1814 | "primaryKey": false, | ||
| 1815 | "notNull": false | ||
| 1816 | }, | ||
| 1817 | "secret_id": { | ||
| 1818 | "name": "secret_id", | ||
| 1819 | "type": "text", | ||
| 1820 | "primaryKey": false, | ||
| 1821 | "notNull": false | ||
| 1822 | }, | ||
| 1823 | "secret_prefix": { | ||
| 1824 | "name": "secret_prefix", | ||
| 1825 | "type": "text", | ||
| 1826 | "primaryKey": false, | ||
| 1827 | "notNull": false | ||
| 1828 | } | ||
| 1829 | }, | ||
| 1830 | "indexes": { | ||
| 1831 | "graphql_source_header_scope_id_idx": { | ||
| 1832 | "name": "graphql_source_header_scope_id_idx", | ||
| 1833 | "columns": [ | ||
| 1834 | { | ||
| 1835 | "expression": "scope_id", | ||
| 1836 | "isExpression": false, | ||
| 1837 | "asc": true, | ||
| 1838 | "nulls": "last" | ||
| 1839 | } | ||
| 1840 | ], | ||
| 1841 | "isUnique": false, | ||
| 1842 | "concurrently": false, | ||
| 1843 | "method": "btree", | ||
| 1844 | "with": {} | ||
| 1845 | }, | ||
| 1846 | "graphql_source_header_source_id_idx": { | ||
| 1847 | "name": "graphql_source_header_source_id_idx", | ||
| 1848 | "columns": [ | ||
| 1849 | { | ||
| 1850 | "expression": "source_id", | ||
| 1851 | "isExpression": false, | ||
| 1852 | "asc": true, | ||
| 1853 | "nulls": "last" | ||
| 1854 | } | ||
| 1855 | ], | ||
| 1856 | "isUnique": false, | ||
| 1857 | "concurrently": false, | ||
| 1858 | "method": "btree", | ||
| 1859 | "with": {} | ||
| 1860 | }, | ||
| 1861 | "graphql_source_header_secret_id_idx": { | ||
| 1862 | "name": "graphql_source_header_secret_id_idx", | ||
| 1863 | "columns": [ | ||
| 1864 | { | ||
| 1865 | "expression": "secret_id", | ||
| 1866 | "isExpression": false, | ||
| 1867 | "asc": true, | ||
| 1868 | "nulls": "last" | ||
| 1869 | } | ||
| 1870 | ], | ||
| 1871 | "isUnique": false, | ||
| 1872 | "concurrently": false, | ||
| 1873 | "method": "btree", | ||
| 1874 | "with": {} | ||
| 1875 | } | ||
| 1876 | }, | ||
| 1877 | "foreignKeys": {}, | ||
| 1878 | "compositePrimaryKeys": { | ||
| 1879 | "graphql_source_header_scope_id_id_pk": { | ||
| 1880 | "name": "graphql_source_header_scope_id_id_pk", | ||
| 1881 | "columns": [ | ||
| 1882 | "scope_id", | ||
| 1883 | "id" | ||
| 1884 | ] | ||
| 1885 | } | ||
| 1886 | }, | ||
| 1887 | "uniqueConstraints": {}, | ||
| 1888 | "policies": {}, | ||
| 1889 | "checkConstraints": {}, | ||
| 1890 | "isRLSEnabled": false | ||
| 1891 | }, | ||
| 1892 | "public.graphql_source_query_param": { | ||
| 1893 | "name": "graphql_source_query_param", | ||
| 1894 | "schema": "", | ||
| 1895 | "columns": { | ||
| 1896 | "id": { | ||
| 1897 | "name": "id", | ||
| 1898 | "type": "text", | ||
| 1899 | "primaryKey": false, | ||
| 1900 | "notNull": true | ||
| 1901 | }, | ||
| 1902 | "scope_id": { | ||
| 1903 | "name": "scope_id", | ||
| 1904 | "type": "text", | ||
| 1905 | "primaryKey": false, | ||
| 1906 | "notNull": true | ||
| 1907 | }, | ||
| 1908 | "source_id": { | ||
| 1909 | "name": "source_id", | ||
| 1910 | "type": "text", | ||
| 1911 | "primaryKey": false, | ||
| 1912 | "notNull": true | ||
| 1913 | }, | ||
| 1914 | "name": { | ||
| 1915 | "name": "name", | ||
| 1916 | "type": "text", | ||
| 1917 | "primaryKey": false, | ||
| 1918 | "notNull": true | ||
| 1919 | }, | ||
| 1920 | "kind": { | ||
| 1921 | "name": "kind", | ||
| 1922 | "type": "text", | ||
| 1923 | "primaryKey": false, | ||
| 1924 | "notNull": true | ||
| 1925 | }, | ||
| 1926 | "text_value": { | ||
| 1927 | "name": "text_value", | ||
| 1928 | "type": "text", | ||
| 1929 | "primaryKey": false, | ||
| 1930 | "notNull": false | ||
| 1931 | }, | ||
| 1932 | "secret_id": { | ||
| 1933 | "name": "secret_id", | ||
| 1934 | "type": "text", | ||
| 1935 | "primaryKey": false, | ||
| 1936 | "notNull": false | ||
| 1937 | }, | ||
| 1938 | "secret_prefix": { | ||
| 1939 | "name": "secret_prefix", | ||
| 1940 | "type": "text", | ||
| 1941 | "primaryKey": false, | ||
| 1942 | "notNull": false | ||
| 1943 | } | ||
| 1944 | }, | ||
| 1945 | "indexes": { | ||
| 1946 | "graphql_source_query_param_scope_id_idx": { | ||
| 1947 | "name": "graphql_source_query_param_scope_id_idx", | ||
| 1948 | "columns": [ | ||
| 1949 | { | ||
| 1950 | "expression": "scope_id", | ||
| 1951 | "isExpression": false, | ||
| 1952 | "asc": true, | ||
| 1953 | "nulls": "last" | ||
| 1954 | } | ||
| 1955 | ], | ||
| 1956 | "isUnique": false, | ||
| 1957 | "concurrently": false, | ||
| 1958 | "method": "btree", | ||
| 1959 | "with": {} | ||
| 1960 | }, | ||
| 1961 | "graphql_source_query_param_source_id_idx": { | ||
| 1962 | "name": "graphql_source_query_param_source_id_idx", | ||
| 1963 | "columns": [ | ||
| 1964 | { | ||
| 1965 | "expression": "source_id", | ||
| 1966 | "isExpression": false, | ||
| 1967 | "asc": true, | ||
| 1968 | "nulls": "last" | ||
| 1969 | } | ||
| 1970 | ], | ||
| 1971 | "isUnique": false, | ||
| 1972 | "concurrently": false, | ||
| 1973 | "method": "btree", | ||
| 1974 | "with": {} | ||
| 1975 | }, | ||
| 1976 | "graphql_source_query_param_secret_id_idx": { | ||
| 1977 | "name": "graphql_source_query_param_secret_id_idx", | ||
| 1978 | "columns": [ | ||
| 1979 | { | ||
| 1980 | "expression": "secret_id", | ||
| 1981 | "isExpression": false, | ||
| 1982 | "asc": true, | ||
| 1983 | "nulls": "last" | ||
| 1984 | } | ||
| 1985 | ], | ||
| 1986 | "isUnique": false, | ||
| 1987 | "concurrently": false, | ||
| 1988 | "method": "btree", | ||
| 1989 | "with": {} | ||
| 1990 | } | ||
| 1991 | }, | ||
| 1992 | "foreignKeys": {}, | ||
| 1993 | "compositePrimaryKeys": { | ||
| 1994 | "graphql_source_query_param_scope_id_id_pk": { | ||
| 1995 | "name": "graphql_source_query_param_scope_id_id_pk", | ||
| 1996 | "columns": [ | ||
| 1997 | "scope_id", | ||
| 1998 | "id" | ||
| 1999 | ] | ||
| 2000 | } | ||
| 2001 | }, | ||
| 2002 | "uniqueConstraints": {}, | ||
| 2003 | "policies": {}, | ||
| 2004 | "checkConstraints": {}, | ||
| 2005 | "isRLSEnabled": false | ||
| 2006 | }, | ||
| 2007 | "public.openapi_source_query_param": { | ||
| 2008 | "name": "openapi_source_query_param", | ||
| 2009 | "schema": "", | ||
| 2010 | "columns": { | ||
| 2011 | "id": { | ||
| 2012 | "name": "id", | ||
| 2013 | "type": "text", | ||
| 2014 | "primaryKey": false, | ||
| 2015 | "notNull": true | ||
| 2016 | }, | ||
| 2017 | "scope_id": { | ||
| 2018 | "name": "scope_id", | ||
| 2019 | "type": "text", | ||
| 2020 | "primaryKey": false, | ||
| 2021 | "notNull": true | ||
| 2022 | }, | ||
| 2023 | "source_id": { | ||
| 2024 | "name": "source_id", | ||
| 2025 | "type": "text", | ||
| 2026 | "primaryKey": false, | ||
| 2027 | "notNull": true | ||
| 2028 | }, | ||
| 2029 | "name": { | ||
| 2030 | "name": "name", | ||
| 2031 | "type": "text", | ||
| 2032 | "primaryKey": false, | ||
| 2033 | "notNull": true | ||
| 2034 | }, | ||
| 2035 | "kind": { | ||
| 2036 | "name": "kind", | ||
| 2037 | "type": "text", | ||
| 2038 | "primaryKey": false, | ||
| 2039 | "notNull": true | ||
| 2040 | }, | ||
| 2041 | "text_value": { | ||
| 2042 | "name": "text_value", | ||
| 2043 | "type": "text", | ||
| 2044 | "primaryKey": false, | ||
| 2045 | "notNull": false | ||
| 2046 | }, | ||
| 2047 | "secret_id": { | ||
| 2048 | "name": "secret_id", | ||
| 2049 | "type": "text", | ||
| 2050 | "primaryKey": false, | ||
| 2051 | "notNull": false | ||
| 2052 | }, | ||
| 2053 | "secret_prefix": { | ||
| 2054 | "name": "secret_prefix", | ||
| 2055 | "type": "text", | ||
| 2056 | "primaryKey": false, | ||
| 2057 | "notNull": false | ||
| 2058 | } | ||
| 2059 | }, | ||
| 2060 | "indexes": { | ||
| 2061 | "openapi_source_query_param_scope_id_idx": { | ||
| 2062 | "name": "openapi_source_query_param_scope_id_idx", | ||
| 2063 | "columns": [ | ||
| 2064 | { | ||
| 2065 | "expression": "scope_id", | ||
| 2066 | "isExpression": false, | ||
| 2067 | "asc": true, | ||
| 2068 | "nulls": "last" | ||
| 2069 | } | ||
| 2070 | ], | ||
| 2071 | "isUnique": false, | ||
| 2072 | "concurrently": false, | ||
| 2073 | "method": "btree", | ||
| 2074 | "with": {} | ||
| 2075 | }, | ||
| 2076 | "openapi_source_query_param_source_id_idx": { | ||
| 2077 | "name": "openapi_source_query_param_source_id_idx", | ||
| 2078 | "columns": [ | ||
| 2079 | { | ||
| 2080 | "expression": "source_id", | ||
| 2081 | "isExpression": false, | ||
| 2082 | "asc": true, | ||
| 2083 | "nulls": "last" | ||
| 2084 | } | ||
| 2085 | ], | ||
| 2086 | "isUnique": false, | ||
| 2087 | "concurrently": false, | ||
| 2088 | "method": "btree", | ||
| 2089 | "with": {} | ||
| 2090 | }, | ||
| 2091 | "openapi_source_query_param_secret_id_idx": { | ||
| 2092 | "name": "openapi_source_query_param_secret_id_idx", | ||
| 2093 | "columns": [ | ||
| 2094 | { | ||
| 2095 | "expression": "secret_id", | ||
| 2096 | "isExpression": false, | ||
| 2097 | "asc": true, | ||
| 2098 | "nulls": "last" | ||
| 2099 | } | ||
| 2100 | ], | ||
| 2101 | "isUnique": false, | ||
| 2102 | "concurrently": false, | ||
| 2103 | "method": "btree", | ||
| 2104 | "with": {} | ||
| 2105 | } | ||
| 2106 | }, | ||
| 2107 | "foreignKeys": {}, | ||
| 2108 | "compositePrimaryKeys": { | ||
| 2109 | "openapi_source_query_param_scope_id_id_pk": { | ||
| 2110 | "name": "openapi_source_query_param_scope_id_id_pk", | ||
| 2111 | "columns": [ | ||
| 2112 | "scope_id", | ||
| 2113 | "id" | ||
| 2114 | ] | ||
| 2115 | } | ||
| 2116 | }, | ||
| 2117 | "uniqueConstraints": {}, | ||
| 2118 | "policies": {}, | ||
| 2119 | "checkConstraints": {}, | ||
| 2120 | "isRLSEnabled": false | ||
| 2121 | }, | ||
| 2122 | "public.openapi_source_spec_fetch_header": { | ||
| 2123 | "name": "openapi_source_spec_fetch_header", | ||
| 2124 | "schema": "", | ||
| 2125 | "columns": { | ||
| 2126 | "id": { | ||
| 2127 | "name": "id", | ||
| 2128 | "type": "text", | ||
| 2129 | "primaryKey": false, | ||
| 2130 | "notNull": true | ||
| 2131 | }, | ||
| 2132 | "scope_id": { | ||
| 2133 | "name": "scope_id", | ||
| 2134 | "type": "text", | ||
| 2135 | "primaryKey": false, | ||
| 2136 | "notNull": true | ||
| 2137 | }, | ||
| 2138 | "source_id": { | ||
| 2139 | "name": "source_id", | ||
| 2140 | "type": "text", | ||
| 2141 | "primaryKey": false, | ||
| 2142 | "notNull": true | ||
| 2143 | }, | ||
| 2144 | "name": { | ||
| 2145 | "name": "name", | ||
| 2146 | "type": "text", | ||
| 2147 | "primaryKey": false, | ||
| 2148 | "notNull": true | ||
| 2149 | }, | ||
| 2150 | "kind": { | ||
| 2151 | "name": "kind", | ||
| 2152 | "type": "text", | ||
| 2153 | "primaryKey": false, | ||
| 2154 | "notNull": true | ||
| 2155 | }, | ||
| 2156 | "text_value": { | ||
| 2157 | "name": "text_value", | ||
| 2158 | "type": "text", | ||
| 2159 | "primaryKey": false, | ||
| 2160 | "notNull": false | ||
| 2161 | }, | ||
| 2162 | "secret_id": { | ||
| 2163 | "name": "secret_id", | ||
| 2164 | "type": "text", | ||
| 2165 | "primaryKey": false, | ||
| 2166 | "notNull": false | ||
| 2167 | }, | ||
| 2168 | "secret_prefix": { | ||
| 2169 | "name": "secret_prefix", | ||
| 2170 | "type": "text", | ||
| 2171 | "primaryKey": false, | ||
| 2172 | "notNull": false | ||
| 2173 | } | ||
| 2174 | }, | ||
| 2175 | "indexes": { | ||
| 2176 | "openapi_source_spec_fetch_header_scope_id_idx": { | ||
| 2177 | "name": "openapi_source_spec_fetch_header_scope_id_idx", | ||
| 2178 | "columns": [ | ||
| 2179 | { | ||
| 2180 | "expression": "scope_id", | ||
| 2181 | "isExpression": false, | ||
| 2182 | "asc": true, | ||
| 2183 | "nulls": "last" | ||
| 2184 | } | ||
| 2185 | ], | ||
| 2186 | "isUnique": false, | ||
| 2187 | "concurrently": false, | ||
| 2188 | "method": "btree", | ||
| 2189 | "with": {} | ||
| 2190 | }, | ||
| 2191 | "openapi_source_spec_fetch_header_source_id_idx": { | ||
| 2192 | "name": "openapi_source_spec_fetch_header_source_id_idx", | ||
| 2193 | "columns": [ | ||
| 2194 | { | ||
| 2195 | "expression": "source_id", | ||
| 2196 | "isExpression": false, | ||
| 2197 | "asc": true, | ||
| 2198 | "nulls": "last" | ||
| 2199 | } | ||
| 2200 | ], | ||
| 2201 | "isUnique": false, | ||
| 2202 | "concurrently": false, | ||
| 2203 | "method": "btree", | ||
| 2204 | "with": {} | ||
| 2205 | }, | ||
| 2206 | "openapi_source_spec_fetch_header_secret_id_idx": { | ||
| 2207 | "name": "openapi_source_spec_fetch_header_secret_id_idx", | ||
| 2208 | "columns": [ | ||
| 2209 | { | ||
| 2210 | "expression": "secret_id", | ||
| 2211 | "isExpression": false, | ||
| 2212 | "asc": true, | ||
| 2213 | "nulls": "last" | ||
| 2214 | } | ||
| 2215 | ], | ||
| 2216 | "isUnique": false, | ||
| 2217 | "concurrently": false, | ||
| 2218 | "method": "btree", | ||
| 2219 | "with": {} | ||
| 2220 | } | ||
| 2221 | }, | ||
| 2222 | "foreignKeys": {}, | ||
| 2223 | "compositePrimaryKeys": { | ||
| 2224 | "openapi_source_spec_fetch_header_scope_id_id_pk": { | ||
| 2225 | "name": "openapi_source_spec_fetch_header_scope_id_id_pk", | ||
| 2226 | "columns": [ | ||
| 2227 | "scope_id", | ||
| 2228 | "id" | ||
| 2229 | ] | ||
| 2230 | } | ||
| 2231 | }, | ||
| 2232 | "uniqueConstraints": {}, | ||
| 2233 | "policies": {}, | ||
| 2234 | "checkConstraints": {}, | ||
| 2235 | "isRLSEnabled": false | ||
| 2236 | }, | ||
| 2237 | "public.openapi_source_spec_fetch_query_param": { | ||
| 2238 | "name": "openapi_source_spec_fetch_query_param", | ||
| 2239 | "schema": "", | ||
| 2240 | "columns": { | ||
| 2241 | "id": { | ||
| 2242 | "name": "id", | ||
| 2243 | "type": "text", | ||
| 2244 | "primaryKey": false, | ||
| 2245 | "notNull": true | ||
| 2246 | }, | ||
| 2247 | "scope_id": { | ||
| 2248 | "name": "scope_id", | ||
| 2249 | "type": "text", | ||
| 2250 | "primaryKey": false, | ||
| 2251 | "notNull": true | ||
| 2252 | }, | ||
| 2253 | "source_id": { | ||
| 2254 | "name": "source_id", | ||
| 2255 | "type": "text", | ||
| 2256 | "primaryKey": false, | ||
| 2257 | "notNull": true | ||
| 2258 | }, | ||
| 2259 | "name": { | ||
| 2260 | "name": "name", | ||
| 2261 | "type": "text", | ||
| 2262 | "primaryKey": false, | ||
| 2263 | "notNull": true | ||
| 2264 | }, | ||
| 2265 | "kind": { | ||
| 2266 | "name": "kind", | ||
| 2267 | "type": "text", | ||
| 2268 | "primaryKey": false, | ||
| 2269 | "notNull": true | ||
| 2270 | }, | ||
| 2271 | "text_value": { | ||
| 2272 | "name": "text_value", | ||
| 2273 | "type": "text", | ||
| 2274 | "primaryKey": false, | ||
| 2275 | "notNull": false | ||
| 2276 | }, | ||
| 2277 | "secret_id": { | ||
| 2278 | "name": "secret_id", | ||
| 2279 | "type": "text", | ||
| 2280 | "primaryKey": false, | ||
| 2281 | "notNull": false | ||
| 2282 | }, | ||
| 2283 | "secret_prefix": { | ||
| 2284 | "name": "secret_prefix", | ||
| 2285 | "type": "text", | ||
| 2286 | "primaryKey": false, | ||
| 2287 | "notNull": false | ||
| 2288 | } | ||
| 2289 | }, | ||
| 2290 | "indexes": { | ||
| 2291 | "openapi_source_spec_fetch_query_param_scope_id_idx": { | ||
| 2292 | "name": "openapi_source_spec_fetch_query_param_scope_id_idx", | ||
| 2293 | "columns": [ | ||
| 2294 | { | ||
| 2295 | "expression": "scope_id", | ||
| 2296 | "isExpression": false, | ||
| 2297 | "asc": true, | ||
| 2298 | "nulls": "last" | ||
| 2299 | } | ||
| 2300 | ], | ||
| 2301 | "isUnique": false, | ||
| 2302 | "concurrently": false, | ||
| 2303 | "method": "btree", | ||
| 2304 | "with": {} | ||
| 2305 | }, | ||
| 2306 | "openapi_source_spec_fetch_query_param_source_id_idx": { | ||
| 2307 | "name": "openapi_source_spec_fetch_query_param_source_id_idx", | ||
| 2308 | "columns": [ | ||
| 2309 | { | ||
| 2310 | "expression": "source_id", | ||
| 2311 | "isExpression": false, | ||
| 2312 | "asc": true, | ||
| 2313 | "nulls": "last" | ||
| 2314 | } | ||
| 2315 | ], | ||
| 2316 | "isUnique": false, | ||
| 2317 | "concurrently": false, | ||
| 2318 | "method": "btree", | ||
| 2319 | "with": {} | ||
| 2320 | }, | ||
| 2321 | "openapi_source_spec_fetch_query_param_secret_id_idx": { | ||
| 2322 | "name": "openapi_source_spec_fetch_query_param_secret_id_idx", | ||
| 2323 | "columns": [ | ||
| 2324 | { | ||
| 2325 | "expression": "secret_id", | ||
| 2326 | "isExpression": false, | ||
| 2327 | "asc": true, | ||
| 2328 | "nulls": "last" | ||
| 2329 | } | ||
| 2330 | ], | ||
| 2331 | "isUnique": false, | ||
| 2332 | "concurrently": false, | ||
| 2333 | "method": "btree", | ||
| 2334 | "with": {} | ||
| 2335 | } | ||
| 2336 | }, | ||
| 2337 | "foreignKeys": {}, | ||
| 2338 | "compositePrimaryKeys": { | ||
| 2339 | "openapi_source_spec_fetch_query_param_scope_id_id_pk": { | ||
| 2340 | "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk", | ||
| 2341 | "columns": [ | ||
| 2342 | "scope_id", | ||
| 2343 | "id" | ||
| 2344 | ] | ||
| 2345 | } | ||
| 2346 | }, | ||
| 2347 | "uniqueConstraints": {}, | ||
| 2348 | "policies": {}, | ||
| 2349 | "checkConstraints": {}, | ||
| 2350 | "isRLSEnabled": false | ||
| 2351 | }, | ||
| 2352 | "public.mcp_source_header": { | ||
| 2353 | "name": "mcp_source_header", | ||
| 2354 | "schema": "", | ||
| 2355 | "columns": { | ||
| 2356 | "id": { | ||
| 2357 | "name": "id", | ||
| 2358 | "type": "text", | ||
| 2359 | "primaryKey": false, | ||
| 2360 | "notNull": true | ||
| 2361 | }, | ||
| 2362 | "scope_id": { | ||
| 2363 | "name": "scope_id", | ||
| 2364 | "type": "text", | ||
| 2365 | "primaryKey": false, | ||
| 2366 | "notNull": true | ||
| 2367 | }, | ||
| 2368 | "source_id": { | ||
| 2369 | "name": "source_id", | ||
| 2370 | "type": "text", | ||
| 2371 | "primaryKey": false, | ||
| 2372 | "notNull": true | ||
| 2373 | }, | ||
| 2374 | "name": { | ||
| 2375 | "name": "name", | ||
| 2376 | "type": "text", | ||
| 2377 | "primaryKey": false, | ||
| 2378 | "notNull": true | ||
| 2379 | }, | ||
| 2380 | "kind": { | ||
| 2381 | "name": "kind", | ||
| 2382 | "type": "text", | ||
| 2383 | "primaryKey": false, | ||
| 2384 | "notNull": true | ||
| 2385 | }, | ||
| 2386 | "text_value": { | ||
| 2387 | "name": "text_value", | ||
| 2388 | "type": "text", | ||
| 2389 | "primaryKey": false, | ||
| 2390 | "notNull": false | ||
| 2391 | }, | ||
| 2392 | "secret_id": { | ||
| 2393 | "name": "secret_id", | ||
| 2394 | "type": "text", | ||
| 2395 | "primaryKey": false, | ||
| 2396 | "notNull": false | ||
| 2397 | }, | ||
| 2398 | "secret_prefix": { | ||
| 2399 | "name": "secret_prefix", | ||
| 2400 | "type": "text", | ||
| 2401 | "primaryKey": false, | ||
| 2402 | "notNull": false | ||
| 2403 | } | ||
| 2404 | }, | ||
| 2405 | "indexes": { | ||
| 2406 | "mcp_source_header_scope_id_idx": { | ||
| 2407 | "name": "mcp_source_header_scope_id_idx", | ||
| 2408 | "columns": [ | ||
| 2409 | { | ||
| 2410 | "expression": "scope_id", | ||
| 2411 | "isExpression": false, | ||
| 2412 | "asc": true, | ||
| 2413 | "nulls": "last" | ||
| 2414 | } | ||
| 2415 | ], | ||
| 2416 | "isUnique": false, | ||
| 2417 | "concurrently": false, | ||
| 2418 | "method": "btree", | ||
| 2419 | "with": {} | ||
| 2420 | }, | ||
| 2421 | "mcp_source_header_source_id_idx": { | ||
| 2422 | "name": "mcp_source_header_source_id_idx", | ||
| 2423 | "columns": [ | ||
| 2424 | { | ||
| 2425 | "expression": "source_id", | ||
| 2426 | "isExpression": false, | ||
| 2427 | "asc": true, | ||
| 2428 | "nulls": "last" | ||
| 2429 | } | ||
| 2430 | ], | ||
| 2431 | "isUnique": false, | ||
| 2432 | "concurrently": false, | ||
| 2433 | "method": "btree", | ||
| 2434 | "with": {} | ||
| 2435 | }, | ||
| 2436 | "mcp_source_header_secret_id_idx": { | ||
| 2437 | "name": "mcp_source_header_secret_id_idx", | ||
| 2438 | "columns": [ | ||
| 2439 | { | ||
| 2440 | "expression": "secret_id", | ||
| 2441 | "isExpression": false, | ||
| 2442 | "asc": true, | ||
| 2443 | "nulls": "last" | ||
| 2444 | } | ||
| 2445 | ], | ||
| 2446 | "isUnique": false, | ||
| 2447 | "concurrently": false, | ||
| 2448 | "method": "btree", | ||
| 2449 | "with": {} | ||
| 2450 | } | ||
| 2451 | }, | ||
| 2452 | "foreignKeys": {}, | ||
| 2453 | "compositePrimaryKeys": { | ||
| 2454 | "mcp_source_header_scope_id_id_pk": { | ||
| 2455 | "name": "mcp_source_header_scope_id_id_pk", | ||
| 2456 | "columns": [ | ||
| 2457 | "scope_id", | ||
| 2458 | "id" | ||
| 2459 | ] | ||
| 2460 | } | ||
| 2461 | }, | ||
| 2462 | "uniqueConstraints": {}, | ||
| 2463 | "policies": {}, | ||
| 2464 | "checkConstraints": {}, | ||
| 2465 | "isRLSEnabled": false | ||
| 2466 | }, | ||
| 2467 | "public.mcp_source_query_param": { | ||
| 2468 | "name": "mcp_source_query_param", | ||
| 2469 | "schema": "", | ||
| 2470 | "columns": { | ||
| 2471 | "id": { | ||
| 2472 | "name": "id", | ||
| 2473 | "type": "text", | ||
| 2474 | "primaryKey": false, | ||
| 2475 | "notNull": true | ||
| 2476 | }, | ||
| 2477 | "scope_id": { | ||
| 2478 | "name": "scope_id", | ||
| 2479 | "type": "text", | ||
| 2480 | "primaryKey": false, | ||
| 2481 | "notNull": true | ||
| 2482 | }, | ||
| 2483 | "source_id": { | ||
| 2484 | "name": "source_id", | ||
| 2485 | "type": "text", | ||
| 2486 | "primaryKey": false, | ||
| 2487 | "notNull": true | ||
| 2488 | }, | ||
| 2489 | "name": { | ||
| 2490 | "name": "name", | ||
| 2491 | "type": "text", | ||
| 2492 | "primaryKey": false, | ||
| 2493 | "notNull": true | ||
| 2494 | }, | ||
| 2495 | "kind": { | ||
| 2496 | "name": "kind", | ||
| 2497 | "type": "text", | ||
| 2498 | "primaryKey": false, | ||
| 2499 | "notNull": true | ||
| 2500 | }, | ||
| 2501 | "text_value": { | ||
| 2502 | "name": "text_value", | ||
| 2503 | "type": "text", | ||
| 2504 | "primaryKey": false, | ||
| 2505 | "notNull": false | ||
| 2506 | }, | ||
| 2507 | "secret_id": { | ||
| 2508 | "name": "secret_id", | ||
| 2509 | "type": "text", | ||
| 2510 | "primaryKey": false, | ||
| 2511 | "notNull": false | ||
| 2512 | }, | ||
| 2513 | "secret_prefix": { | ||
| 2514 | "name": "secret_prefix", | ||
| 2515 | "type": "text", | ||
| 2516 | "primaryKey": false, | ||
| 2517 | "notNull": false | ||
| 2518 | } | ||
| 2519 | }, | ||
| 2520 | "indexes": { | ||
| 2521 | "mcp_source_query_param_scope_id_idx": { | ||
| 2522 | "name": "mcp_source_query_param_scope_id_idx", | ||
| 2523 | "columns": [ | ||
| 2524 | { | ||
| 2525 | "expression": "scope_id", | ||
| 2526 | "isExpression": false, | ||
| 2527 | "asc": true, | ||
| 2528 | "nulls": "last" | ||
| 2529 | } | ||
| 2530 | ], | ||
| 2531 | "isUnique": false, | ||
| 2532 | "concurrently": false, | ||
| 2533 | "method": "btree", | ||
| 2534 | "with": {} | ||
| 2535 | }, | ||
| 2536 | "mcp_source_query_param_source_id_idx": { | ||
| 2537 | "name": "mcp_source_query_param_source_id_idx", | ||
| 2538 | "columns": [ | ||
| 2539 | { | ||
| 2540 | "expression": "source_id", | ||
| 2541 | "isExpression": false, | ||
| 2542 | "asc": true, | ||
| 2543 | "nulls": "last" | ||
| 2544 | } | ||
| 2545 | ], | ||
| 2546 | "isUnique": false, | ||
| 2547 | "concurrently": false, | ||
| 2548 | "method": "btree", | ||
| 2549 | "with": {} | ||
| 2550 | }, | ||
| 2551 | "mcp_source_query_param_secret_id_idx": { | ||
| 2552 | "name": "mcp_source_query_param_secret_id_idx", | ||
| 2553 | "columns": [ | ||
| 2554 | { | ||
| 2555 | "expression": "secret_id", | ||
| 2556 | "isExpression": false, | ||
| 2557 | "asc": true, | ||
| 2558 | "nulls": "last" | ||
| 2559 | } | ||
| 2560 | ], | ||
| 2561 | "isUnique": false, | ||
| 2562 | "concurrently": false, | ||
| 2563 | "method": "btree", | ||
| 2564 | "with": {} | ||
| 2565 | } | ||
| 2566 | }, | ||
| 2567 | "foreignKeys": {}, | ||
| 2568 | "compositePrimaryKeys": { | ||
| 2569 | "mcp_source_query_param_scope_id_id_pk": { | ||
| 2570 | "name": "mcp_source_query_param_scope_id_id_pk", | ||
| 2571 | "columns": [ | ||
| 2572 | "scope_id", | ||
| 2573 | "id" | ||
| 2574 | ] | ||
| 2575 | } | ||
| 2576 | }, | ||
| 2577 | "uniqueConstraints": {}, | ||
| 2578 | "policies": {}, | ||
| 2579 | "checkConstraints": {}, | ||
| 2580 | "isRLSEnabled": false | ||
| 2581 | } | ||
| 2582 | }, | ||
| 2583 | "enums": {}, | ||
| 2584 | "schemas": {}, | ||
| 2585 | "sequences": {}, | ||
| 2586 | "roles": {}, | ||
| 2587 | "policies": {}, | ||
| 2588 | "views": {}, | ||
| 2589 | "_meta": { | ||
| 2590 | "columns": {}, | ||
| 2591 | "schemas": {}, | ||
| 2592 | "tables": {} | ||
| 2593 | } | ||
| 2594 | } | ||
| @@ -57,6 +57,13 @@ · 56 unmodified lines | |||
| 57 | "when": 1777567556847, | 57 | "when": 1777567556847, |
| 58 | "tag": "0007_military_young_avengers", | 58 | "tag": "0007_military_young_avengers", |
| 59 | "breakpoints": true | 59 | "breakpoints": true |
| 60 | }, | ||
| 61 | { | ||
| 62 | "idx": 8, | ||
| 63 | "version": "7", | ||
| 64 | "when": 1778004191000, | ||
| 65 | "tag": "0008_normalize_plugin_secret_refs", | ||
| 66 | "breakpoints": true | ||
| 60 | } | 67 | } |
| 61 | ] | 68 | ] |
| 62 | } | 69 | } |
| @@ -122,9 +122,7 @@ export const openapi_source = pgTable("openapi_source", { · 121 unmodified lines | |||
| 122 | source_url: text('source_url'), | 122 | source_url: text('source_url'), |
| 123 | base_url: text('base_url'), | 123 | base_url: text('base_url'), |
| 124 | headers: jsonb('headers'), | 124 | headers: jsonb('headers'), |
| 125 | query_params: jsonb('query_params'), | 125 | oauth2: jsonb('oauth2') |
| 126 | oauth2: jsonb('oauth2'), | ||
| 127 | invocation_config: jsonb('invocation_config').notNull() | ||
| 128 | }, (table) => [ | 126 | }, (table) => [ |
| 129 | primaryKey({ columns: [table.scope_id, table.id] }), | 127 | primaryKey({ columns: [table.scope_id, table.id] }), |
| 130 | index("openapi_source_scope_id_idx").on(table.scope_id), | 128 | index("openapi_source_scope_id_idx").on(table.scope_id), |
| @@ -147,7 +145,10 @@ export const openapi_source_binding = pgTable("openapi_source_binding", { · 16 unmodified lines | |||
| 147 | source_scope_id: text('source_scope_id').notNull(), | 145 | source_scope_id: text('source_scope_id').notNull(), |
| 148 | target_scope_id: text('target_scope_id').notNull(), | 146 | target_scope_id: text('target_scope_id').notNull(), |
| 149 | slot: text('slot').notNull(), | 147 | slot: text('slot').notNull(), |
| 150 | value: jsonb('value').notNull(), | 148 | kind: text('kind', { enum: ['secret', 'connection', 'text'] }).notNull(), |
| 149 | secret_id: text('secret_id'), | ||
| 150 | connection_id: text('connection_id'), | ||
| 151 | text_value: text('text_value'), | ||
| 151 | created_at: timestamp('created_at').notNull(), | 152 | created_at: timestamp('created_at').notNull(), |
| 152 | updated_at: timestamp('updated_at').notNull() | 153 | updated_at: timestamp('updated_at').notNull() |
| 153 | }, (table) => [ | 154 | }, (table) => [ |
| @@ -155,6 +156,56 @@ export const openapi_source_binding = pgTable("openapi_source_binding", { · 1 unmodified line | |||
| 155 | index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id), | 156 | index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id), |
| 156 | index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id), | 157 | index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id), |
| 157 | index("openapi_source_binding_slot_idx").on(table.slot), | 158 | index("openapi_source_binding_slot_idx").on(table.slot), |
| 159 | index("openapi_source_binding_secret_id_idx").on(table.secret_id), | ||
| 160 | index("openapi_source_binding_connection_id_idx").on(table.connection_id), | ||
| 161 | ]); | ||
| 162 | |||
| 163 | export const openapi_source_query_param = pgTable("openapi_source_query_param", { | ||
| 164 | id: text('id').notNull(), | ||
| 165 | scope_id: text('scope_id').notNull(), | ||
| 166 | source_id: text('source_id').notNull(), | ||
| 167 | name: text('name').notNull(), | ||
| 168 | kind: text('kind', { enum: ['text', 'secret'] }).notNull(), | ||
| 169 | text_value: text('text_value'), | ||
| 170 | secret_id: text('secret_id'), | ||
| 171 | secret_prefix: text('secret_prefix') | ||
| 172 | }, (table) => [ | ||
| 173 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 174 | index("openapi_source_query_param_scope_id_idx").on(table.scope_id), | ||
| 175 | index("openapi_source_query_param_source_id_idx").on(table.source_id), | ||
| 176 | index("openapi_source_query_param_secret_id_idx").on(table.secret_id), | ||
| 177 | ]); | ||
| 178 | |||
| 179 | export const openapi_source_spec_fetch_header = pgTable("openapi_source_spec_fetch_header", { | ||
| 180 | id: text('id').notNull(), | ||
| 181 | scope_id: text('scope_id').notNull(), | ||
| 182 | source_id: text('source_id').notNull(), | ||
| 183 | name: text('name').notNull(), | ||
| 184 | kind: text('kind', { enum: ['text', 'secret'] }).notNull(), | ||
| 185 | text_value: text('text_value'), | ||
| 186 | secret_id: text('secret_id'), | ||
| 187 | secret_prefix: text('secret_prefix') | ||
| 188 | }, (table) => [ | ||
| 189 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 190 | index("openapi_source_spec_fetch_header_scope_id_idx").on(table.scope_id), | ||
| 191 | index("openapi_source_spec_fetch_header_source_id_idx").on(table.source_id), | ||
| 192 | index("openapi_source_spec_fetch_header_secret_id_idx").on(table.secret_id), | ||
| 193 | ]); | ||
| 194 | |||
| 195 | export const openapi_source_spec_fetch_query_param = pgTable("openapi_source_spec_fetch_query_param", { | ||
| 196 | id: text('id').notNull(), | ||
| 197 | scope_id: text('scope_id').notNull(), | ||
| 198 | source_id: text('source_id').notNull(), | ||
| 199 | name: text('name').notNull(), | ||
| 200 | kind: text('kind', { enum: ['text', 'secret'] }).notNull(), | ||
| 201 | text_value: text('text_value'), | ||
| 202 | secret_id: text('secret_id'), | ||
| 203 | secret_prefix: text('secret_prefix') | ||
| 204 | }, (table) => [ | ||
| 205 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 206 | index("openapi_source_spec_fetch_query_param_scope_id_idx").on(table.scope_id), | ||
| 207 | index("openapi_source_spec_fetch_query_param_source_id_idx").on(table.source_id), | ||
| 208 | index("openapi_source_spec_fetch_query_param_secret_id_idx").on(table.secret_id), | ||
| 158 | ]); | 209 | ]); |
| 159 | 210 | ||
| 160 | export const mcp_source = pgTable("mcp_source", { | 211 | export const mcp_source = pgTable("mcp_source", { |
| @@ -162,10 +213,53 @@ export const mcp_source = pgTable("mcp_source", { · 1 unmodified line | |||
| 162 | scope_id: text('scope_id').notNull(), | 213 | scope_id: text('scope_id').notNull(), |
| 163 | name: text('name').notNull(), | 214 | name: text('name').notNull(), |
| 164 | config: jsonb('config').notNull(), | 215 | config: jsonb('config').notNull(), |
| 216 | auth_kind: text('auth_kind', { enum: ['none', 'header', 'oauth2'] }).default("none").notNull(), | ||
| 217 | auth_header_name: text('auth_header_name'), | ||
| 218 | auth_secret_id: text('auth_secret_id'), | ||
| 219 | auth_secret_prefix: text('auth_secret_prefix'), | ||
| 220 | auth_connection_id: text('auth_connection_id'), | ||
| 221 | auth_client_id_secret_id: text('auth_client_id_secret_id'), | ||
| 222 | auth_client_secret_secret_id: text('auth_client_secret_secret_id'), | ||
| 165 | created_at: timestamp('created_at').notNull() | 223 | created_at: timestamp('created_at').notNull() |
| 166 | }, (table) => [ | 224 | }, (table) => [ |
| 167 | primaryKey({ columns: [table.scope_id, table.id] }), | 225 | primaryKey({ columns: [table.scope_id, table.id] }), |
| 168 | index("mcp_source_scope_id_idx").on(table.scope_id), | 226 | index("mcp_source_scope_id_idx").on(table.scope_id), |
| 227 | index("mcp_source_auth_secret_id_idx").on(table.auth_secret_id), | ||
| 228 | index("mcp_source_auth_connection_id_idx").on(table.auth_connection_id), | ||
| 229 | index("mcp_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id), | ||
| 230 | index("mcp_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id), | ||
| 231 | ]); | ||
| 232 | |||
| 233 | export const mcp_source_header = pgTable("mcp_source_header", { | ||
| 234 | id: text('id').notNull(), | ||
| 235 | scope_id: text('scope_id').notNull(), | ||
| 236 | source_id: text('source_id').notNull(), | ||
| 237 | name: text('name').notNull(), | ||
| 238 | kind: text('kind', { enum: ['text', 'secret'] }).notNull(), | ||
| 239 | text_value: text('text_value'), | ||
| 240 | secret_id: text('secret_id'), | ||
| 241 | secret_prefix: text('secret_prefix') | ||
| 242 | }, (table) => [ | ||
| 243 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 244 | index("mcp_source_header_scope_id_idx").on(table.scope_id), | ||
| 245 | index("mcp_source_header_source_id_idx").on(table.source_id), | ||
| 246 | index("mcp_source_header_secret_id_idx").on(table.secret_id), | ||
| 247 | ]); | ||
| 248 | |||
| 249 | export const mcp_source_query_param = pgTable("mcp_source_query_param", { | ||
| 250 | id: text('id').notNull(), | ||
| 251 | scope_id: text('scope_id').notNull(), | ||
| 252 | source_id: text('source_id').notNull(), | ||
| 253 | name: text('name').notNull(), | ||
| 254 | kind: text('kind', { enum: ['text', 'secret'] }).notNull(), | ||
| 255 | text_value: text('text_value'), | ||
| 256 | secret_id: text('secret_id'), | ||
| 257 | secret_prefix: text('secret_prefix') | ||
| 258 | }, (table) => [ | ||
| 259 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 260 | index("mcp_source_query_param_scope_id_idx").on(table.scope_id), | ||
| 261 | index("mcp_source_query_param_source_id_idx").on(table.source_id), | ||
| 262 | index("mcp_source_query_param_secret_id_idx").on(table.secret_id), | ||
| 169 | ]); | 263 | ]); |
| 170 | 264 | ||
| 171 | export const mcp_binding = pgTable("mcp_binding", { | 265 | export const mcp_binding = pgTable("mcp_binding", { |
| @@ -185,12 +279,44 @@ export const graphql_source = pgTable("graphql_source", { · 13 unmodified lines | |||
| 185 | scope_id: text('scope_id').notNull(), | 279 | scope_id: text('scope_id').notNull(), |
| 186 | name: text('name').notNull(), | 280 | name: text('name').notNull(), |
| 187 | endpoint: text('endpoint').notNull(), | 281 | endpoint: text('endpoint').notNull(), |
| 188 | headers: jsonb('headers'), | 282 | auth_kind: text('auth_kind', { enum: ['none', 'oauth2'] }).default("none").notNull(), |
| 189 | query_params: jsonb('query_params'), | 283 | auth_connection_id: text('auth_connection_id') |
| 190 | auth: jsonb('auth') | ||
| 191 | }, (table) => [ | 284 | }, (table) => [ |
| 192 | primaryKey({ columns: [table.scope_id, table.id] }), | 285 | primaryKey({ columns: [table.scope_id, table.id] }), |
| 193 | index("graphql_source_scope_id_idx").on(table.scope_id), | 286 | index("graphql_source_scope_id_idx").on(table.scope_id), |
| 287 | index("graphql_source_auth_connection_id_idx").on(table.auth_connection_id), | ||
| 288 | ]); | ||
| 289 | |||
| 290 | export const graphql_source_header = pgTable("graphql_source_header", { | ||
| 291 | id: text('id').notNull(), | ||
| 292 | scope_id: text('scope_id').notNull(), | ||
| 293 | source_id: text('source_id').notNull(), | ||
| 294 | name: text('name').notNull(), | ||
| 295 | kind: text('kind', { enum: ['text', 'secret'] }).notNull(), | ||
| 296 | text_value: text('text_value'), | ||
| 297 | secret_id: text('secret_id'), | ||
| 298 | secret_prefix: text('secret_prefix') | ||
| 299 | }, (table) => [ | ||
| 300 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 301 | index("graphql_source_header_scope_id_idx").on(table.scope_id), | ||
| 302 | index("graphql_source_header_source_id_idx").on(table.source_id), | ||
| 303 | index("graphql_source_header_secret_id_idx").on(table.secret_id), | ||
| 304 | ]); | ||
| 305 | |||
| 306 | export const graphql_source_query_param = pgTable("graphql_source_query_param", { | ||
| 307 | id: text('id').notNull(), | ||
| 308 | scope_id: text('scope_id').notNull(), | ||
| 309 | source_id: text('source_id').notNull(), | ||
| 310 | name: text('name').notNull(), | ||
| 311 | kind: text('kind', { enum: ['text', 'secret'] }).notNull(), | ||
| 312 | text_value: text('text_value'), | ||
| 313 | secret_id: text('secret_id'), | ||
| 314 | secret_prefix: text('secret_prefix') | ||
| 315 | }, (table) => [ | ||
| 316 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 317 | index("graphql_source_query_param_scope_id_idx").on(table.scope_id), | ||
| 318 | index("graphql_source_query_param_source_id_idx").on(table.source_id), | ||
| 319 | index("graphql_source_query_param_secret_id_idx").on(table.secret_id), | ||
| 194 | ]); | 320 | ]); |
| 195 | 321 | ||
| 196 | export const graphql_operation = pgTable("graphql_operation", { | 322 | export const graphql_operation = pgTable("graphql_operation", { |
| @@ -0,0 +1,579 @@ | |||
| 1 | -- Normalize all plugin secret/connection refs out of JSON columns | ||
| 2 | -- into proper relational shape: graphql, openapi, mcp, | ||
| 3 | -- google-discovery. Each block is a self-contained backfill of one | ||
| 4 | -- plugin's tables; collected here so the schema lands in one | ||
| 5 | -- migration step instead of four sequential ones. | ||
| 6 | -- | ||
| 7 | -- Old shape (per plugin): | ||
| 8 | -- {plugin}_source.{auth, headers, query_params, ...}: json blobs | ||
| 9 | -- openapi_source_binding.value: json union | ||
| 10 | -- {plugin}_source.config.{auth, headers, queryParams}: nested json | ||
| 11 | -- | ||
| 12 | -- New shape (per plugin): | ||
| 13 | -- Flat auth_* columns on the parent (auth_kind enum + per-kind id | ||
| 14 | -- columns, all indexed where they hold refs). | ||
| 15 | -- Child tables for SecretBackedMap entries: one row per (source, | ||
| 16 | -- header_name) keyed by JSON tuple [source_id,name] so user keys | ||
| 17 | -- with separators don't collide. `secret_id` indexed for the | ||
| 18 | -- usagesForSecret query. | ||
| 19 | |||
| 20 | -- ============================================================ | ||
| 21 | -- graphql | ||
| 22 | -- ============================================================ | ||
| 23 | |||
| 24 | -- Normalize graphql plugin: move secret/connection refs out of JSON | ||
| 25 | -- columns into proper relational shape so usagesForSecret / | ||
| 26 | -- usagesForConnection are one indexed SELECT instead of a JSON scan. | ||
| 27 | -- | ||
| 28 | -- Old shape: | ||
| 29 | -- graphql_source.headers json Record<name, string | {secretId,prefix?}> | ||
| 30 | -- graphql_source.query_params json Record<name, string | {secretId,prefix?}> | ||
| 31 | -- graphql_source.auth json {kind:"none"} | {kind:"oauth2", connectionId} | ||
| 32 | -- | ||
| 33 | -- New shape: | ||
| 34 | -- graphql_source.auth_kind enum("none","oauth2") NOT NULL | ||
| 35 | -- graphql_source.auth_connection_id text indexed nullable | ||
| 36 | -- graphql_source_header(scope_id, id, source_id, name, kind, text_value, secret_id, secret_prefix) | ||
| 37 | -- graphql_source_query_param(scope_id, id, source_id, name, kind, text_value, secret_id, secret_prefix) | ||
| 38 | |||
| 39 | CREATE TABLE `graphql_source_header` ( | ||
| 40 | `id` text NOT NULL, | ||
| 41 | `scope_id` text NOT NULL, | ||
| 42 | `source_id` text NOT NULL, | ||
| 43 | `name` text NOT NULL, | ||
| 44 | `kind` text NOT NULL, | ||
| 45 | `text_value` text, | ||
| 46 | `secret_id` text, | ||
| 47 | `secret_prefix` text, | ||
| 48 | PRIMARY KEY(`scope_id`, `id`) | ||
| 49 | ); | ||
| 50 | --> statement-breakpoint | ||
| 51 | CREATE INDEX `graphql_source_header_scope_id_idx` ON `graphql_source_header` (`scope_id`);--> statement-breakpoint | ||
| 52 | CREATE INDEX `graphql_source_header_source_id_idx` ON `graphql_source_header` (`source_id`);--> statement-breakpoint | ||
| 53 | CREATE INDEX `graphql_source_header_secret_id_idx` ON `graphql_source_header` (`secret_id`);--> statement-breakpoint | ||
| 54 | |||
| 55 | CREATE TABLE `graphql_source_query_param` ( | ||
| 56 | `id` text NOT NULL, | ||
| 57 | `scope_id` text NOT NULL, | ||
| 58 | `source_id` text NOT NULL, | ||
| 59 | `name` text NOT NULL, | ||
| 60 | `kind` text NOT NULL, | ||
| 61 | `text_value` text, | ||
| 62 | `secret_id` text, | ||
| 63 | `secret_prefix` text, | ||
| 64 | PRIMARY KEY(`scope_id`, `id`) | ||
| 65 | ); | ||
| 66 | --> statement-breakpoint | ||
| 67 | CREATE INDEX `graphql_source_query_param_scope_id_idx` ON `graphql_source_query_param` (`scope_id`);--> statement-breakpoint | ||
| 68 | CREATE INDEX `graphql_source_query_param_source_id_idx` ON `graphql_source_query_param` (`source_id`);--> statement-breakpoint | ||
| 69 | CREATE INDEX `graphql_source_query_param_secret_id_idx` ON `graphql_source_query_param` (`secret_id`);--> statement-breakpoint | ||
| 70 | |||
| 71 | -- New auth columns. `auth_kind` defaults to "none" so existing rows that | ||
| 72 | -- predate this migration are valid even if the json was null. | ||
| 73 | ALTER TABLE `graphql_source` ADD `auth_kind` text DEFAULT 'none' NOT NULL;--> statement-breakpoint | ||
| 74 | ALTER TABLE `graphql_source` ADD `auth_connection_id` text;--> statement-breakpoint | ||
| 75 | CREATE INDEX `graphql_source_auth_connection_id_idx` ON `graphql_source` (`auth_connection_id`);--> statement-breakpoint | ||
| 76 | |||
| 77 | -- Backfill auth from the JSON column. json_extract returns NULL for | ||
| 78 | -- missing paths, so a row with auth=NULL or kind="none" leaves | ||
| 79 | -- auth_connection_id NULL and auth_kind defaulted to "none". | ||
| 80 | UPDATE `graphql_source` | ||
| 81 | SET | ||
| 82 | `auth_kind` = COALESCE(json_extract(`auth`, '$.kind'), 'none'), | ||
| 83 | `auth_connection_id` = json_extract(`auth`, '$.connectionId') | ||
| 84 | WHERE `auth` IS NOT NULL;--> statement-breakpoint | ||
| 85 | |||
| 86 | -- Backfill headers. For each (source, header_name) pair: if the value | ||
| 87 | -- is a json object with .secretId, write a kind=secret row; otherwise | ||
| 88 | -- write a kind=text row with the literal string. json_each iterates | ||
| 89 | -- the keys of the headers object. | ||
| 90 | INSERT OR IGNORE INTO `graphql_source_header` | ||
| 91 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 92 | SELECT | ||
| 93 | s.`scope_id`, | ||
| 94 | json_array(s.`id`, h.`key`), | ||
| 95 | s.`id`, | ||
| 96 | h.`key`, | ||
| 97 | CASE | ||
| 98 | WHEN h.`type` = 'object' AND json_extract(h.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 99 | ELSE 'text' | ||
| 100 | END, | ||
| 101 | CASE WHEN h.`type` = 'object' THEN NULL ELSE h.`value` END, | ||
| 102 | CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.secretId') ELSE NULL END, | ||
| 103 | CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.prefix') ELSE NULL END | ||
| 104 | FROM `graphql_source` s, json_each(s.`headers`) h | ||
| 105 | WHERE s.`headers` IS NOT NULL;--> statement-breakpoint | ||
| 106 | |||
| 107 | -- Same for query_params. | ||
| 108 | INSERT OR IGNORE INTO `graphql_source_query_param` | ||
| 109 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 110 | SELECT | ||
| 111 | s.`scope_id`, | ||
| 112 | json_array(s.`id`, q.`key`), | ||
| 113 | s.`id`, | ||
| 114 | q.`key`, | ||
| 115 | CASE | ||
| 116 | WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 117 | ELSE 'text' | ||
| 118 | END, | ||
| 119 | CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, | ||
| 120 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, | ||
| 121 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END | ||
| 122 | FROM `graphql_source` s, json_each(s.`query_params`) q | ||
| 123 | WHERE s.`query_params` IS NOT NULL;--> statement-breakpoint | ||
| 124 | |||
| 125 | -- Drop the old JSON columns. SQLite ≥ 3.35 supports ALTER TABLE DROP | ||
| 126 | -- COLUMN directly; bun's bundled SQLite is well past that. | ||
| 127 | ALTER TABLE `graphql_source` DROP COLUMN `headers`;--> statement-breakpoint | ||
| 128 | ALTER TABLE `graphql_source` DROP COLUMN `query_params`;--> statement-breakpoint | ||
| 129 | ALTER TABLE `graphql_source` DROP COLUMN `auth`; | ||
| 130 | |||
| 131 | --> statement-breakpoint | ||
| 132 | |||
| 133 | -- ============================================================ | ||
| 134 | -- openapi | ||
| 135 | -- ============================================================ | ||
| 136 | |||
| 137 | -- Normalize openapi plugin: move every direct secret/connection ref out | ||
| 138 | -- of JSON columns into proper relational shape. | ||
| 139 | -- | ||
| 140 | -- Old shape: | ||
| 141 | -- openapi_source.query_params json Record<name, string | {secretId,prefix?}> | ||
| 142 | -- openapi_source.invocation_config json { specFetchCredentials?: { headers, queryParams } } | ||
| 143 | -- openapi_source_binding.value json discriminated union | ||
| 144 | -- {kind:"secret",secretId} | {kind:"connection",connectionId} | {kind:"text",text} | ||
| 145 | -- | ||
| 146 | -- New shape: | ||
| 147 | -- openapi_source_binding gains kind/secret_id/connection_id/text_value columns. | ||
| 148 | -- `headers` / `oauth2` on openapi_source stay JSON because they hold | ||
| 149 | -- slot names, not direct refs — the actual credentials reach those | ||
| 150 | -- slots through openapi_source_binding rows, which ARE normalized. | ||
| 151 | -- openapi_source_query_param: child table, secret-backed entries. | ||
| 152 | -- openapi_source_spec_fetch_header / spec_fetch_query_param: child | ||
| 153 | -- tables for the equivalent maps inside specFetchCredentials. | ||
| 154 | |||
| 155 | CREATE TABLE `openapi_source_query_param` ( | ||
| 156 | `id` text NOT NULL, | ||
| 157 | `scope_id` text NOT NULL, | ||
| 158 | `source_id` text NOT NULL, | ||
| 159 | `name` text NOT NULL, | ||
| 160 | `kind` text NOT NULL, | ||
| 161 | `text_value` text, | ||
| 162 | `secret_id` text, | ||
| 163 | `secret_prefix` text, | ||
| 164 | PRIMARY KEY(`scope_id`, `id`) | ||
| 165 | ); | ||
| 166 | --> statement-breakpoint | ||
| 167 | CREATE INDEX `openapi_source_query_param_scope_id_idx` ON `openapi_source_query_param` (`scope_id`);--> statement-breakpoint | ||
| 168 | CREATE INDEX `openapi_source_query_param_source_id_idx` ON `openapi_source_query_param` (`source_id`);--> statement-breakpoint | ||
| 169 | CREATE INDEX `openapi_source_query_param_secret_id_idx` ON `openapi_source_query_param` (`secret_id`);--> statement-breakpoint | ||
| 170 | |||
| 171 | CREATE TABLE `openapi_source_spec_fetch_header` ( | ||
| 172 | `id` text NOT NULL, | ||
| 173 | `scope_id` text NOT NULL, | ||
| 174 | `source_id` text NOT NULL, | ||
| 175 | `name` text NOT NULL, | ||
| 176 | `kind` text NOT NULL, | ||
| 177 | `text_value` text, | ||
| 178 | `secret_id` text, | ||
| 179 | `secret_prefix` text, | ||
| 180 | PRIMARY KEY(`scope_id`, `id`) | ||
| 181 | ); | ||
| 182 | --> statement-breakpoint | ||
| 183 | CREATE INDEX `openapi_source_spec_fetch_header_scope_id_idx` ON `openapi_source_spec_fetch_header` (`scope_id`);--> statement-breakpoint | ||
| 184 | CREATE INDEX `openapi_source_spec_fetch_header_source_id_idx` ON `openapi_source_spec_fetch_header` (`source_id`);--> statement-breakpoint | ||
| 185 | CREATE INDEX `openapi_source_spec_fetch_header_secret_id_idx` ON `openapi_source_spec_fetch_header` (`secret_id`);--> statement-breakpoint | ||
| 186 | |||
| 187 | CREATE TABLE `openapi_source_spec_fetch_query_param` ( | ||
| 188 | `id` text NOT NULL, | ||
| 189 | `scope_id` text NOT NULL, | ||
| 190 | `source_id` text NOT NULL, | ||
| 191 | `name` text NOT NULL, | ||
| 192 | `kind` text NOT NULL, | ||
| 193 | `text_value` text, | ||
| 194 | `secret_id` text, | ||
| 195 | `secret_prefix` text, | ||
| 196 | PRIMARY KEY(`scope_id`, `id`) | ||
| 197 | ); | ||
| 198 | --> statement-breakpoint | ||
| 199 | CREATE INDEX `openapi_source_spec_fetch_query_param_scope_id_idx` ON `openapi_source_spec_fetch_query_param` (`scope_id`);--> statement-breakpoint | ||
| 200 | CREATE INDEX `openapi_source_spec_fetch_query_param_source_id_idx` ON `openapi_source_spec_fetch_query_param` (`source_id`);--> statement-breakpoint | ||
| 201 | CREATE INDEX `openapi_source_spec_fetch_query_param_secret_id_idx` ON `openapi_source_spec_fetch_query_param` (`secret_id`);--> statement-breakpoint | ||
| 202 | |||
| 203 | -- New columns on openapi_source_binding to flatten the value json. | ||
| 204 | -- `kind` defaults to 'text' so the ALTER works on existing rows; the | ||
| 205 | -- backfill below stamps the real value. | ||
| 206 | ALTER TABLE `openapi_source_binding` ADD `kind` text DEFAULT 'text' NOT NULL;--> statement-breakpoint | ||
| 207 | ALTER TABLE `openapi_source_binding` ADD `secret_id` text;--> statement-breakpoint | ||
| 208 | ALTER TABLE `openapi_source_binding` ADD `connection_id` text;--> statement-breakpoint | ||
| 209 | ALTER TABLE `openapi_source_binding` ADD `text_value` text;--> statement-breakpoint | ||
| 210 | CREATE INDEX `openapi_source_binding_secret_id_idx` ON `openapi_source_binding` (`secret_id`);--> statement-breakpoint | ||
| 211 | CREATE INDEX `openapi_source_binding_connection_id_idx` ON `openapi_source_binding` (`connection_id`);--> statement-breakpoint | ||
| 212 | |||
| 213 | -- Backfill the binding columns from the legacy `value` JSON. We pull | ||
| 214 | -- $.kind into `kind` directly; for each kind the matching id field | ||
| 215 | -- (`secretId` / `connectionId` / `text`) gets copied into the matching | ||
| 216 | -- column. Rows whose value JSON is malformed or missing $.kind fall | ||
| 217 | -- through to kind='text' with a NULL text_value — same as a missing | ||
| 218 | -- text binding, the source will surface "binding not configured" at | ||
| 219 | -- invoke time rather than crashing the migration. | ||
| 220 | UPDATE `openapi_source_binding` | ||
| 221 | SET | ||
| 222 | `kind` = COALESCE(json_extract(`value`, '$.kind'), 'text'), | ||
| 223 | `secret_id` = CASE WHEN json_extract(`value`, '$.kind') = 'secret' THEN json_extract(`value`, '$.secretId') ELSE NULL END, | ||
| 224 | `connection_id` = CASE WHEN json_extract(`value`, '$.kind') = 'connection' THEN json_extract(`value`, '$.connectionId') ELSE NULL END, | ||
| 225 | `text_value` = CASE WHEN json_extract(`value`, '$.kind') = 'text' THEN json_extract(`value`, '$.text') ELSE NULL END | ||
| 226 | WHERE `value` IS NOT NULL;--> statement-breakpoint | ||
| 227 | |||
| 228 | -- Backfill openapi_source_query_param from openapi_source.query_params. | ||
| 229 | -- json_each iterates the keys of the query_params object. For each | ||
| 230 | -- entry: if the value is an object with .secretId, write a kind=secret | ||
| 231 | -- row; otherwise write a kind=text row with the literal string. | ||
| 232 | INSERT OR IGNORE INTO `openapi_source_query_param` | ||
| 233 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 234 | SELECT | ||
| 235 | s.`scope_id`, | ||
| 236 | json_array(s.`id`, q.`key`), | ||
| 237 | s.`id`, | ||
| 238 | q.`key`, | ||
| 239 | CASE | ||
| 240 | WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 241 | ELSE 'text' | ||
| 242 | END, | ||
| 243 | CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, | ||
| 244 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, | ||
| 245 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END | ||
| 246 | FROM `openapi_source` s, json_each(s.`query_params`) q | ||
| 247 | WHERE s.`query_params` IS NOT NULL;--> statement-breakpoint | ||
| 248 | |||
| 249 | -- Backfill openapi_source_spec_fetch_header from | ||
| 250 | -- openapi_source.invocation_config.specFetchCredentials.headers. Same | ||
| 251 | -- shape as query_params; the JSON path is one level deeper. | ||
| 252 | INSERT OR IGNORE INTO `openapi_source_spec_fetch_header` | ||
| 253 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 254 | SELECT | ||
| 255 | s.`scope_id`, | ||
| 256 | json_array(s.`id`, h.`key`), | ||
| 257 | s.`id`, | ||
| 258 | h.`key`, | ||
| 259 | CASE | ||
| 260 | WHEN h.`type` = 'object' AND json_extract(h.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 261 | ELSE 'text' | ||
| 262 | END, | ||
| 263 | CASE WHEN h.`type` = 'object' THEN NULL ELSE h.`value` END, | ||
| 264 | CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.secretId') ELSE NULL END, | ||
| 265 | CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.prefix') ELSE NULL END | ||
| 266 | FROM `openapi_source` s, json_each(json_extract(s.`invocation_config`, '$.specFetchCredentials.headers')) h | ||
| 267 | WHERE json_extract(s.`invocation_config`, '$.specFetchCredentials.headers') IS NOT NULL;--> statement-breakpoint | ||
| 268 | |||
| 269 | INSERT OR IGNORE INTO `openapi_source_spec_fetch_query_param` | ||
| 270 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 271 | SELECT | ||
| 272 | s.`scope_id`, | ||
| 273 | json_array(s.`id`, q.`key`), | ||
| 274 | s.`id`, | ||
| 275 | q.`key`, | ||
| 276 | CASE | ||
| 277 | WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 278 | ELSE 'text' | ||
| 279 | END, | ||
| 280 | CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, | ||
| 281 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, | ||
| 282 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END | ||
| 283 | FROM `openapi_source` s, json_each(json_extract(s.`invocation_config`, '$.specFetchCredentials.queryParams')) q | ||
| 284 | WHERE json_extract(s.`invocation_config`, '$.specFetchCredentials.queryParams') IS NOT NULL;--> statement-breakpoint | ||
| 285 | |||
| 286 | -- Preserve any legacy OAuth payload from invocation_config.oauth2 into | ||
| 287 | -- the still-existing oauth2 column before we drop invocation_config. | ||
| 288 | -- migrateLegacyConnections runs after drizzle migrations and reads | ||
| 289 | -- oauth2 to detect the legacy shape; without this, rows that only had | ||
| 290 | -- their OAuth payload under invocation_config.oauth2 would lose it. | ||
| 291 | UPDATE `openapi_source` | ||
| 292 | SET `oauth2` = json_extract(`invocation_config`, '$.oauth2') | ||
| 293 | WHERE `oauth2` IS NULL | ||
| 294 | AND json_extract(`invocation_config`, '$.oauth2') IS NOT NULL;--> statement-breakpoint | ||
| 295 | |||
| 296 | -- Drop the legacy JSON columns now that everything is normalized. | ||
| 297 | ALTER TABLE `openapi_source_binding` DROP COLUMN `value`;--> statement-breakpoint | ||
| 298 | ALTER TABLE `openapi_source` DROP COLUMN `query_params`;--> statement-breakpoint | ||
| 299 | ALTER TABLE `openapi_source` DROP COLUMN `invocation_config`; | ||
| 300 | |||
| 301 | --> statement-breakpoint | ||
| 302 | |||
| 303 | -- ============================================================ | ||
| 304 | -- mcp | ||
| 305 | -- ============================================================ | ||
| 306 | |||
| 307 | -- Normalize mcp plugin: lift the McpConnectionAuth secret/connection | ||
| 308 | -- refs and the SecretBackedMap headers/query_params out of | ||
| 309 | -- mcp_source.config JSON into proper columns / child tables. | ||
| 310 | -- | ||
| 311 | -- Old shape: | ||
| 312 | -- mcp_source.config (json) — McpStoredSourceData discriminated union | ||
| 313 | -- remote: { transport, endpoint, remoteTransport?, queryParams?, | ||
| 314 | -- headers?, auth: McpConnectionAuth } | ||
| 315 | -- stdio: { transport, command, args?, env?, cwd? } | ||
| 316 | -- | ||
| 317 | -- New shape: | ||
| 318 | -- mcp_source gains: auth_kind enum, auth_header_name, auth_secret_id, | ||
| 319 | -- auth_secret_prefix, auth_connection_id, auth_client_id_secret_id, | ||
| 320 | -- auth_client_secret_secret_id. The remaining structural fields | ||
| 321 | -- stay in `config` as JSON because they're plugin-private and | ||
| 322 | -- vary by transport. | ||
| 323 | -- mcp_source_header / mcp_source_query_param: child tables for | ||
| 324 | -- remote sources' SecretBackedMap entries (same column shape as | ||
| 325 | -- graphql_source_header / openapi_source_query_param). | ||
| 326 | |||
| 327 | CREATE TABLE `mcp_source_header` ( | ||
| 328 | `id` text NOT NULL, | ||
| 329 | `scope_id` text NOT NULL, | ||
| 330 | `source_id` text NOT NULL, | ||
| 331 | `name` text NOT NULL, | ||
| 332 | `kind` text NOT NULL, | ||
| 333 | `text_value` text, | ||
| 334 | `secret_id` text, | ||
| 335 | `secret_prefix` text, | ||
| 336 | PRIMARY KEY(`scope_id`, `id`) | ||
| 337 | ); | ||
| 338 | --> statement-breakpoint | ||
| 339 | CREATE INDEX `mcp_source_header_scope_id_idx` ON `mcp_source_header` (`scope_id`);--> statement-breakpoint | ||
| 340 | CREATE INDEX `mcp_source_header_source_id_idx` ON `mcp_source_header` (`source_id`);--> statement-breakpoint | ||
| 341 | CREATE INDEX `mcp_source_header_secret_id_idx` ON `mcp_source_header` (`secret_id`);--> statement-breakpoint | ||
| 342 | |||
| 343 | CREATE TABLE `mcp_source_query_param` ( | ||
| 344 | `id` text NOT NULL, | ||
| 345 | `scope_id` text NOT NULL, | ||
| 346 | `source_id` text NOT NULL, | ||
| 347 | `name` text NOT NULL, | ||
| 348 | `kind` text NOT NULL, | ||
| 349 | `text_value` text, | ||
| 350 | `secret_id` text, | ||
| 351 | `secret_prefix` text, | ||
| 352 | PRIMARY KEY(`scope_id`, `id`) | ||
| 353 | ); | ||
| 354 | --> statement-breakpoint | ||
| 355 | CREATE INDEX `mcp_source_query_param_scope_id_idx` ON `mcp_source_query_param` (`scope_id`);--> statement-breakpoint | ||
| 356 | CREATE INDEX `mcp_source_query_param_source_id_idx` ON `mcp_source_query_param` (`source_id`);--> statement-breakpoint | ||
| 357 | CREATE INDEX `mcp_source_query_param_secret_id_idx` ON `mcp_source_query_param` (`secret_id`);--> statement-breakpoint | ||
| 358 | |||
| 359 | -- New auth columns. `auth_kind` defaults to "none" so the ALTER passes | ||
| 360 | -- on existing rows; the backfill below stamps the real value. | ||
| 361 | ALTER TABLE `mcp_source` ADD `auth_kind` text DEFAULT 'none' NOT NULL;--> statement-breakpoint | ||
| 362 | ALTER TABLE `mcp_source` ADD `auth_header_name` text;--> statement-breakpoint | ||
| 363 | ALTER TABLE `mcp_source` ADD `auth_secret_id` text;--> statement-breakpoint | ||
| 364 | ALTER TABLE `mcp_source` ADD `auth_secret_prefix` text;--> statement-breakpoint | ||
| 365 | ALTER TABLE `mcp_source` ADD `auth_connection_id` text;--> statement-breakpoint | ||
| 366 | ALTER TABLE `mcp_source` ADD `auth_client_id_secret_id` text;--> statement-breakpoint | ||
| 367 | ALTER TABLE `mcp_source` ADD `auth_client_secret_secret_id` text;--> statement-breakpoint | ||
| 368 | CREATE INDEX `mcp_source_auth_secret_id_idx` ON `mcp_source` (`auth_secret_id`);--> statement-breakpoint | ||
| 369 | CREATE INDEX `mcp_source_auth_connection_id_idx` ON `mcp_source` (`auth_connection_id`);--> statement-breakpoint | ||
| 370 | CREATE INDEX `mcp_source_auth_client_id_secret_id_idx` ON `mcp_source` (`auth_client_id_secret_id`);--> statement-breakpoint | ||
| 371 | CREATE INDEX `mcp_source_auth_client_secret_secret_id_idx` ON `mcp_source` (`auth_client_secret_secret_id`);--> statement-breakpoint | ||
| 372 | |||
| 373 | -- Backfill auth columns from config.auth — but only for rows whose | ||
| 374 | -- config.auth matches the *current* shape: | ||
| 375 | -- - kind=none (no extra fields) | ||
| 376 | -- - kind=header (secretId present) | ||
| 377 | -- - kind=oauth2 (connectionId present) | ||
| 378 | -- Truly-legacy rows (inline OAuth shape with accessTokenSecretId etc.) | ||
| 379 | -- are left untouched here so the post-migrate `migrateLegacyConnections` | ||
| 380 | -- script can convert them to a Connection and write the resulting | ||
| 381 | -- pointer to these columns. Setting auth_kind explicitly to NULL/none | ||
| 382 | -- on those rows would lose the legacy payload before it gets converted. | ||
| 383 | UPDATE `mcp_source` | ||
| 384 | SET | ||
| 385 | `auth_kind` = json_extract(`config`, '$.auth.kind'), | ||
| 386 | `auth_header_name` = json_extract(`config`, '$.auth.headerName'), | ||
| 387 | `auth_secret_id` = json_extract(`config`, '$.auth.secretId'), | ||
| 388 | `auth_secret_prefix` = json_extract(`config`, '$.auth.prefix'), | ||
| 389 | `auth_connection_id` = json_extract(`config`, '$.auth.connectionId'), | ||
| 390 | `auth_client_id_secret_id` = json_extract(`config`, '$.auth.clientIdSecretId'), | ||
| 391 | `auth_client_secret_secret_id` = json_extract(`config`, '$.auth.clientSecretSecretId') | ||
| 392 | WHERE `config` IS NOT NULL | ||
| 393 | AND ( | ||
| 394 | -- kind=none and "no auth at all" both leave auth_kind defaulted to | ||
| 395 | -- 'none' (the column DEFAULT), so we only UPDATE rows that have a | ||
| 396 | -- non-trivial current-shape auth payload to extract. | ||
| 397 | ( | ||
| 398 | json_extract(`config`, '$.auth.kind') = 'header' | ||
| 399 | AND json_extract(`config`, '$.auth.secretId') IS NOT NULL | ||
| 400 | ) | ||
| 401 | OR ( | ||
| 402 | json_extract(`config`, '$.auth.kind') = 'oauth2' | ||
| 403 | AND json_extract(`config`, '$.auth.connectionId') IS NOT NULL | ||
| 404 | ) | ||
| 405 | );--> statement-breakpoint | ||
| 406 | |||
| 407 | -- Backfill mcp_source_header from config.headers. Remote sources only; | ||
| 408 | -- stdio's config has no `.headers` key so json_each returns nothing. | ||
| 409 | INSERT OR IGNORE INTO `mcp_source_header` | ||
| 410 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 411 | SELECT | ||
| 412 | s.`scope_id`, | ||
| 413 | json_array(s.`id`, h.`key`), | ||
| 414 | s.`id`, | ||
| 415 | h.`key`, | ||
| 416 | CASE | ||
| 417 | WHEN h.`type` = 'object' AND json_extract(h.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 418 | ELSE 'text' | ||
| 419 | END, | ||
| 420 | CASE WHEN h.`type` = 'object' THEN NULL ELSE h.`value` END, | ||
| 421 | CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.secretId') ELSE NULL END, | ||
| 422 | CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.prefix') ELSE NULL END | ||
| 423 | FROM `mcp_source` s, json_each(json_extract(s.`config`, '$.headers')) h | ||
| 424 | WHERE json_extract(s.`config`, '$.headers') IS NOT NULL;--> statement-breakpoint | ||
| 425 | |||
| 426 | INSERT OR IGNORE INTO `mcp_source_query_param` | ||
| 427 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 428 | SELECT | ||
| 429 | s.`scope_id`, | ||
| 430 | json_array(s.`id`, q.`key`), | ||
| 431 | s.`id`, | ||
| 432 | q.`key`, | ||
| 433 | CASE | ||
| 434 | WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 435 | ELSE 'text' | ||
| 436 | END, | ||
| 437 | CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, | ||
| 438 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, | ||
| 439 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END | ||
| 440 | FROM `mcp_source` s, json_each(json_extract(s.`config`, '$.queryParams')) q | ||
| 441 | WHERE json_extract(s.`config`, '$.queryParams') IS NOT NULL;--> statement-breakpoint | ||
| 442 | |||
| 443 | -- Strip the now-extracted fields from the legacy config JSON. Skip | ||
| 444 | -- rows whose config.auth still holds a legacy inline-OAuth payload — | ||
| 445 | -- migrateLegacyConnections needs to read it to mint the matching | ||
| 446 | -- Connection. headers/queryParams are always safe to strip (already | ||
| 447 | -- copied to child tables). SQLite's json_remove returns the input | ||
| 448 | -- unchanged when a path is missing, so stdio rows pass through | ||
| 449 | -- cleanly. | ||
| 450 | UPDATE `mcp_source` | ||
| 451 | SET `config` = json_remove(`config`, '$.headers', '$.queryParams') | ||
| 452 | WHERE `config` IS NOT NULL;--> statement-breakpoint | ||
| 453 | |||
| 454 | UPDATE `mcp_source` | ||
| 455 | SET `config` = json_remove(`config`, '$.auth') | ||
| 456 | WHERE `config` IS NOT NULL | ||
| 457 | AND ( | ||
| 458 | json_extract(`config`, '$.auth.kind') = 'none' | ||
| 459 | OR ( | ||
| 460 | json_extract(`config`, '$.auth.kind') = 'header' | ||
| 461 | AND json_extract(`config`, '$.auth.secretId') IS NOT NULL | ||
| 462 | ) | ||
| 463 | OR ( | ||
| 464 | json_extract(`config`, '$.auth.kind') = 'oauth2' | ||
| 465 | AND json_extract(`config`, '$.auth.connectionId') IS NOT NULL | ||
| 466 | ) | ||
| 467 | ); | ||
| 468 | |||
| 469 | --> statement-breakpoint | ||
| 470 | |||
| 471 | -- ============================================================ | ||
| 472 | -- google-discovery | ||
| 473 | -- ============================================================ | ||
| 474 | |||
| 475 | -- Normalize google-discovery plugin: lift the GoogleDiscoveryAuth and | ||
| 476 | -- the credentials.{headers,queryParams} SecretBackedMaps out of | ||
| 477 | -- google_discovery_source.config JSON. | ||
| 478 | -- | ||
| 479 | -- Old shape: | ||
| 480 | -- google_discovery_source.config (json) — GoogleDiscoveryStoredSourceData | ||
| 481 | -- with `auth: {kind:"none"} | {kind:"oauth2", connectionId, clientId..., clientSecret..., scopes}` | ||
| 482 | -- and optional `credentials: { headers?, queryParams? }` | ||
| 483 | -- | ||
| 484 | -- New shape: | ||
| 485 | -- google_discovery_source gains: auth_kind, auth_connection_id, | ||
| 486 | -- auth_client_id_secret_id, auth_client_secret_secret_id, auth_scopes. | ||
| 487 | -- google_discovery_source_credential_header / _query_param: child | ||
| 488 | -- tables for the SecretBackedMap entries. | ||
| 489 | |||
| 490 | CREATE TABLE `google_discovery_source_credential_header` ( | ||
| 491 | `id` text NOT NULL, | ||
| 492 | `scope_id` text NOT NULL, | ||
| 493 | `source_id` text NOT NULL, | ||
| 494 | `name` text NOT NULL, | ||
| 495 | `kind` text NOT NULL, | ||
| 496 | `text_value` text, | ||
| 497 | `secret_id` text, | ||
| 498 | `secret_prefix` text, | ||
| 499 | PRIMARY KEY(`scope_id`, `id`) | ||
| 500 | ); | ||
| 501 | --> statement-breakpoint | ||
| 502 | CREATE INDEX `google_discovery_source_credential_header_scope_id_idx` ON `google_discovery_source_credential_header` (`scope_id`);--> statement-breakpoint | ||
| 503 | CREATE INDEX `google_discovery_source_credential_header_source_id_idx` ON `google_discovery_source_credential_header` (`source_id`);--> statement-breakpoint | ||
| 504 | CREATE INDEX `google_discovery_source_credential_header_secret_id_idx` ON `google_discovery_source_credential_header` (`secret_id`);--> statement-breakpoint | ||
| 505 | |||
| 506 | CREATE TABLE `google_discovery_source_credential_query_param` ( | ||
| 507 | `id` text NOT NULL, | ||
| 508 | `scope_id` text NOT NULL, | ||
| 509 | `source_id` text NOT NULL, | ||
| 510 | `name` text NOT NULL, | ||
| 511 | `kind` text NOT NULL, | ||
| 512 | `text_value` text, | ||
| 513 | `secret_id` text, | ||
| 514 | `secret_prefix` text, | ||
| 515 | PRIMARY KEY(`scope_id`, `id`) | ||
| 516 | ); | ||
| 517 | --> statement-breakpoint | ||
| 518 | CREATE INDEX `google_discovery_source_credential_query_param_scope_id_idx` ON `google_discovery_source_credential_query_param` (`scope_id`);--> statement-breakpoint | ||
| 519 | CREATE INDEX `google_discovery_source_credential_query_param_source_id_idx` ON `google_discovery_source_credential_query_param` (`source_id`);--> statement-breakpoint | ||
| 520 | CREATE INDEX `google_discovery_source_credential_query_param_secret_id_idx` ON `google_discovery_source_credential_query_param` (`secret_id`);--> statement-breakpoint | ||
| 521 | |||
| 522 | ALTER TABLE `google_discovery_source` ADD `auth_kind` text DEFAULT 'none' NOT NULL;--> statement-breakpoint | ||
| 523 | ALTER TABLE `google_discovery_source` ADD `auth_connection_id` text;--> statement-breakpoint | ||
| 524 | ALTER TABLE `google_discovery_source` ADD `auth_client_id_secret_id` text;--> statement-breakpoint | ||
| 525 | ALTER TABLE `google_discovery_source` ADD `auth_client_secret_secret_id` text;--> statement-breakpoint | ||
| 526 | ALTER TABLE `google_discovery_source` ADD `auth_scopes` text;--> statement-breakpoint | ||
| 527 | CREATE INDEX `google_discovery_source_auth_connection_id_idx` ON `google_discovery_source` (`auth_connection_id`);--> statement-breakpoint | ||
| 528 | CREATE INDEX `google_discovery_source_auth_client_id_secret_id_idx` ON `google_discovery_source` (`auth_client_id_secret_id`);--> statement-breakpoint | ||
| 529 | CREATE INDEX `google_discovery_source_auth_client_secret_secret_id_idx` ON `google_discovery_source` (`auth_client_secret_secret_id`);--> statement-breakpoint | ||
| 530 | |||
| 531 | -- Backfill auth columns from config.auth. | ||
| 532 | UPDATE `google_discovery_source` | ||
| 533 | SET | ||
| 534 | `auth_kind` = COALESCE(json_extract(`config`, '$.auth.kind'), 'none'), | ||
| 535 | `auth_connection_id` = json_extract(`config`, '$.auth.connectionId'), | ||
| 536 | `auth_client_id_secret_id` = json_extract(`config`, '$.auth.clientIdSecretId'), | ||
| 537 | `auth_client_secret_secret_id` = json_extract(`config`, '$.auth.clientSecretSecretId'), | ||
| 538 | `auth_scopes` = json_extract(`config`, '$.auth.scopes') | ||
| 539 | WHERE `config` IS NOT NULL;--> statement-breakpoint | ||
| 540 | |||
| 541 | -- Backfill credential header / query_param child rows. | ||
| 542 | INSERT OR IGNORE INTO `google_discovery_source_credential_header` | ||
| 543 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 544 | SELECT | ||
| 545 | s.`scope_id`, | ||
| 546 | json_array(s.`id`, h.`key`), | ||
| 547 | s.`id`, | ||
| 548 | h.`key`, | ||
| 549 | CASE | ||
| 550 | WHEN h.`type` = 'object' AND json_extract(h.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 551 | ELSE 'text' | ||
| 552 | END, | ||
| 553 | CASE WHEN h.`type` = 'object' THEN NULL ELSE h.`value` END, | ||
| 554 | CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.secretId') ELSE NULL END, | ||
| 555 | CASE WHEN h.`type` = 'object' THEN json_extract(h.`value`, '$.prefix') ELSE NULL END | ||
| 556 | FROM `google_discovery_source` s, json_each(json_extract(s.`config`, '$.credentials.headers')) h | ||
| 557 | WHERE json_extract(s.`config`, '$.credentials.headers') IS NOT NULL;--> statement-breakpoint | ||
| 558 | |||
| 559 | INSERT OR IGNORE INTO `google_discovery_source_credential_query_param` | ||
| 560 | (`scope_id`, `id`, `source_id`, `name`, `kind`, `text_value`, `secret_id`, `secret_prefix`) | ||
| 561 | SELECT | ||
| 562 | s.`scope_id`, | ||
| 563 | json_array(s.`id`, q.`key`), | ||
| 564 | s.`id`, | ||
| 565 | q.`key`, | ||
| 566 | CASE | ||
| 567 | WHEN q.`type` = 'object' AND json_extract(q.`value`, '$.secretId') IS NOT NULL THEN 'secret' | ||
| 568 | ELSE 'text' | ||
| 569 | END, | ||
| 570 | CASE WHEN q.`type` = 'object' THEN NULL ELSE q.`value` END, | ||
| 571 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.secretId') ELSE NULL END, | ||
| 572 | CASE WHEN q.`type` = 'object' THEN json_extract(q.`value`, '$.prefix') ELSE NULL END | ||
| 573 | FROM `google_discovery_source` s, json_each(json_extract(s.`config`, '$.credentials.queryParams')) q | ||
| 574 | WHERE json_extract(s.`config`, '$.credentials.queryParams') IS NOT NULL;--> statement-breakpoint | ||
| 575 | |||
| 576 | -- Strip the extracted fields from the legacy config JSON. | ||
| 577 | UPDATE `google_discovery_source` | ||
| 578 | SET `config` = json_remove(`config`, '$.auth', '$.credentials') | ||
| 579 | WHERE `config` IS NOT NULL; | ||
| @@ -0,0 +1,2422 @@ | |||
| 1 | { | ||
| 2 | "version": "6", | ||
| 3 | "dialect": "sqlite", | ||
| 4 | "id": "44444444-5555-6666-7777-888888888888", | ||
| 5 | "prevId": "7a331df8-ce69-4d97-b6ce-6bf3aff98b56", | ||
| 6 | "tables": { | ||
| 7 | "blob": { | ||
| 8 | "name": "blob", | ||
| 9 | "columns": { | ||
| 10 | "namespace": { | ||
| 11 | "name": "namespace", | ||
| 12 | "type": "text", | ||
| 13 | "primaryKey": false, | ||
| 14 | "notNull": true, | ||
| 15 | "autoincrement": false | ||
| 16 | }, | ||
| 17 | "key": { | ||
| 18 | "name": "key", | ||
| 19 | "type": "text", | ||
| 20 | "primaryKey": false, | ||
| 21 | "notNull": true, | ||
| 22 | "autoincrement": false | ||
| 23 | }, | ||
| 24 | "value": { | ||
| 25 | "name": "value", | ||
| 26 | "type": "text", | ||
| 27 | "primaryKey": false, | ||
| 28 | "notNull": true, | ||
| 29 | "autoincrement": false | ||
| 30 | } | ||
| 31 | }, | ||
| 32 | "indexes": {}, | ||
| 33 | "foreignKeys": {}, | ||
| 34 | "compositePrimaryKeys": { | ||
| 35 | "blob_namespace_key_pk": { | ||
| 36 | "columns": [ | ||
| 37 | "namespace", | ||
| 38 | "key" | ||
| 39 | ], | ||
| 40 | "name": "blob_namespace_key_pk" | ||
| 41 | } | ||
| 42 | }, | ||
| 43 | "uniqueConstraints": {}, | ||
| 44 | "checkConstraints": {} | ||
| 45 | }, | ||
| 46 | "connection": { | ||
| 47 | "name": "connection", | ||
| 48 | "columns": { | ||
| 49 | "id": { | ||
| 50 | "name": "id", | ||
| 51 | "type": "text", | ||
| 52 | "primaryKey": false, | ||
| 53 | "notNull": true, | ||
| 54 | "autoincrement": false | ||
| 55 | }, | ||
| 56 | "scope_id": { | ||
| 57 | "name": "scope_id", | ||
| 58 | "type": "text", | ||
| 59 | "primaryKey": false, | ||
| 60 | "notNull": true, | ||
| 61 | "autoincrement": false | ||
| 62 | }, | ||
| 63 | "provider": { | ||
| 64 | "name": "provider", | ||
| 65 | "type": "text", | ||
| 66 | "primaryKey": false, | ||
| 67 | "notNull": true, | ||
| 68 | "autoincrement": false | ||
| 69 | }, | ||
| 70 | "identity_label": { | ||
| 71 | "name": "identity_label", | ||
| 72 | "type": "text", | ||
| 73 | "primaryKey": false, | ||
| 74 | "notNull": false, | ||
| 75 | "autoincrement": false | ||
| 76 | }, | ||
| 77 | "access_token_secret_id": { | ||
| 78 | "name": "access_token_secret_id", | ||
| 79 | "type": "text", | ||
| 80 | "primaryKey": false, | ||
| 81 | "notNull": true, | ||
| 82 | "autoincrement": false | ||
| 83 | }, | ||
| 84 | "refresh_token_secret_id": { | ||
| 85 | "name": "refresh_token_secret_id", | ||
| 86 | "type": "text", | ||
| 87 | "primaryKey": false, | ||
| 88 | "notNull": false, | ||
| 89 | "autoincrement": false | ||
| 90 | }, | ||
| 91 | "expires_at": { | ||
| 92 | "name": "expires_at", | ||
| 93 | "type": "integer", | ||
| 94 | "primaryKey": false, | ||
| 95 | "notNull": false, | ||
| 96 | "autoincrement": false | ||
| 97 | }, | ||
| 98 | "scope": { | ||
| 99 | "name": "scope", | ||
| 100 | "type": "text", | ||
| 101 | "primaryKey": false, | ||
| 102 | "notNull": false, | ||
| 103 | "autoincrement": false | ||
| 104 | }, | ||
| 105 | "provider_state": { | ||
| 106 | "name": "provider_state", | ||
| 107 | "type": "text", | ||
| 108 | "primaryKey": false, | ||
| 109 | "notNull": false, | ||
| 110 | "autoincrement": false | ||
| 111 | }, | ||
| 112 | "created_at": { | ||
| 113 | "name": "created_at", | ||
| 114 | "type": "integer", | ||
| 115 | "primaryKey": false, | ||
| 116 | "notNull": true, | ||
| 117 | "autoincrement": false | ||
| 118 | }, | ||
| 119 | "updated_at": { | ||
| 120 | "name": "updated_at", | ||
| 121 | "type": "integer", | ||
| 122 | "primaryKey": false, | ||
| 123 | "notNull": true, | ||
| 124 | "autoincrement": false | ||
| 125 | } | ||
| 126 | }, | ||
| 127 | "indexes": { | ||
| 128 | "connection_scope_id_idx": { | ||
| 129 | "name": "connection_scope_id_idx", | ||
| 130 | "columns": [ | ||
| 131 | "scope_id" | ||
| 132 | ], | ||
| 133 | "isUnique": false | ||
| 134 | }, | ||
| 135 | "connection_provider_idx": { | ||
| 136 | "name": "connection_provider_idx", | ||
| 137 | "columns": [ | ||
| 138 | "provider" | ||
| 139 | ], | ||
| 140 | "isUnique": false | ||
| 141 | } | ||
| 142 | }, | ||
| 143 | "foreignKeys": {}, | ||
| 144 | "compositePrimaryKeys": { | ||
| 145 | "connection_scope_id_id_pk": { | ||
| 146 | "columns": [ | ||
| 147 | "scope_id", | ||
| 148 | "id" | ||
| 149 | ], | ||
| 150 | "name": "connection_scope_id_id_pk" | ||
| 151 | } | ||
| 152 | }, | ||
| 153 | "uniqueConstraints": {}, | ||
| 154 | "checkConstraints": {} | ||
| 155 | }, | ||
| 156 | "definition": { | ||
| 157 | "name": "definition", | ||
| 158 | "columns": { | ||
| 159 | "id": { | ||
| 160 | "name": "id", | ||
| 161 | "type": "text", | ||
| 162 | "primaryKey": false, | ||
| 163 | "notNull": true, | ||
| 164 | "autoincrement": false | ||
| 165 | }, | ||
| 166 | "scope_id": { | ||
| 167 | "name": "scope_id", | ||
| 168 | "type": "text", | ||
| 169 | "primaryKey": false, | ||
| 170 | "notNull": true, | ||
| 171 | "autoincrement": false | ||
| 172 | }, | ||
| 173 | "source_id": { | ||
| 174 | "name": "source_id", | ||
| 175 | "type": "text", | ||
| 176 | "primaryKey": false, | ||
| 177 | "notNull": true, | ||
| 178 | "autoincrement": false | ||
| 179 | }, | ||
| 180 | "plugin_id": { | ||
| 181 | "name": "plugin_id", | ||
| 182 | "type": "text", | ||
| 183 | "primaryKey": false, | ||
| 184 | "notNull": true, | ||
| 185 | "autoincrement": false | ||
| 186 | }, | ||
| 187 | "name": { | ||
| 188 | "name": "name", | ||
| 189 | "type": "text", | ||
| 190 | "primaryKey": false, | ||
| 191 | "notNull": true, | ||
| 192 | "autoincrement": false | ||
| 193 | }, | ||
| 194 | "schema": { | ||
| 195 | "name": "schema", | ||
| 196 | "type": "text", | ||
| 197 | "primaryKey": false, | ||
| 198 | "notNull": true, | ||
| 199 | "autoincrement": false | ||
| 200 | }, | ||
| 201 | "created_at": { | ||
| 202 | "name": "created_at", | ||
| 203 | "type": "integer", | ||
| 204 | "primaryKey": false, | ||
| 205 | "notNull": true, | ||
| 206 | "autoincrement": false | ||
| 207 | } | ||
| 208 | }, | ||
| 209 | "indexes": { | ||
| 210 | "definition_scope_id_idx": { | ||
| 211 | "name": "definition_scope_id_idx", | ||
| 212 | "columns": [ | ||
| 213 | "scope_id" | ||
| 214 | ], | ||
| 215 | "isUnique": false | ||
| 216 | }, | ||
| 217 | "definition_source_id_idx": { | ||
| 218 | "name": "definition_source_id_idx", | ||
| 219 | "columns": [ | ||
| 220 | "source_id" | ||
| 221 | ], | ||
| 222 | "isUnique": false | ||
| 223 | }, | ||
| 224 | "definition_plugin_id_idx": { | ||
| 225 | "name": "definition_plugin_id_idx", | ||
| 226 | "columns": [ | ||
| 227 | "plugin_id" | ||
| 228 | ], | ||
| 229 | "isUnique": false | ||
| 230 | } | ||
| 231 | }, | ||
| 232 | "foreignKeys": {}, | ||
| 233 | "compositePrimaryKeys": { | ||
| 234 | "definition_scope_id_id_pk": { | ||
| 235 | "columns": [ | ||
| 236 | "scope_id", | ||
| 237 | "id" | ||
| 238 | ], | ||
| 239 | "name": "definition_scope_id_id_pk" | ||
| 240 | } | ||
| 241 | }, | ||
| 242 | "uniqueConstraints": {}, | ||
| 243 | "checkConstraints": {} | ||
| 244 | }, | ||
| 245 | "google_discovery_binding": { | ||
| 246 | "name": "google_discovery_binding", | ||
| 247 | "columns": { | ||
| 248 | "id": { | ||
| 249 | "name": "id", | ||
| 250 | "type": "text", | ||
| 251 | "primaryKey": false, | ||
| 252 | "notNull": true, | ||
| 253 | "autoincrement": false | ||
| 254 | }, | ||
| 255 | "scope_id": { | ||
| 256 | "name": "scope_id", | ||
| 257 | "type": "text", | ||
| 258 | "primaryKey": false, | ||
| 259 | "notNull": true, | ||
| 260 | "autoincrement": false | ||
| 261 | }, | ||
| 262 | "source_id": { | ||
| 263 | "name": "source_id", | ||
| 264 | "type": "text", | ||
| 265 | "primaryKey": false, | ||
| 266 | "notNull": true, | ||
| 267 | "autoincrement": false | ||
| 268 | }, | ||
| 269 | "binding": { | ||
| 270 | "name": "binding", | ||
| 271 | "type": "text", | ||
| 272 | "primaryKey": false, | ||
| 273 | "notNull": true, | ||
| 274 | "autoincrement": false | ||
| 275 | }, | ||
| 276 | "created_at": { | ||
| 277 | "name": "created_at", | ||
| 278 | "type": "integer", | ||
| 279 | "primaryKey": false, | ||
| 280 | "notNull": true, | ||
| 281 | "autoincrement": false | ||
| 282 | } | ||
| 283 | }, | ||
| 284 | "indexes": { | ||
| 285 | "google_discovery_binding_scope_id_idx": { | ||
| 286 | "name": "google_discovery_binding_scope_id_idx", | ||
| 287 | "columns": [ | ||
| 288 | "scope_id" | ||
| 289 | ], | ||
| 290 | "isUnique": false | ||
| 291 | }, | ||
| 292 | "google_discovery_binding_source_id_idx": { | ||
| 293 | "name": "google_discovery_binding_source_id_idx", | ||
| 294 | "columns": [ | ||
| 295 | "source_id" | ||
| 296 | ], | ||
| 297 | "isUnique": false | ||
| 298 | } | ||
| 299 | }, | ||
| 300 | "foreignKeys": {}, | ||
| 301 | "compositePrimaryKeys": { | ||
| 302 | "google_discovery_binding_scope_id_id_pk": { | ||
| 303 | "columns": [ | ||
| 304 | "scope_id", | ||
| 305 | "id" | ||
| 306 | ], | ||
| 307 | "name": "google_discovery_binding_scope_id_id_pk" | ||
| 308 | } | ||
| 309 | }, | ||
| 310 | "uniqueConstraints": {}, | ||
| 311 | "checkConstraints": {} | ||
| 312 | }, | ||
| 313 | "google_discovery_source": { | ||
| 314 | "name": "google_discovery_source", | ||
| 315 | "columns": { | ||
| 316 | "id": { | ||
| 317 | "name": "id", | ||
| 318 | "type": "text", | ||
| 319 | "primaryKey": false, | ||
| 320 | "notNull": true, | ||
| 321 | "autoincrement": false | ||
| 322 | }, | ||
| 323 | "scope_id": { | ||
| 324 | "name": "scope_id", | ||
| 325 | "type": "text", | ||
| 326 | "primaryKey": false, | ||
| 327 | "notNull": true, | ||
| 328 | "autoincrement": false | ||
| 329 | }, | ||
| 330 | "name": { | ||
| 331 | "name": "name", | ||
| 332 | "type": "text", | ||
| 333 | "primaryKey": false, | ||
| 334 | "notNull": true, | ||
| 335 | "autoincrement": false | ||
| 336 | }, | ||
| 337 | "config": { | ||
| 338 | "name": "config", | ||
| 339 | "type": "text", | ||
| 340 | "primaryKey": false, | ||
| 341 | "notNull": true, | ||
| 342 | "autoincrement": false | ||
| 343 | }, | ||
| 344 | "created_at": { | ||
| 345 | "name": "created_at", | ||
| 346 | "type": "integer", | ||
| 347 | "primaryKey": false, | ||
| 348 | "notNull": true, | ||
| 349 | "autoincrement": false | ||
| 350 | }, | ||
| 351 | "updated_at": { | ||
| 352 | "name": "updated_at", | ||
| 353 | "type": "integer", | ||
| 354 | "primaryKey": false, | ||
| 355 | "notNull": true, | ||
| 356 | "autoincrement": false | ||
| 357 | }, | ||
| 358 | "auth_kind": { | ||
| 359 | "name": "auth_kind", | ||
| 360 | "type": "text", | ||
| 361 | "primaryKey": false, | ||
| 362 | "notNull": true, | ||
| 363 | "autoincrement": false, | ||
| 364 | "default": "'none'" | ||
| 365 | }, | ||
| 366 | "auth_connection_id": { | ||
| 367 | "name": "auth_connection_id", | ||
| 368 | "type": "text", | ||
| 369 | "primaryKey": false, | ||
| 370 | "notNull": false, | ||
| 371 | "autoincrement": false | ||
| 372 | }, | ||
| 373 | "auth_client_id_secret_id": { | ||
| 374 | "name": "auth_client_id_secret_id", | ||
| 375 | "type": "text", | ||
| 376 | "primaryKey": false, | ||
| 377 | "notNull": false, | ||
| 378 | "autoincrement": false | ||
| 379 | }, | ||
| 380 | "auth_client_secret_secret_id": { | ||
| 381 | "name": "auth_client_secret_secret_id", | ||
| 382 | "type": "text", | ||
| 383 | "primaryKey": false, | ||
| 384 | "notNull": false, | ||
| 385 | "autoincrement": false | ||
| 386 | }, | ||
| 387 | "auth_scopes": { | ||
| 388 | "name": "auth_scopes", | ||
| 389 | "type": "text", | ||
| 390 | "primaryKey": false, | ||
| 391 | "notNull": false, | ||
| 392 | "autoincrement": false | ||
| 393 | } | ||
| 394 | }, | ||
| 395 | "indexes": { | ||
| 396 | "google_discovery_source_scope_id_idx": { | ||
| 397 | "name": "google_discovery_source_scope_id_idx", | ||
| 398 | "columns": [ | ||
| 399 | "scope_id" | ||
| 400 | ], | ||
| 401 | "isUnique": false | ||
| 402 | }, | ||
| 403 | "google_discovery_source_auth_connection_id_idx": { | ||
| 404 | "name": "google_discovery_source_auth_connection_id_idx", | ||
| 405 | "columns": [ | ||
| 406 | "auth_connection_id" | ||
| 407 | ], | ||
| 408 | "isUnique": false | ||
| 409 | }, | ||
| 410 | "google_discovery_source_auth_client_id_secret_id_idx": { | ||
| 411 | "name": "google_discovery_source_auth_client_id_secret_id_idx", | ||
| 412 | "columns": [ | ||
| 413 | "auth_client_id_secret_id" | ||
| 414 | ], | ||
| 415 | "isUnique": false | ||
| 416 | }, | ||
| 417 | "google_discovery_source_auth_client_secret_secret_id_idx": { | ||
| 418 | "name": "google_discovery_source_auth_client_secret_secret_id_idx", | ||
| 419 | "columns": [ | ||
| 420 | "auth_client_secret_secret_id" | ||
| 421 | ], | ||
| 422 | "isUnique": false | ||
| 423 | } | ||
| 424 | }, | ||
| 425 | "foreignKeys": {}, | ||
| 426 | "compositePrimaryKeys": { | ||
| 427 | "google_discovery_source_scope_id_id_pk": { | ||
| 428 | "columns": [ | ||
| 429 | "scope_id", | ||
| 430 | "id" | ||
| 431 | ], | ||
| 432 | "name": "google_discovery_source_scope_id_id_pk" | ||
| 433 | } | ||
| 434 | }, | ||
| 435 | "uniqueConstraints": {}, | ||
| 436 | "checkConstraints": {} | ||
| 437 | }, | ||
| 438 | "graphql_operation": { | ||
| 439 | "name": "graphql_operation", | ||
| 440 | "columns": { | ||
| 441 | "id": { | ||
| 442 | "name": "id", | ||
| 443 | "type": "text", | ||
| 444 | "primaryKey": false, | ||
| 445 | "notNull": true, | ||
| 446 | "autoincrement": false | ||
| 447 | }, | ||
| 448 | "scope_id": { | ||
| 449 | "name": "scope_id", | ||
| 450 | "type": "text", | ||
| 451 | "primaryKey": false, | ||
| 452 | "notNull": true, | ||
| 453 | "autoincrement": false | ||
| 454 | }, | ||
| 455 | "source_id": { | ||
| 456 | "name": "source_id", | ||
| 457 | "type": "text", | ||
| 458 | "primaryKey": false, | ||
| 459 | "notNull": true, | ||
| 460 | "autoincrement": false | ||
| 461 | }, | ||
| 462 | "binding": { | ||
| 463 | "name": "binding", | ||
| 464 | "type": "text", | ||
| 465 | "primaryKey": false, | ||
| 466 | "notNull": true, | ||
| 467 | "autoincrement": false | ||
| 468 | } | ||
| 469 | }, | ||
| 470 | "indexes": { | ||
| 471 | "graphql_operation_scope_id_idx": { | ||
| 472 | "name": "graphql_operation_scope_id_idx", | ||
| 473 | "columns": [ | ||
| 474 | "scope_id" | ||
| 475 | ], | ||
| 476 | "isUnique": false | ||
| 477 | }, | ||
| 478 | "graphql_operation_source_id_idx": { | ||
| 479 | "name": "graphql_operation_source_id_idx", | ||
| 480 | "columns": [ | ||
| 481 | "source_id" | ||
| 482 | ], | ||
| 483 | "isUnique": false | ||
| 484 | } | ||
| 485 | }, | ||
| 486 | "foreignKeys": {}, | ||
| 487 | "compositePrimaryKeys": { | ||
| 488 | "graphql_operation_scope_id_id_pk": { | ||
| 489 | "columns": [ | ||
| 490 | "scope_id", | ||
| 491 | "id" | ||
| 492 | ], | ||
| 493 | "name": "graphql_operation_scope_id_id_pk" | ||
| 494 | } | ||
| 495 | }, | ||
| 496 | "uniqueConstraints": {}, | ||
| 497 | "checkConstraints": {} | ||
| 498 | }, | ||
| 499 | "graphql_source": { | ||
| 500 | "name": "graphql_source", | ||
| 501 | "columns": { | ||
| 502 | "id": { | ||
| 503 | "name": "id", | ||
| 504 | "type": "text", | ||
| 505 | "primaryKey": false, | ||
| 506 | "notNull": true, | ||
| 507 | "autoincrement": false | ||
| 508 | }, | ||
| 509 | "scope_id": { | ||
| 510 | "name": "scope_id", | ||
| 511 | "type": "text", | ||
| 512 | "primaryKey": false, | ||
| 513 | "notNull": true, | ||
| 514 | "autoincrement": false | ||
| 515 | }, | ||
| 516 | "name": { | ||
| 517 | "name": "name", | ||
| 518 | "type": "text", | ||
| 519 | "primaryKey": false, | ||
| 520 | "notNull": true, | ||
| 521 | "autoincrement": false | ||
| 522 | }, | ||
| 523 | "endpoint": { | ||
| 524 | "name": "endpoint", | ||
| 525 | "type": "text", | ||
| 526 | "primaryKey": false, | ||
| 527 | "notNull": true, | ||
| 528 | "autoincrement": false | ||
| 529 | }, | ||
| 530 | "auth_kind": { | ||
| 531 | "name": "auth_kind", | ||
| 532 | "type": "text", | ||
| 533 | "primaryKey": false, | ||
| 534 | "notNull": true, | ||
| 535 | "autoincrement": false, | ||
| 536 | "default": "'none'" | ||
| 537 | }, | ||
| 538 | "auth_connection_id": { | ||
| 539 | "name": "auth_connection_id", | ||
| 540 | "type": "text", | ||
| 541 | "primaryKey": false, | ||
| 542 | "notNull": false, | ||
| 543 | "autoincrement": false | ||
| 544 | } | ||
| 545 | }, | ||
| 546 | "indexes": { | ||
| 547 | "graphql_source_scope_id_idx": { | ||
| 548 | "name": "graphql_source_scope_id_idx", | ||
| 549 | "columns": [ | ||
| 550 | "scope_id" | ||
| 551 | ], | ||
| 552 | "isUnique": false | ||
| 553 | }, | ||
| 554 | "graphql_source_auth_connection_id_idx": { | ||
| 555 | "name": "graphql_source_auth_connection_id_idx", | ||
| 556 | "columns": [ | ||
| 557 | "auth_connection_id" | ||
| 558 | ], | ||
| 559 | "isUnique": false | ||
| 560 | } | ||
| 561 | }, | ||
| 562 | "foreignKeys": {}, | ||
| 563 | "compositePrimaryKeys": { | ||
| 564 | "graphql_source_scope_id_id_pk": { | ||
| 565 | "columns": [ | ||
| 566 | "scope_id", | ||
| 567 | "id" | ||
| 568 | ], | ||
| 569 | "name": "graphql_source_scope_id_id_pk" | ||
| 570 | } | ||
| 571 | }, | ||
| 572 | "uniqueConstraints": {}, | ||
| 573 | "checkConstraints": {} | ||
| 574 | }, | ||
| 575 | "mcp_binding": { | ||
| 576 | "name": "mcp_binding", | ||
| 577 | "columns": { | ||
| 578 | "id": { | ||
| 579 | "name": "id", | ||
| 580 | "type": "text", | ||
| 581 | "primaryKey": false, | ||
| 582 | "notNull": true, | ||
| 583 | "autoincrement": false | ||
| 584 | }, | ||
| 585 | "scope_id": { | ||
| 586 | "name": "scope_id", | ||
| 587 | "type": "text", | ||
| 588 | "primaryKey": false, | ||
| 589 | "notNull": true, | ||
| 590 | "autoincrement": false | ||
| 591 | }, | ||
| 592 | "source_id": { | ||
| 593 | "name": "source_id", | ||
| 594 | "type": "text", | ||
| 595 | "primaryKey": false, | ||
| 596 | "notNull": true, | ||
| 597 | "autoincrement": false | ||
| 598 | }, | ||
| 599 | "binding": { | ||
| 600 | "name": "binding", | ||
| 601 | "type": "text", | ||
| 602 | "primaryKey": false, | ||
| 603 | "notNull": true, | ||
| 604 | "autoincrement": false | ||
| 605 | }, | ||
| 606 | "created_at": { | ||
| 607 | "name": "created_at", | ||
| 608 | "type": "integer", | ||
| 609 | "primaryKey": false, | ||
| 610 | "notNull": true, | ||
| 611 | "autoincrement": false | ||
| 612 | } | ||
| 613 | }, | ||
| 614 | "indexes": { | ||
| 615 | "mcp_binding_scope_id_idx": { | ||
| 616 | "name": "mcp_binding_scope_id_idx", | ||
| 617 | "columns": [ | ||
| 618 | "scope_id" | ||
| 619 | ], | ||
| 620 | "isUnique": false | ||
| 621 | }, | ||
| 622 | "mcp_binding_source_id_idx": { | ||
| 623 | "name": "mcp_binding_source_id_idx", | ||
| 624 | "columns": [ | ||
| 625 | "source_id" | ||
| 626 | ], | ||
| 627 | "isUnique": false | ||
| 628 | } | ||
| 629 | }, | ||
| 630 | "foreignKeys": {}, | ||
| 631 | "compositePrimaryKeys": { | ||
| 632 | "mcp_binding_scope_id_id_pk": { | ||
| 633 | "columns": [ | ||
| 634 | "scope_id", | ||
| 635 | "id" | ||
| 636 | ], | ||
| 637 | "name": "mcp_binding_scope_id_id_pk" | ||
| 638 | } | ||
| 639 | }, | ||
| 640 | "uniqueConstraints": {}, | ||
| 641 | "checkConstraints": {} | ||
| 642 | }, | ||
| 643 | "mcp_source": { | ||
| 644 | "name": "mcp_source", | ||
| 645 | "columns": { | ||
| 646 | "id": { | ||
| 647 | "name": "id", | ||
| 648 | "type": "text", | ||
| 649 | "primaryKey": false, | ||
| 650 | "notNull": true, | ||
| 651 | "autoincrement": false | ||
| 652 | }, | ||
| 653 | "scope_id": { | ||
| 654 | "name": "scope_id", | ||
| 655 | "type": "text", | ||
| 656 | "primaryKey": false, | ||
| 657 | "notNull": true, | ||
| 658 | "autoincrement": false | ||
| 659 | }, | ||
| 660 | "name": { | ||
| 661 | "name": "name", | ||
| 662 | "type": "text", | ||
| 663 | "primaryKey": false, | ||
| 664 | "notNull": true, | ||
| 665 | "autoincrement": false | ||
| 666 | }, | ||
| 667 | "config": { | ||
| 668 | "name": "config", | ||
| 669 | "type": "text", | ||
| 670 | "primaryKey": false, | ||
| 671 | "notNull": true, | ||
| 672 | "autoincrement": false | ||
| 673 | }, | ||
| 674 | "created_at": { | ||
| 675 | "name": "created_at", | ||
| 676 | "type": "integer", | ||
| 677 | "primaryKey": false, | ||
| 678 | "notNull": true, | ||
| 679 | "autoincrement": false | ||
| 680 | }, | ||
| 681 | "auth_kind": { | ||
| 682 | "name": "auth_kind", | ||
| 683 | "type": "text", | ||
| 684 | "primaryKey": false, | ||
| 685 | "notNull": true, | ||
| 686 | "autoincrement": false, | ||
| 687 | "default": "'none'" | ||
| 688 | }, | ||
| 689 | "auth_header_name": { | ||
| 690 | "name": "auth_header_name", | ||
| 691 | "type": "text", | ||
| 692 | "primaryKey": false, | ||
| 693 | "notNull": false, | ||
| 694 | "autoincrement": false | ||
| 695 | }, | ||
| 696 | "auth_secret_id": { | ||
| 697 | "name": "auth_secret_id", | ||
| 698 | "type": "text", | ||
| 699 | "primaryKey": false, | ||
| 700 | "notNull": false, | ||
| 701 | "autoincrement": false | ||
| 702 | }, | ||
| 703 | "auth_secret_prefix": { | ||
| 704 | "name": "auth_secret_prefix", | ||
| 705 | "type": "text", | ||
| 706 | "primaryKey": false, | ||
| 707 | "notNull": false, | ||
| 708 | "autoincrement": false | ||
| 709 | }, | ||
| 710 | "auth_connection_id": { | ||
| 711 | "name": "auth_connection_id", | ||
| 712 | "type": "text", | ||
| 713 | "primaryKey": false, | ||
| 714 | "notNull": false, | ||
| 715 | "autoincrement": false | ||
| 716 | }, | ||
| 717 | "auth_client_id_secret_id": { | ||
| 718 | "name": "auth_client_id_secret_id", | ||
| 719 | "type": "text", | ||
| 720 | "primaryKey": false, | ||
| 721 | "notNull": false, | ||
| 722 | "autoincrement": false | ||
| 723 | }, | ||
| 724 | "auth_client_secret_secret_id": { | ||
| 725 | "name": "auth_client_secret_secret_id", | ||
| 726 | "type": "text", | ||
| 727 | "primaryKey": false, | ||
| 728 | "notNull": false, | ||
| 729 | "autoincrement": false | ||
| 730 | } | ||
| 731 | }, | ||
| 732 | "indexes": { | ||
| 733 | "mcp_source_scope_id_idx": { | ||
| 734 | "name": "mcp_source_scope_id_idx", | ||
| 735 | "columns": [ | ||
| 736 | "scope_id" | ||
| 737 | ], | ||
| 738 | "isUnique": false | ||
| 739 | }, | ||
| 740 | "mcp_source_auth_secret_id_idx": { | ||
| 741 | "name": "mcp_source_auth_secret_id_idx", | ||
| 742 | "columns": [ | ||
| 743 | "auth_secret_id" | ||
| 744 | ], | ||
| 745 | "isUnique": false | ||
| 746 | }, | ||
| 747 | "mcp_source_auth_connection_id_idx": { | ||
| 748 | "name": "mcp_source_auth_connection_id_idx", | ||
| 749 | "columns": [ | ||
| 750 | "auth_connection_id" | ||
| 751 | ], | ||
| 752 | "isUnique": false | ||
| 753 | }, | ||
| 754 | "mcp_source_auth_client_id_secret_id_idx": { | ||
| 755 | "name": "mcp_source_auth_client_id_secret_id_idx", | ||
| 756 | "columns": [ | ||
| 757 | "auth_client_id_secret_id" | ||
| 758 | ], | ||
| 759 | "isUnique": false | ||
| 760 | }, | ||
| 761 | "mcp_source_auth_client_secret_secret_id_idx": { | ||
| 762 | "name": "mcp_source_auth_client_secret_secret_id_idx", | ||
| 763 | "columns": [ | ||
| 764 | "auth_client_secret_secret_id" | ||
| 765 | ], | ||
| 766 | "isUnique": false | ||
| 767 | } | ||
| 768 | }, | ||
| 769 | "foreignKeys": {}, | ||
| 770 | "compositePrimaryKeys": { | ||
| 771 | "mcp_source_scope_id_id_pk": { | ||
| 772 | "columns": [ | ||
| 773 | "scope_id", | ||
| 774 | "id" | ||
| 775 | ], | ||
| 776 | "name": "mcp_source_scope_id_id_pk" | ||
| 777 | } | ||
| 778 | }, | ||
| 779 | "uniqueConstraints": {}, | ||
| 780 | "checkConstraints": {} | ||
| 781 | }, | ||
| 782 | "oauth2_session": { | ||
| 783 | "name": "oauth2_session", | ||
| 784 | "columns": { | ||
| 785 | "id": { | ||
| 786 | "name": "id", | ||
| 787 | "type": "text", | ||
| 788 | "primaryKey": false, | ||
| 789 | "notNull": true, | ||
| 790 | "autoincrement": false | ||
| 791 | }, | ||
| 792 | "scope_id": { | ||
| 793 | "name": "scope_id", | ||
| 794 | "type": "text", | ||
| 795 | "primaryKey": false, | ||
| 796 | "notNull": true, | ||
| 797 | "autoincrement": false | ||
| 798 | }, | ||
| 799 | "plugin_id": { | ||
| 800 | "name": "plugin_id", | ||
| 801 | "type": "text", | ||
| 802 | "primaryKey": false, | ||
| 803 | "notNull": true, | ||
| 804 | "autoincrement": false | ||
| 805 | }, | ||
| 806 | "strategy": { | ||
| 807 | "name": "strategy", | ||
| 808 | "type": "text", | ||
| 809 | "primaryKey": false, | ||
| 810 | "notNull": true, | ||
| 811 | "autoincrement": false | ||
| 812 | }, | ||
| 813 | "connection_id": { | ||
| 814 | "name": "connection_id", | ||
| 815 | "type": "text", | ||
| 816 | "primaryKey": false, | ||
| 817 | "notNull": true, | ||
| 818 | "autoincrement": false | ||
| 819 | }, | ||
| 820 | "token_scope": { | ||
| 821 | "name": "token_scope", | ||
| 822 | "type": "text", | ||
| 823 | "primaryKey": false, | ||
| 824 | "notNull": true, | ||
| 825 | "autoincrement": false | ||
| 826 | }, | ||
| 827 | "redirect_url": { | ||
| 828 | "name": "redirect_url", | ||
| 829 | "type": "text", | ||
| 830 | "primaryKey": false, | ||
| 831 | "notNull": true, | ||
| 832 | "autoincrement": false | ||
| 833 | }, | ||
| 834 | "payload": { | ||
| 835 | "name": "payload", | ||
| 836 | "type": "text", | ||
| 837 | "primaryKey": false, | ||
| 838 | "notNull": true, | ||
| 839 | "autoincrement": false | ||
| 840 | }, | ||
| 841 | "expires_at": { | ||
| 842 | "name": "expires_at", | ||
| 843 | "type": "integer", | ||
| 844 | "primaryKey": false, | ||
| 845 | "notNull": true, | ||
| 846 | "autoincrement": false | ||
| 847 | }, | ||
| 848 | "created_at": { | ||
| 849 | "name": "created_at", | ||
| 850 | "type": "integer", | ||
| 851 | "primaryKey": false, | ||
| 852 | "notNull": true, | ||
| 853 | "autoincrement": false | ||
| 854 | } | ||
| 855 | }, | ||
| 856 | "indexes": { | ||
| 857 | "oauth2_session_scope_id_idx": { | ||
| 858 | "name": "oauth2_session_scope_id_idx", | ||
| 859 | "columns": [ | ||
| 860 | "scope_id" | ||
| 861 | ], | ||
| 862 | "isUnique": false | ||
| 863 | }, | ||
| 864 | "oauth2_session_plugin_id_idx": { | ||
| 865 | "name": "oauth2_session_plugin_id_idx", | ||
| 866 | "columns": [ | ||
| 867 | "plugin_id" | ||
| 868 | ], | ||
| 869 | "isUnique": false | ||
| 870 | }, | ||
| 871 | "oauth2_session_connection_id_idx": { | ||
| 872 | "name": "oauth2_session_connection_id_idx", | ||
| 873 | "columns": [ | ||
| 874 | "connection_id" | ||
| 875 | ], | ||
| 876 | "isUnique": false | ||
| 877 | } | ||
| 878 | }, | ||
| 879 | "foreignKeys": {}, | ||
| 880 | "compositePrimaryKeys": { | ||
| 881 | "oauth2_session_scope_id_id_pk": { | ||
| 882 | "columns": [ | ||
| 883 | "scope_id", | ||
| 884 | "id" | ||
| 885 | ], | ||
| 886 | "name": "oauth2_session_scope_id_id_pk" | ||
| 887 | } | ||
| 888 | }, | ||
| 889 | "uniqueConstraints": {}, | ||
| 890 | "checkConstraints": {} | ||
| 891 | }, | ||
| 892 | "openapi_operation": { | ||
| 893 | "name": "openapi_operation", | ||
| 894 | "columns": { | ||
| 895 | "id": { | ||
| 896 | "name": "id", | ||
| 897 | "type": "text", | ||
| 898 | "primaryKey": false, | ||
| 899 | "notNull": true, | ||
| 900 | "autoincrement": false | ||
| 901 | }, | ||
| 902 | "scope_id": { | ||
| 903 | "name": "scope_id", | ||
| 904 | "type": "text", | ||
| 905 | "primaryKey": false, | ||
| 906 | "notNull": true, | ||
| 907 | "autoincrement": false | ||
| 908 | }, | ||
| 909 | "source_id": { | ||
| 910 | "name": "source_id", | ||
| 911 | "type": "text", | ||
| 912 | "primaryKey": false, | ||
| 913 | "notNull": true, | ||
| 914 | "autoincrement": false | ||
| 915 | }, | ||
| 916 | "binding": { | ||
| 917 | "name": "binding", | ||
| 918 | "type": "text", | ||
| 919 | "primaryKey": false, | ||
| 920 | "notNull": true, | ||
| 921 | "autoincrement": false | ||
| 922 | } | ||
| 923 | }, | ||
| 924 | "indexes": { | ||
| 925 | "openapi_operation_scope_id_idx": { | ||
| 926 | "name": "openapi_operation_scope_id_idx", | ||
| 927 | "columns": [ | ||
| 928 | "scope_id" | ||
| 929 | ], | ||
| 930 | "isUnique": false | ||
| 931 | }, | ||
| 932 | "openapi_operation_source_id_idx": { | ||
| 933 | "name": "openapi_operation_source_id_idx", | ||
| 934 | "columns": [ | ||
| 935 | "source_id" | ||
| 936 | ], | ||
| 937 | "isUnique": false | ||
| 938 | } | ||
| 939 | }, | ||
| 940 | "foreignKeys": {}, | ||
| 941 | "compositePrimaryKeys": { | ||
| 942 | "openapi_operation_scope_id_id_pk": { | ||
| 943 | "columns": [ | ||
| 944 | "scope_id", | ||
| 945 | "id" | ||
| 946 | ], | ||
| 947 | "name": "openapi_operation_scope_id_id_pk" | ||
| 948 | } | ||
| 949 | }, | ||
| 950 | "uniqueConstraints": {}, | ||
| 951 | "checkConstraints": {} | ||
| 952 | }, | ||
| 953 | "openapi_source": { | ||
| 954 | "name": "openapi_source", | ||
| 955 | "columns": { | ||
| 956 | "id": { | ||
| 957 | "name": "id", | ||
| 958 | "type": "text", | ||
| 959 | "primaryKey": false, | ||
| 960 | "notNull": true, | ||
| 961 | "autoincrement": false | ||
| 962 | }, | ||
| 963 | "scope_id": { | ||
| 964 | "name": "scope_id", | ||
| 965 | "type": "text", | ||
| 966 | "primaryKey": false, | ||
| 967 | "notNull": true, | ||
| 968 | "autoincrement": false | ||
| 969 | }, | ||
| 970 | "name": { | ||
| 971 | "name": "name", | ||
| 972 | "type": "text", | ||
| 973 | "primaryKey": false, | ||
| 974 | "notNull": true, | ||
| 975 | "autoincrement": false | ||
| 976 | }, | ||
| 977 | "spec": { | ||
| 978 | "name": "spec", | ||
| 979 | "type": "text", | ||
| 980 | "primaryKey": false, | ||
| 981 | "notNull": true, | ||
| 982 | "autoincrement": false | ||
| 983 | }, | ||
| 984 | "source_url": { | ||
| 985 | "name": "source_url", | ||
| 986 | "type": "text", | ||
| 987 | "primaryKey": false, | ||
| 988 | "notNull": false, | ||
| 989 | "autoincrement": false | ||
| 990 | }, | ||
| 991 | "base_url": { | ||
| 992 | "name": "base_url", | ||
| 993 | "type": "text", | ||
| 994 | "primaryKey": false, | ||
| 995 | "notNull": false, | ||
| 996 | "autoincrement": false | ||
| 997 | }, | ||
| 998 | "headers": { | ||
| 999 | "name": "headers", | ||
| 1000 | "type": "text", | ||
| 1001 | "primaryKey": false, | ||
| 1002 | "notNull": false, | ||
| 1003 | "autoincrement": false | ||
| 1004 | }, | ||
| 1005 | "oauth2": { | ||
| 1006 | "name": "oauth2", | ||
| 1007 | "type": "text", | ||
| 1008 | "primaryKey": false, | ||
| 1009 | "notNull": false, | ||
| 1010 | "autoincrement": false | ||
| 1011 | } | ||
| 1012 | }, | ||
| 1013 | "indexes": { | ||
| 1014 | "openapi_source_scope_id_idx": { | ||
| 1015 | "name": "openapi_source_scope_id_idx", | ||
| 1016 | "columns": [ | ||
| 1017 | "scope_id" | ||
| 1018 | ], | ||
| 1019 | "isUnique": false | ||
| 1020 | } | ||
| 1021 | }, | ||
| 1022 | "foreignKeys": {}, | ||
| 1023 | "compositePrimaryKeys": { | ||
| 1024 | "openapi_source_scope_id_id_pk": { | ||
| 1025 | "columns": [ | ||
| 1026 | "scope_id", | ||
| 1027 | "id" | ||
| 1028 | ], | ||
| 1029 | "name": "openapi_source_scope_id_id_pk" | ||
| 1030 | } | ||
| 1031 | }, | ||
| 1032 | "uniqueConstraints": {}, | ||
| 1033 | "checkConstraints": {} | ||
| 1034 | }, | ||
| 1035 | "openapi_source_binding": { | ||
| 1036 | "name": "openapi_source_binding", | ||
| 1037 | "columns": { | ||
| 1038 | "id": { | ||
| 1039 | "name": "id", | ||
| 1040 | "type": "text", | ||
| 1041 | "primaryKey": true, | ||
| 1042 | "notNull": true, | ||
| 1043 | "autoincrement": false | ||
| 1044 | }, | ||
| 1045 | "source_id": { | ||
| 1046 | "name": "source_id", | ||
| 1047 | "type": "text", | ||
| 1048 | "primaryKey": false, | ||
| 1049 | "notNull": true, | ||
| 1050 | "autoincrement": false | ||
| 1051 | }, | ||
| 1052 | "source_scope_id": { | ||
| 1053 | "name": "source_scope_id", | ||
| 1054 | "type": "text", | ||
| 1055 | "primaryKey": false, | ||
| 1056 | "notNull": true, | ||
| 1057 | "autoincrement": false | ||
| 1058 | }, | ||
| 1059 | "target_scope_id": { | ||
| 1060 | "name": "target_scope_id", | ||
| 1061 | "type": "text", | ||
| 1062 | "primaryKey": false, | ||
| 1063 | "notNull": true, | ||
| 1064 | "autoincrement": false | ||
| 1065 | }, | ||
| 1066 | "slot": { | ||
| 1067 | "name": "slot", | ||
| 1068 | "type": "text", | ||
| 1069 | "primaryKey": false, | ||
| 1070 | "notNull": true, | ||
| 1071 | "autoincrement": false | ||
| 1072 | }, | ||
| 1073 | "created_at": { | ||
| 1074 | "name": "created_at", | ||
| 1075 | "type": "integer", | ||
| 1076 | "primaryKey": false, | ||
| 1077 | "notNull": true, | ||
| 1078 | "autoincrement": false | ||
| 1079 | }, | ||
| 1080 | "updated_at": { | ||
| 1081 | "name": "updated_at", | ||
| 1082 | "type": "integer", | ||
| 1083 | "primaryKey": false, | ||
| 1084 | "notNull": true, | ||
| 1085 | "autoincrement": false | ||
| 1086 | }, | ||
| 1087 | "kind": { | ||
| 1088 | "name": "kind", | ||
| 1089 | "type": "text", | ||
| 1090 | "primaryKey": false, | ||
| 1091 | "notNull": true, | ||
| 1092 | "autoincrement": false, | ||
| 1093 | "default": "'text'" | ||
| 1094 | }, | ||
| 1095 | "secret_id": { | ||
| 1096 | "name": "secret_id", | ||
| 1097 | "type": "text", | ||
| 1098 | "primaryKey": false, | ||
| 1099 | "notNull": false, | ||
| 1100 | "autoincrement": false | ||
| 1101 | }, | ||
| 1102 | "connection_id": { | ||
| 1103 | "name": "connection_id", | ||
| 1104 | "type": "text", | ||
| 1105 | "primaryKey": false, | ||
| 1106 | "notNull": false, | ||
| 1107 | "autoincrement": false | ||
| 1108 | }, | ||
| 1109 | "text_value": { | ||
| 1110 | "name": "text_value", | ||
| 1111 | "type": "text", | ||
| 1112 | "primaryKey": false, | ||
| 1113 | "notNull": false, | ||
| 1114 | "autoincrement": false | ||
| 1115 | } | ||
| 1116 | }, | ||
| 1117 | "indexes": { | ||
| 1118 | "openapi_source_binding_source_id_idx": { | ||
| 1119 | "name": "openapi_source_binding_source_id_idx", | ||
| 1120 | "columns": [ | ||
| 1121 | "source_id" | ||
| 1122 | ], | ||
| 1123 | "isUnique": false | ||
| 1124 | }, | ||
| 1125 | "openapi_source_binding_source_scope_id_idx": { | ||
| 1126 | "name": "openapi_source_binding_source_scope_id_idx", | ||
| 1127 | "columns": [ | ||
| 1128 | "source_scope_id" | ||
| 1129 | ], | ||
| 1130 | "isUnique": false | ||
| 1131 | }, | ||
| 1132 | "openapi_source_binding_target_scope_id_idx": { | ||
| 1133 | "name": "openapi_source_binding_target_scope_id_idx", | ||
| 1134 | "columns": [ | ||
| 1135 | "target_scope_id" | ||
| 1136 | ], | ||
| 1137 | "isUnique": false | ||
| 1138 | }, | ||
| 1139 | "openapi_source_binding_slot_idx": { | ||
| 1140 | "name": "openapi_source_binding_slot_idx", | ||
| 1141 | "columns": [ | ||
| 1142 | "slot" | ||
| 1143 | ], | ||
| 1144 | "isUnique": false | ||
| 1145 | }, | ||
| 1146 | "openapi_source_binding_secret_id_idx": { | ||
| 1147 | "name": "openapi_source_binding_secret_id_idx", | ||
| 1148 | "columns": [ | ||
| 1149 | "secret_id" | ||
| 1150 | ], | ||
| 1151 | "isUnique": false | ||
| 1152 | }, | ||
| 1153 | "openapi_source_binding_connection_id_idx": { | ||
| 1154 | "name": "openapi_source_binding_connection_id_idx", | ||
| 1155 | "columns": [ | ||
| 1156 | "connection_id" | ||
| 1157 | ], | ||
| 1158 | "isUnique": false | ||
| 1159 | } | ||
| 1160 | }, | ||
| 1161 | "foreignKeys": {}, | ||
| 1162 | "compositePrimaryKeys": {}, | ||
| 1163 | "uniqueConstraints": {}, | ||
| 1164 | "checkConstraints": {} | ||
| 1165 | }, | ||
| 1166 | "secret": { | ||
| 1167 | "name": "secret", | ||
| 1168 | "columns": { | ||
| 1169 | "id": { | ||
| 1170 | "name": "id", | ||
| 1171 | "type": "text", | ||
| 1172 | "primaryKey": false, | ||
| 1173 | "notNull": true, | ||
| 1174 | "autoincrement": false | ||
| 1175 | }, | ||
| 1176 | "scope_id": { | ||
| 1177 | "name": "scope_id", | ||
| 1178 | "type": "text", | ||
| 1179 | "primaryKey": false, | ||
| 1180 | "notNull": true, | ||
| 1181 | "autoincrement": false | ||
| 1182 | }, | ||
| 1183 | "name": { | ||
| 1184 | "name": "name", | ||
| 1185 | "type": "text", | ||
| 1186 | "primaryKey": false, | ||
| 1187 | "notNull": true, | ||
| 1188 | "autoincrement": false | ||
| 1189 | }, | ||
| 1190 | "provider": { | ||
| 1191 | "name": "provider", | ||
| 1192 | "type": "text", | ||
| 1193 | "primaryKey": false, | ||
| 1194 | "notNull": true, | ||
| 1195 | "autoincrement": false | ||
| 1196 | }, | ||
| 1197 | "owned_by_connection_id": { | ||
| 1198 | "name": "owned_by_connection_id", | ||
| 1199 | "type": "text", | ||
| 1200 | "primaryKey": false, | ||
| 1201 | "notNull": false, | ||
| 1202 | "autoincrement": false | ||
| 1203 | }, | ||
| 1204 | "created_at": { | ||
| 1205 | "name": "created_at", | ||
| 1206 | "type": "integer", | ||
| 1207 | "primaryKey": false, | ||
| 1208 | "notNull": true, | ||
| 1209 | "autoincrement": false | ||
| 1210 | } | ||
| 1211 | }, | ||
| 1212 | "indexes": { | ||
| 1213 | "secret_scope_id_idx": { | ||
| 1214 | "name": "secret_scope_id_idx", | ||
| 1215 | "columns": [ | ||
| 1216 | "scope_id" | ||
| 1217 | ], | ||
| 1218 | "isUnique": false | ||
| 1219 | }, | ||
| 1220 | "secret_provider_idx": { | ||
| 1221 | "name": "secret_provider_idx", | ||
| 1222 | "columns": [ | ||
| 1223 | "provider" | ||
| 1224 | ], | ||
| 1225 | "isUnique": false | ||
| 1226 | }, | ||
| 1227 | "secret_owned_by_connection_id_idx": { | ||
| 1228 | "name": "secret_owned_by_connection_id_idx", | ||
| 1229 | "columns": [ | ||
| 1230 | "owned_by_connection_id" | ||
| 1231 | ], | ||
| 1232 | "isUnique": false | ||
| 1233 | } | ||
| 1234 | }, | ||
| 1235 | "foreignKeys": {}, | ||
| 1236 | "compositePrimaryKeys": { | ||
| 1237 | "secret_scope_id_id_pk": { | ||
| 1238 | "columns": [ | ||
| 1239 | "scope_id", | ||
| 1240 | "id" | ||
| 1241 | ], | ||
| 1242 | "name": "secret_scope_id_id_pk" | ||
| 1243 | } | ||
| 1244 | }, | ||
| 1245 | "uniqueConstraints": {}, | ||
| 1246 | "checkConstraints": {} | ||
| 1247 | }, | ||
| 1248 | "source": { | ||
| 1249 | "name": "source", | ||
| 1250 | "columns": { | ||
| 1251 | "id": { | ||
| 1252 | "name": "id", | ||
| 1253 | "type": "text", | ||
| 1254 | "primaryKey": false, | ||
| 1255 | "notNull": true, | ||
| 1256 | "autoincrement": false | ||
| 1257 | }, | ||
| 1258 | "scope_id": { | ||
| 1259 | "name": "scope_id", | ||
| 1260 | "type": "text", | ||
| 1261 | "primaryKey": false, | ||
| 1262 | "notNull": true, | ||
| 1263 | "autoincrement": false | ||
| 1264 | }, | ||
| 1265 | "plugin_id": { | ||
| 1266 | "name": "plugin_id", | ||
| 1267 | "type": "text", | ||
| 1268 | "primaryKey": false, | ||
| 1269 | "notNull": true, | ||
| 1270 | "autoincrement": false | ||
| 1271 | }, | ||
| 1272 | "kind": { | ||
| 1273 | "name": "kind", | ||
| 1274 | "type": "text", | ||
| 1275 | "primaryKey": false, | ||
| 1276 | "notNull": true, | ||
| 1277 | "autoincrement": false | ||
| 1278 | }, | ||
| 1279 | "name": { | ||
| 1280 | "name": "name", | ||
| 1281 | "type": "text", | ||
| 1282 | "primaryKey": false, | ||
| 1283 | "notNull": true, | ||
| 1284 | "autoincrement": false | ||
| 1285 | }, | ||
| 1286 | "url": { | ||
| 1287 | "name": "url", | ||
| 1288 | "type": "text", | ||
| 1289 | "primaryKey": false, | ||
| 1290 | "notNull": false, | ||
| 1291 | "autoincrement": false | ||
| 1292 | }, | ||
| 1293 | "can_remove": { | ||
| 1294 | "name": "can_remove", | ||
| 1295 | "type": "integer", | ||
| 1296 | "primaryKey": false, | ||
| 1297 | "notNull": true, | ||
| 1298 | "autoincrement": false, | ||
| 1299 | "default": true | ||
| 1300 | }, | ||
| 1301 | "can_refresh": { | ||
| 1302 | "name": "can_refresh", | ||
| 1303 | "type": "integer", | ||
| 1304 | "primaryKey": false, | ||
| 1305 | "notNull": true, | ||
| 1306 | "autoincrement": false, | ||
| 1307 | "default": false | ||
| 1308 | }, | ||
| 1309 | "can_edit": { | ||
| 1310 | "name": "can_edit", | ||
| 1311 | "type": "integer", | ||
| 1312 | "primaryKey": false, | ||
| 1313 | "notNull": true, | ||
| 1314 | "autoincrement": false, | ||
| 1315 | "default": false | ||
| 1316 | }, | ||
| 1317 | "created_at": { | ||
| 1318 | "name": "created_at", | ||
| 1319 | "type": "integer", | ||
| 1320 | "primaryKey": false, | ||
| 1321 | "notNull": true, | ||
| 1322 | "autoincrement": false | ||
| 1323 | }, | ||
| 1324 | "updated_at": { | ||
| 1325 | "name": "updated_at", | ||
| 1326 | "type": "integer", | ||
| 1327 | "primaryKey": false, | ||
| 1328 | "notNull": true, | ||
| 1329 | "autoincrement": false | ||
| 1330 | } | ||
| 1331 | }, | ||
| 1332 | "indexes": { | ||
| 1333 | "source_scope_id_idx": { | ||
| 1334 | "name": "source_scope_id_idx", | ||
| 1335 | "columns": [ | ||
| 1336 | "scope_id" | ||
| 1337 | ], | ||
| 1338 | "isUnique": false | ||
| 1339 | }, | ||
| 1340 | "source_plugin_id_idx": { | ||
| 1341 | "name": "source_plugin_id_idx", | ||
| 1342 | "columns": [ | ||
| 1343 | "plugin_id" | ||
| 1344 | ], | ||
| 1345 | "isUnique": false | ||
| 1346 | } | ||
| 1347 | }, | ||
| 1348 | "foreignKeys": {}, | ||
| 1349 | "compositePrimaryKeys": { | ||
| 1350 | "source_scope_id_id_pk": { | ||
| 1351 | "columns": [ | ||
| 1352 | "scope_id", | ||
| 1353 | "id" | ||
| 1354 | ], | ||
| 1355 | "name": "source_scope_id_id_pk" | ||
| 1356 | } | ||
| 1357 | }, | ||
| 1358 | "uniqueConstraints": {}, | ||
| 1359 | "checkConstraints": {} | ||
| 1360 | }, | ||
| 1361 | "tool": { | ||
| 1362 | "name": "tool", | ||
| 1363 | "columns": { | ||
| 1364 | "id": { | ||
| 1365 | "name": "id", | ||
| 1366 | "type": "text", | ||
| 1367 | "primaryKey": false, | ||
| 1368 | "notNull": true, | ||
| 1369 | "autoincrement": false | ||
| 1370 | }, | ||
| 1371 | "scope_id": { | ||
| 1372 | "name": "scope_id", | ||
| 1373 | "type": "text", | ||
| 1374 | "primaryKey": false, | ||
| 1375 | "notNull": true, | ||
| 1376 | "autoincrement": false | ||
| 1377 | }, | ||
| 1378 | "source_id": { | ||
| 1379 | "name": "source_id", | ||
| 1380 | "type": "text", | ||
| 1381 | "primaryKey": false, | ||
| 1382 | "notNull": true, | ||
| 1383 | "autoincrement": false | ||
| 1384 | }, | ||
| 1385 | "plugin_id": { | ||
| 1386 | "name": "plugin_id", | ||
| 1387 | "type": "text", | ||
| 1388 | "primaryKey": false, | ||
| 1389 | "notNull": true, | ||
| 1390 | "autoincrement": false | ||
| 1391 | }, | ||
| 1392 | "name": { | ||
| 1393 | "name": "name", | ||
| 1394 | "type": "text", | ||
| 1395 | "primaryKey": false, | ||
| 1396 | "notNull": true, | ||
| 1397 | "autoincrement": false | ||
| 1398 | }, | ||
| 1399 | "description": { | ||
| 1400 | "name": "description", | ||
| 1401 | "type": "text", | ||
| 1402 | "primaryKey": false, | ||
| 1403 | "notNull": true, | ||
| 1404 | "autoincrement": false | ||
| 1405 | }, | ||
| 1406 | "input_schema": { | ||
| 1407 | "name": "input_schema", | ||
| 1408 | "type": "text", | ||
| 1409 | "primaryKey": false, | ||
| 1410 | "notNull": false, | ||
| 1411 | "autoincrement": false | ||
| 1412 | }, | ||
| 1413 | "output_schema": { | ||
| 1414 | "name": "output_schema", | ||
| 1415 | "type": "text", | ||
| 1416 | "primaryKey": false, | ||
| 1417 | "notNull": false, | ||
| 1418 | "autoincrement": false | ||
| 1419 | }, | ||
| 1420 | "created_at": { | ||
| 1421 | "name": "created_at", | ||
| 1422 | "type": "integer", | ||
| 1423 | "primaryKey": false, | ||
| 1424 | "notNull": true, | ||
| 1425 | "autoincrement": false | ||
| 1426 | }, | ||
| 1427 | "updated_at": { | ||
| 1428 | "name": "updated_at", | ||
| 1429 | "type": "integer", | ||
| 1430 | "primaryKey": false, | ||
| 1431 | "notNull": true, | ||
| 1432 | "autoincrement": false | ||
| 1433 | } | ||
| 1434 | }, | ||
| 1435 | "indexes": { | ||
| 1436 | "tool_scope_id_idx": { | ||
| 1437 | "name": "tool_scope_id_idx", | ||
| 1438 | "columns": [ | ||
| 1439 | "scope_id" | ||
| 1440 | ], | ||
| 1441 | "isUnique": false | ||
| 1442 | }, | ||
| 1443 | "tool_source_id_idx": { | ||
| 1444 | "name": "tool_source_id_idx", | ||
| 1445 | "columns": [ | ||
| 1446 | "source_id" | ||
| 1447 | ], | ||
| 1448 | "isUnique": false | ||
| 1449 | }, | ||
| 1450 | "tool_plugin_id_idx": { | ||
| 1451 | "name": "tool_plugin_id_idx", | ||
| 1452 | "columns": [ | ||
| 1453 | "plugin_id" | ||
| 1454 | ], | ||
| 1455 | "isUnique": false | ||
| 1456 | } | ||
| 1457 | }, | ||
| 1458 | "foreignKeys": {}, | ||
| 1459 | "compositePrimaryKeys": { | ||
| 1460 | "tool_scope_id_id_pk": { | ||
| 1461 | "columns": [ | ||
| 1462 | "scope_id", | ||
| 1463 | "id" | ||
| 1464 | ], | ||
| 1465 | "name": "tool_scope_id_id_pk" | ||
| 1466 | } | ||
| 1467 | }, | ||
| 1468 | "uniqueConstraints": {}, | ||
| 1469 | "checkConstraints": {} | ||
| 1470 | }, | ||
| 1471 | "tool_policy": { | ||
| 1472 | "name": "tool_policy", | ||
| 1473 | "columns": { | ||
| 1474 | "id": { | ||
| 1475 | "name": "id", | ||
| 1476 | "type": "text", | ||
| 1477 | "primaryKey": false, | ||
| 1478 | "notNull": true, | ||
| 1479 | "autoincrement": false | ||
| 1480 | }, | ||
| 1481 | "scope_id": { | ||
| 1482 | "name": "scope_id", | ||
| 1483 | "type": "text", | ||
| 1484 | "primaryKey": false, | ||
| 1485 | "notNull": true, | ||
| 1486 | "autoincrement": false | ||
| 1487 | }, | ||
| 1488 | "pattern": { | ||
| 1489 | "name": "pattern", | ||
| 1490 | "type": "text", | ||
| 1491 | "primaryKey": false, | ||
| 1492 | "notNull": true, | ||
| 1493 | "autoincrement": false | ||
| 1494 | }, | ||
| 1495 | "action": { | ||
| 1496 | "name": "action", | ||
| 1497 | "type": "text", | ||
| 1498 | "primaryKey": false, | ||
| 1499 | "notNull": true, | ||
| 1500 | "autoincrement": false | ||
| 1501 | }, | ||
| 1502 | "position": { | ||
| 1503 | "name": "position", | ||
| 1504 | "type": "text", | ||
| 1505 | "primaryKey": false, | ||
| 1506 | "notNull": true, | ||
| 1507 | "autoincrement": false | ||
| 1508 | }, | ||
| 1509 | "created_at": { | ||
| 1510 | "name": "created_at", | ||
| 1511 | "type": "integer", | ||
| 1512 | "primaryKey": false, | ||
| 1513 | "notNull": true, | ||
| 1514 | "autoincrement": false | ||
| 1515 | }, | ||
| 1516 | "updated_at": { | ||
| 1517 | "name": "updated_at", | ||
| 1518 | "type": "integer", | ||
| 1519 | "primaryKey": false, | ||
| 1520 | "notNull": true, | ||
| 1521 | "autoincrement": false | ||
| 1522 | } | ||
| 1523 | }, | ||
| 1524 | "indexes": { | ||
| 1525 | "tool_policy_scope_id_position_idx": { | ||
| 1526 | "name": "tool_policy_scope_id_position_idx", | ||
| 1527 | "columns": [ | ||
| 1528 | "scope_id", | ||
| 1529 | "position" | ||
| 1530 | ], | ||
| 1531 | "isUnique": false | ||
| 1532 | } | ||
| 1533 | }, | ||
| 1534 | "foreignKeys": {}, | ||
| 1535 | "compositePrimaryKeys": { | ||
| 1536 | "tool_policy_scope_id_id_pk": { | ||
| 1537 | "columns": [ | ||
| 1538 | "scope_id", | ||
| 1539 | "id" | ||
| 1540 | ], | ||
| 1541 | "name": "tool_policy_scope_id_id_pk" | ||
| 1542 | } | ||
| 1543 | }, | ||
| 1544 | "uniqueConstraints": {}, | ||
| 1545 | "checkConstraints": {} | ||
| 1546 | }, | ||
| 1547 | "graphql_source_header": { | ||
| 1548 | "name": "graphql_source_header", | ||
| 1549 | "columns": { | ||
| 1550 | "id": { | ||
| 1551 | "name": "id", | ||
| 1552 | "type": "text", | ||
| 1553 | "primaryKey": false, | ||
| 1554 | "notNull": true, | ||
| 1555 | "autoincrement": false | ||
| 1556 | }, | ||
| 1557 | "scope_id": { | ||
| 1558 | "name": "scope_id", | ||
| 1559 | "type": "text", | ||
| 1560 | "primaryKey": false, | ||
| 1561 | "notNull": true, | ||
| 1562 | "autoincrement": false | ||
| 1563 | }, | ||
| 1564 | "source_id": { | ||
| 1565 | "name": "source_id", | ||
| 1566 | "type": "text", | ||
| 1567 | "primaryKey": false, | ||
| 1568 | "notNull": true, | ||
| 1569 | "autoincrement": false | ||
| 1570 | }, | ||
| 1571 | "name": { | ||
| 1572 | "name": "name", | ||
| 1573 | "type": "text", | ||
| 1574 | "primaryKey": false, | ||
| 1575 | "notNull": true, | ||
| 1576 | "autoincrement": false | ||
| 1577 | }, | ||
| 1578 | "kind": { | ||
| 1579 | "name": "kind", | ||
| 1580 | "type": "text", | ||
| 1581 | "primaryKey": false, | ||
| 1582 | "notNull": true, | ||
| 1583 | "autoincrement": false | ||
| 1584 | }, | ||
| 1585 | "text_value": { | ||
| 1586 | "name": "text_value", | ||
| 1587 | "type": "text", | ||
| 1588 | "primaryKey": false, | ||
| 1589 | "notNull": false, | ||
| 1590 | "autoincrement": false | ||
| 1591 | }, | ||
| 1592 | "secret_id": { | ||
| 1593 | "name": "secret_id", | ||
| 1594 | "type": "text", | ||
| 1595 | "primaryKey": false, | ||
| 1596 | "notNull": false, | ||
| 1597 | "autoincrement": false | ||
| 1598 | }, | ||
| 1599 | "secret_prefix": { | ||
| 1600 | "name": "secret_prefix", | ||
| 1601 | "type": "text", | ||
| 1602 | "primaryKey": false, | ||
| 1603 | "notNull": false, | ||
| 1604 | "autoincrement": false | ||
| 1605 | } | ||
| 1606 | }, | ||
| 1607 | "indexes": { | ||
| 1608 | "graphql_source_header_scope_id_idx": { | ||
| 1609 | "name": "graphql_source_header_scope_id_idx", | ||
| 1610 | "columns": [ | ||
| 1611 | "scope_id" | ||
| 1612 | ], | ||
| 1613 | "isUnique": false | ||
| 1614 | }, | ||
| 1615 | "graphql_source_header_source_id_idx": { | ||
| 1616 | "name": "graphql_source_header_source_id_idx", | ||
| 1617 | "columns": [ | ||
| 1618 | "source_id" | ||
| 1619 | ], | ||
| 1620 | "isUnique": false | ||
| 1621 | }, | ||
| 1622 | "graphql_source_header_secret_id_idx": { | ||
| 1623 | "name": "graphql_source_header_secret_id_idx", | ||
| 1624 | "columns": [ | ||
| 1625 | "secret_id" | ||
| 1626 | ], | ||
| 1627 | "isUnique": false | ||
| 1628 | } | ||
| 1629 | }, | ||
| 1630 | "foreignKeys": {}, | ||
| 1631 | "compositePrimaryKeys": { | ||
| 1632 | "graphql_source_header_scope_id_id_pk": { | ||
| 1633 | "columns": [ | ||
| 1634 | "scope_id", | ||
| 1635 | "id" | ||
| 1636 | ], | ||
| 1637 | "name": "graphql_source_header_scope_id_id_pk" | ||
| 1638 | } | ||
| 1639 | }, | ||
| 1640 | "uniqueConstraints": {}, | ||
| 1641 | "checkConstraints": {} | ||
| 1642 | }, | ||
| 1643 | "graphql_source_query_param": { | ||
| 1644 | "name": "graphql_source_query_param", | ||
| 1645 | "columns": { | ||
| 1646 | "id": { | ||
| 1647 | "name": "id", | ||
| 1648 | "type": "text", | ||
| 1649 | "primaryKey": false, | ||
| 1650 | "notNull": true, | ||
| 1651 | "autoincrement": false | ||
| 1652 | }, | ||
| 1653 | "scope_id": { | ||
| 1654 | "name": "scope_id", | ||
| 1655 | "type": "text", | ||
| 1656 | "primaryKey": false, | ||
| 1657 | "notNull": true, | ||
| 1658 | "autoincrement": false | ||
| 1659 | }, | ||
| 1660 | "source_id": { | ||
| 1661 | "name": "source_id", | ||
| 1662 | "type": "text", | ||
| 1663 | "primaryKey": false, | ||
| 1664 | "notNull": true, | ||
| 1665 | "autoincrement": false | ||
| 1666 | }, | ||
| 1667 | "name": { | ||
| 1668 | "name": "name", | ||
| 1669 | "type": "text", | ||
| 1670 | "primaryKey": false, | ||
| 1671 | "notNull": true, | ||
| 1672 | "autoincrement": false | ||
| 1673 | }, | ||
| 1674 | "kind": { | ||
| 1675 | "name": "kind", | ||
| 1676 | "type": "text", | ||
| 1677 | "primaryKey": false, | ||
| 1678 | "notNull": true, | ||
| 1679 | "autoincrement": false | ||
| 1680 | }, | ||
| 1681 | "text_value": { | ||
| 1682 | "name": "text_value", | ||
| 1683 | "type": "text", | ||
| 1684 | "primaryKey": false, | ||
| 1685 | "notNull": false, | ||
| 1686 | "autoincrement": false | ||
| 1687 | }, | ||
| 1688 | "secret_id": { | ||
| 1689 | "name": "secret_id", | ||
| 1690 | "type": "text", | ||
| 1691 | "primaryKey": false, | ||
| 1692 | "notNull": false, | ||
| 1693 | "autoincrement": false | ||
| 1694 | }, | ||
| 1695 | "secret_prefix": { | ||
| 1696 | "name": "secret_prefix", | ||
| 1697 | "type": "text", | ||
| 1698 | "primaryKey": false, | ||
| 1699 | "notNull": false, | ||
| 1700 | "autoincrement": false | ||
| 1701 | } | ||
| 1702 | }, | ||
| 1703 | "indexes": { | ||
| 1704 | "graphql_source_query_param_scope_id_idx": { | ||
| 1705 | "name": "graphql_source_query_param_scope_id_idx", | ||
| 1706 | "columns": [ | ||
| 1707 | "scope_id" | ||
| 1708 | ], | ||
| 1709 | "isUnique": false | ||
| 1710 | }, | ||
| 1711 | "graphql_source_query_param_source_id_idx": { | ||
| 1712 | "name": "graphql_source_query_param_source_id_idx", | ||
| 1713 | "columns": [ | ||
| 1714 | "source_id" | ||
| 1715 | ], | ||
| 1716 | "isUnique": false | ||
| 1717 | }, | ||
| 1718 | "graphql_source_query_param_secret_id_idx": { | ||
| 1719 | "name": "graphql_source_query_param_secret_id_idx", | ||
| 1720 | "columns": [ | ||
| 1721 | "secret_id" | ||
| 1722 | ], | ||
| 1723 | "isUnique": false | ||
| 1724 | } | ||
| 1725 | }, | ||
| 1726 | "foreignKeys": {}, | ||
| 1727 | "compositePrimaryKeys": { | ||
| 1728 | "graphql_source_query_param_scope_id_id_pk": { | ||
| 1729 | "columns": [ | ||
| 1730 | "scope_id", | ||
| 1731 | "id" | ||
| 1732 | ], | ||
| 1733 | "name": "graphql_source_query_param_scope_id_id_pk" | ||
| 1734 | } | ||
| 1735 | }, | ||
| 1736 | "uniqueConstraints": {}, | ||
| 1737 | "checkConstraints": {} | ||
| 1738 | }, | ||
| 1739 | "openapi_source_query_param": { | ||
| 1740 | "name": "openapi_source_query_param", | ||
| 1741 | "columns": { | ||
| 1742 | "id": { | ||
| 1743 | "name": "id", | ||
| 1744 | "type": "text", | ||
| 1745 | "primaryKey": false, | ||
| 1746 | "notNull": true, | ||
| 1747 | "autoincrement": false | ||
| 1748 | }, | ||
| 1749 | "scope_id": { | ||
| 1750 | "name": "scope_id", | ||
| 1751 | "type": "text", | ||
| 1752 | "primaryKey": false, | ||
| 1753 | "notNull": true, | ||
| 1754 | "autoincrement": false | ||
| 1755 | }, | ||
| 1756 | "source_id": { | ||
| 1757 | "name": "source_id", | ||
| 1758 | "type": "text", | ||
| 1759 | "primaryKey": false, | ||
| 1760 | "notNull": true, | ||
| 1761 | "autoincrement": false | ||
| 1762 | }, | ||
| 1763 | "name": { | ||
| 1764 | "name": "name", | ||
| 1765 | "type": "text", | ||
| 1766 | "primaryKey": false, | ||
| 1767 | "notNull": true, | ||
| 1768 | "autoincrement": false | ||
| 1769 | }, | ||
| 1770 | "kind": { | ||
| 1771 | "name": "kind", | ||
| 1772 | "type": "text", | ||
| 1773 | "primaryKey": false, | ||
| 1774 | "notNull": true, | ||
| 1775 | "autoincrement": false | ||
| 1776 | }, | ||
| 1777 | "text_value": { | ||
| 1778 | "name": "text_value", | ||
| 1779 | "type": "text", | ||
| 1780 | "primaryKey": false, | ||
| 1781 | "notNull": false, | ||
| 1782 | "autoincrement": false | ||
| 1783 | }, | ||
| 1784 | "secret_id": { | ||
| 1785 | "name": "secret_id", | ||
| 1786 | "type": "text", | ||
| 1787 | "primaryKey": false, | ||
| 1788 | "notNull": false, | ||
| 1789 | "autoincrement": false | ||
| 1790 | }, | ||
| 1791 | "secret_prefix": { | ||
| 1792 | "name": "secret_prefix", | ||
| 1793 | "type": "text", | ||
| 1794 | "primaryKey": false, | ||
| 1795 | "notNull": false, | ||
| 1796 | "autoincrement": false | ||
| 1797 | } | ||
| 1798 | }, | ||
| 1799 | "indexes": { | ||
| 1800 | "openapi_source_query_param_scope_id_idx": { | ||
| 1801 | "name": "openapi_source_query_param_scope_id_idx", | ||
| 1802 | "columns": [ | ||
| 1803 | "scope_id" | ||
| 1804 | ], | ||
| 1805 | "isUnique": false | ||
| 1806 | }, | ||
| 1807 | "openapi_source_query_param_source_id_idx": { | ||
| 1808 | "name": "openapi_source_query_param_source_id_idx", | ||
| 1809 | "columns": [ | ||
| 1810 | "source_id" | ||
| 1811 | ], | ||
| 1812 | "isUnique": false | ||
| 1813 | }, | ||
| 1814 | "openapi_source_query_param_secret_id_idx": { | ||
| 1815 | "name": "openapi_source_query_param_secret_id_idx", | ||
| 1816 | "columns": [ | ||
| 1817 | "secret_id" | ||
| 1818 | ], | ||
| 1819 | "isUnique": false | ||
| 1820 | } | ||
| 1821 | }, | ||
| 1822 | "foreignKeys": {}, | ||
| 1823 | "compositePrimaryKeys": { | ||
| 1824 | "openapi_source_query_param_scope_id_id_pk": { | ||
| 1825 | "columns": [ | ||
| 1826 | "scope_id", | ||
| 1827 | "id" | ||
| 1828 | ], | ||
| 1829 | "name": "openapi_source_query_param_scope_id_id_pk" | ||
| 1830 | } | ||
| 1831 | }, | ||
| 1832 | "uniqueConstraints": {}, | ||
| 1833 | "checkConstraints": {} | ||
| 1834 | }, | ||
| 1835 | "openapi_source_spec_fetch_header": { | ||
| 1836 | "name": "openapi_source_spec_fetch_header", | ||
| 1837 | "columns": { | ||
| 1838 | "id": { | ||
| 1839 | "name": "id", | ||
| 1840 | "type": "text", | ||
| 1841 | "primaryKey": false, | ||
| 1842 | "notNull": true, | ||
| 1843 | "autoincrement": false | ||
| 1844 | }, | ||
| 1845 | "scope_id": { | ||
| 1846 | "name": "scope_id", | ||
| 1847 | "type": "text", | ||
| 1848 | "primaryKey": false, | ||
| 1849 | "notNull": true, | ||
| 1850 | "autoincrement": false | ||
| 1851 | }, | ||
| 1852 | "source_id": { | ||
| 1853 | "name": "source_id", | ||
| 1854 | "type": "text", | ||
| 1855 | "primaryKey": false, | ||
| 1856 | "notNull": true, | ||
| 1857 | "autoincrement": false | ||
| 1858 | }, | ||
| 1859 | "name": { | ||
| 1860 | "name": "name", | ||
| 1861 | "type": "text", | ||
| 1862 | "primaryKey": false, | ||
| 1863 | "notNull": true, | ||
| 1864 | "autoincrement": false | ||
| 1865 | }, | ||
| 1866 | "kind": { | ||
| 1867 | "name": "kind", | ||
| 1868 | "type": "text", | ||
| 1869 | "primaryKey": false, | ||
| 1870 | "notNull": true, | ||
| 1871 | "autoincrement": false | ||
| 1872 | }, | ||
| 1873 | "text_value": { | ||
| 1874 | "name": "text_value", | ||
| 1875 | "type": "text", | ||
| 1876 | "primaryKey": false, | ||
| 1877 | "notNull": false, | ||
| 1878 | "autoincrement": false | ||
| 1879 | }, | ||
| 1880 | "secret_id": { | ||
| 1881 | "name": "secret_id", | ||
| 1882 | "type": "text", | ||
| 1883 | "primaryKey": false, | ||
| 1884 | "notNull": false, | ||
| 1885 | "autoincrement": false | ||
| 1886 | }, | ||
| 1887 | "secret_prefix": { | ||
| 1888 | "name": "secret_prefix", | ||
| 1889 | "type": "text", | ||
| 1890 | "primaryKey": false, | ||
| 1891 | "notNull": false, | ||
| 1892 | "autoincrement": false | ||
| 1893 | } | ||
| 1894 | }, | ||
| 1895 | "indexes": { | ||
| 1896 | "openapi_source_spec_fetch_header_scope_id_idx": { | ||
| 1897 | "name": "openapi_source_spec_fetch_header_scope_id_idx", | ||
| 1898 | "columns": [ | ||
| 1899 | "scope_id" | ||
| 1900 | ], | ||
| 1901 | "isUnique": false | ||
| 1902 | }, | ||
| 1903 | "openapi_source_spec_fetch_header_source_id_idx": { | ||
| 1904 | "name": "openapi_source_spec_fetch_header_source_id_idx", | ||
| 1905 | "columns": [ | ||
| 1906 | "source_id" | ||
| 1907 | ], | ||
| 1908 | "isUnique": false | ||
| 1909 | }, | ||
| 1910 | "openapi_source_spec_fetch_header_secret_id_idx": { | ||
| 1911 | "name": "openapi_source_spec_fetch_header_secret_id_idx", | ||
| 1912 | "columns": [ | ||
| 1913 | "secret_id" | ||
| 1914 | ], | ||
| 1915 | "isUnique": false | ||
| 1916 | } | ||
| 1917 | }, | ||
| 1918 | "foreignKeys": {}, | ||
| 1919 | "compositePrimaryKeys": { | ||
| 1920 | "openapi_source_spec_fetch_header_scope_id_id_pk": { | ||
| 1921 | "columns": [ | ||
| 1922 | "scope_id", | ||
| 1923 | "id" | ||
| 1924 | ], | ||
| 1925 | "name": "openapi_source_spec_fetch_header_scope_id_id_pk" | ||
| 1926 | } | ||
| 1927 | }, | ||
| 1928 | "uniqueConstraints": {}, | ||
| 1929 | "checkConstraints": {} | ||
| 1930 | }, | ||
| 1931 | "openapi_source_spec_fetch_query_param": { | ||
| 1932 | "name": "openapi_source_spec_fetch_query_param", | ||
| 1933 | "columns": { | ||
| 1934 | "id": { | ||
| 1935 | "name": "id", | ||
| 1936 | "type": "text", | ||
| 1937 | "primaryKey": false, | ||
| 1938 | "notNull": true, | ||
| 1939 | "autoincrement": false | ||
| 1940 | }, | ||
| 1941 | "scope_id": { | ||
| 1942 | "name": "scope_id", | ||
| 1943 | "type": "text", | ||
| 1944 | "primaryKey": false, | ||
| 1945 | "notNull": true, | ||
| 1946 | "autoincrement": false | ||
| 1947 | }, | ||
| 1948 | "source_id": { | ||
| 1949 | "name": "source_id", | ||
| 1950 | "type": "text", | ||
| 1951 | "primaryKey": false, | ||
| 1952 | "notNull": true, | ||
| 1953 | "autoincrement": false | ||
| 1954 | }, | ||
| 1955 | "name": { | ||
| 1956 | "name": "name", | ||
| 1957 | "type": "text", | ||
| 1958 | "primaryKey": false, | ||
| 1959 | "notNull": true, | ||
| 1960 | "autoincrement": false | ||
| 1961 | }, | ||
| 1962 | "kind": { | ||
| 1963 | "name": "kind", | ||
| 1964 | "type": "text", | ||
| 1965 | "primaryKey": false, | ||
| 1966 | "notNull": true, | ||
| 1967 | "autoincrement": false | ||
| 1968 | }, | ||
| 1969 | "text_value": { | ||
| 1970 | "name": "text_value", | ||
| 1971 | "type": "text", | ||
| 1972 | "primaryKey": false, | ||
| 1973 | "notNull": false, | ||
| 1974 | "autoincrement": false | ||
| 1975 | }, | ||
| 1976 | "secret_id": { | ||
| 1977 | "name": "secret_id", | ||
| 1978 | "type": "text", | ||
| 1979 | "primaryKey": false, | ||
| 1980 | "notNull": false, | ||
| 1981 | "autoincrement": false | ||
| 1982 | }, | ||
| 1983 | "secret_prefix": { | ||
| 1984 | "name": "secret_prefix", | ||
| 1985 | "type": "text", | ||
| 1986 | "primaryKey": false, | ||
| 1987 | "notNull": false, | ||
| 1988 | "autoincrement": false | ||
| 1989 | } | ||
| 1990 | }, | ||
| 1991 | "indexes": { | ||
| 1992 | "openapi_source_spec_fetch_query_param_scope_id_idx": { | ||
| 1993 | "name": "openapi_source_spec_fetch_query_param_scope_id_idx", | ||
| 1994 | "columns": [ | ||
| 1995 | "scope_id" | ||
| 1996 | ], | ||
| 1997 | "isUnique": false | ||
| 1998 | }, | ||
| 1999 | "openapi_source_spec_fetch_query_param_source_id_idx": { | ||
| 2000 | "name": "openapi_source_spec_fetch_query_param_source_id_idx", | ||
| 2001 | "columns": [ | ||
| 2002 | "source_id" | ||
| 2003 | ], | ||
| 2004 | "isUnique": false | ||
| 2005 | }, | ||
| 2006 | "openapi_source_spec_fetch_query_param_secret_id_idx": { | ||
| 2007 | "name": "openapi_source_spec_fetch_query_param_secret_id_idx", | ||
| 2008 | "columns": [ | ||
| 2009 | "secret_id" | ||
| 2010 | ], | ||
| 2011 | "isUnique": false | ||
| 2012 | } | ||
| 2013 | }, | ||
| 2014 | "foreignKeys": {}, | ||
| 2015 | "compositePrimaryKeys": { | ||
| 2016 | "openapi_source_spec_fetch_query_param_scope_id_id_pk": { | ||
| 2017 | "columns": [ | ||
| 2018 | "scope_id", | ||
| 2019 | "id" | ||
| 2020 | ], | ||
| 2021 | "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk" | ||
| 2022 | } | ||
| 2023 | }, | ||
| 2024 | "uniqueConstraints": {}, | ||
| 2025 | "checkConstraints": {} | ||
| 2026 | }, | ||
| 2027 | "mcp_source_header": { | ||
| 2028 | "name": "mcp_source_header", | ||
| 2029 | "columns": { | ||
| 2030 | "id": { | ||
| 2031 | "name": "id", | ||
| 2032 | "type": "text", | ||
| 2033 | "primaryKey": false, | ||
| 2034 | "notNull": true, | ||
| 2035 | "autoincrement": false | ||
| 2036 | }, | ||
| 2037 | "scope_id": { | ||
| 2038 | "name": "scope_id", | ||
| 2039 | "type": "text", | ||
| 2040 | "primaryKey": false, | ||
| 2041 | "notNull": true, | ||
| 2042 | "autoincrement": false | ||
| 2043 | }, | ||
| 2044 | "source_id": { | ||
| 2045 | "name": "source_id", | ||
| 2046 | "type": "text", | ||
| 2047 | "primaryKey": false, | ||
| 2048 | "notNull": true, | ||
| 2049 | "autoincrement": false | ||
| 2050 | }, | ||
| 2051 | "name": { | ||
| 2052 | "name": "name", | ||
| 2053 | "type": "text", | ||
| 2054 | "primaryKey": false, | ||
| 2055 | "notNull": true, | ||
| 2056 | "autoincrement": false | ||
| 2057 | }, | ||
| 2058 | "kind": { | ||
| 2059 | "name": "kind", | ||
| 2060 | "type": "text", | ||
| 2061 | "primaryKey": false, | ||
| 2062 | "notNull": true, | ||
| 2063 | "autoincrement": false | ||
| 2064 | }, | ||
| 2065 | "text_value": { | ||
| 2066 | "name": "text_value", | ||
| 2067 | "type": "text", | ||
| 2068 | "primaryKey": false, | ||
| 2069 | "notNull": false, | ||
| 2070 | "autoincrement": false | ||
| 2071 | }, | ||
| 2072 | "secret_id": { | ||
| 2073 | "name": "secret_id", | ||
| 2074 | "type": "text", | ||
| 2075 | "primaryKey": false, | ||
| 2076 | "notNull": false, | ||
| 2077 | "autoincrement": false | ||
| 2078 | }, | ||
| 2079 | "secret_prefix": { | ||
| 2080 | "name": "secret_prefix", | ||
| 2081 | "type": "text", | ||
| 2082 | "primaryKey": false, | ||
| 2083 | "notNull": false, | ||
| 2084 | "autoincrement": false | ||
| 2085 | } | ||
| 2086 | }, | ||
| 2087 | "indexes": { | ||
| 2088 | "mcp_source_header_scope_id_idx": { | ||
| 2089 | "name": "mcp_source_header_scope_id_idx", | ||
| 2090 | "columns": [ | ||
| 2091 | "scope_id" | ||
| 2092 | ], | ||
| 2093 | "isUnique": false | ||
| 2094 | }, | ||
| 2095 | "mcp_source_header_source_id_idx": { | ||
| 2096 | "name": "mcp_source_header_source_id_idx", | ||
| 2097 | "columns": [ | ||
| 2098 | "source_id" | ||
| 2099 | ], | ||
| 2100 | "isUnique": false | ||
| 2101 | }, | ||
| 2102 | "mcp_source_header_secret_id_idx": { | ||
| 2103 | "name": "mcp_source_header_secret_id_idx", | ||
| 2104 | "columns": [ | ||
| 2105 | "secret_id" | ||
| 2106 | ], | ||
| 2107 | "isUnique": false | ||
| 2108 | } | ||
| 2109 | }, | ||
| 2110 | "foreignKeys": {}, | ||
| 2111 | "compositePrimaryKeys": { | ||
| 2112 | "mcp_source_header_scope_id_id_pk": { | ||
| 2113 | "columns": [ | ||
| 2114 | "scope_id", | ||
| 2115 | "id" | ||
| 2116 | ], | ||
| 2117 | "name": "mcp_source_header_scope_id_id_pk" | ||
| 2118 | } | ||
| 2119 | }, | ||
| 2120 | "uniqueConstraints": {}, | ||
| 2121 | "checkConstraints": {} | ||
| 2122 | }, | ||
| 2123 | "mcp_source_query_param": { | ||
| 2124 | "name": "mcp_source_query_param", | ||
| 2125 | "columns": { | ||
| 2126 | "id": { | ||
| 2127 | "name": "id", | ||
| 2128 | "type": "text", | ||
| 2129 | "primaryKey": false, | ||
| 2130 | "notNull": true, | ||
| 2131 | "autoincrement": false | ||
| 2132 | }, | ||
| 2133 | "scope_id": { | ||
| 2134 | "name": "scope_id", | ||
| 2135 | "type": "text", | ||
| 2136 | "primaryKey": false, | ||
| 2137 | "notNull": true, | ||
| 2138 | "autoincrement": false | ||
| 2139 | }, | ||
| 2140 | "source_id": { | ||
| 2141 | "name": "source_id", | ||
| 2142 | "type": "text", | ||
| 2143 | "primaryKey": false, | ||
| 2144 | "notNull": true, | ||
| 2145 | "autoincrement": false | ||
| 2146 | }, | ||
| 2147 | "name": { | ||
| 2148 | "name": "name", | ||
| 2149 | "type": "text", | ||
| 2150 | "primaryKey": false, | ||
| 2151 | "notNull": true, | ||
| 2152 | "autoincrement": false | ||
| 2153 | }, | ||
| 2154 | "kind": { | ||
| 2155 | "name": "kind", | ||
| 2156 | "type": "text", | ||
| 2157 | "primaryKey": false, | ||
| 2158 | "notNull": true, | ||
| 2159 | "autoincrement": false | ||
| 2160 | }, | ||
| 2161 | "text_value": { | ||
| 2162 | "name": "text_value", | ||
| 2163 | "type": "text", | ||
| 2164 | "primaryKey": false, | ||
| 2165 | "notNull": false, | ||
| 2166 | "autoincrement": false | ||
| 2167 | }, | ||
| 2168 | "secret_id": { | ||
| 2169 | "name": "secret_id", | ||
| 2170 | "type": "text", | ||
| 2171 | "primaryKey": false, | ||
| 2172 | "notNull": false, | ||
| 2173 | "autoincrement": false | ||
| 2174 | }, | ||
| 2175 | "secret_prefix": { | ||
| 2176 | "name": "secret_prefix", | ||
| 2177 | "type": "text", | ||
| 2178 | "primaryKey": false, | ||
| 2179 | "notNull": false, | ||
| 2180 | "autoincrement": false | ||
| 2181 | } | ||
| 2182 | }, | ||
| 2183 | "indexes": { | ||
| 2184 | "mcp_source_query_param_scope_id_idx": { | ||
| 2185 | "name": "mcp_source_query_param_scope_id_idx", | ||
| 2186 | "columns": [ | ||
| 2187 | "scope_id" | ||
| 2188 | ], | ||
| 2189 | "isUnique": false | ||
| 2190 | }, | ||
| 2191 | "mcp_source_query_param_source_id_idx": { | ||
| 2192 | "name": "mcp_source_query_param_source_id_idx", | ||
| 2193 | "columns": [ | ||
| 2194 | "source_id" | ||
| 2195 | ], | ||
| 2196 | "isUnique": false | ||
| 2197 | }, | ||
| 2198 | "mcp_source_query_param_secret_id_idx": { | ||
| 2199 | "name": "mcp_source_query_param_secret_id_idx", | ||
| 2200 | "columns": [ | ||
| 2201 | "secret_id" | ||
| 2202 | ], | ||
| 2203 | "isUnique": false | ||
| 2204 | } | ||
| 2205 | }, | ||
| 2206 | "foreignKeys": {}, | ||
| 2207 | "compositePrimaryKeys": { | ||
| 2208 | "mcp_source_query_param_scope_id_id_pk": { | ||
| 2209 | "columns": [ | ||
| 2210 | "scope_id", | ||
| 2211 | "id" | ||
| 2212 | ], | ||
| 2213 | "name": "mcp_source_query_param_scope_id_id_pk" | ||
| 2214 | } | ||
| 2215 | }, | ||
| 2216 | "uniqueConstraints": {}, | ||
| 2217 | "checkConstraints": {} | ||
| 2218 | }, | ||
| 2219 | "google_discovery_source_credential_header": { | ||
| 2220 | "name": "google_discovery_source_credential_header", | ||
| 2221 | "columns": { | ||
| 2222 | "id": { | ||
| 2223 | "name": "id", | ||
| 2224 | "type": "text", | ||
| 2225 | "primaryKey": false, | ||
| 2226 | "notNull": true, | ||
| 2227 | "autoincrement": false | ||
| 2228 | }, | ||
| 2229 | "scope_id": { | ||
| 2230 | "name": "scope_id", | ||
| 2231 | "type": "text", | ||
| 2232 | "primaryKey": false, | ||
| 2233 | "notNull": true, | ||
| 2234 | "autoincrement": false | ||
| 2235 | }, | ||
| 2236 | "source_id": { | ||
| 2237 | "name": "source_id", | ||
| 2238 | "type": "text", | ||
| 2239 | "primaryKey": false, | ||
| 2240 | "notNull": true, | ||
| 2241 | "autoincrement": false | ||
| 2242 | }, | ||
| 2243 | "name": { | ||
| 2244 | "name": "name", | ||
| 2245 | "type": "text", | ||
| 2246 | "primaryKey": false, | ||
| 2247 | "notNull": true, | ||
| 2248 | "autoincrement": false | ||
| 2249 | }, | ||
| 2250 | "kind": { | ||
| 2251 | "name": "kind", | ||
| 2252 | "type": "text", | ||
| 2253 | "primaryKey": false, | ||
| 2254 | "notNull": true, | ||
| 2255 | "autoincrement": false | ||
| 2256 | }, | ||
| 2257 | "text_value": { | ||
| 2258 | "name": "text_value", | ||
| 2259 | "type": "text", | ||
| 2260 | "primaryKey": false, | ||
| 2261 | "notNull": false, | ||
| 2262 | "autoincrement": false | ||
| 2263 | }, | ||
| 2264 | "secret_id": { | ||
| 2265 | "name": "secret_id", | ||
| 2266 | "type": "text", | ||
| 2267 | "primaryKey": false, | ||
| 2268 | "notNull": false, | ||
| 2269 | "autoincrement": false | ||
| 2270 | }, | ||
| 2271 | "secret_prefix": { | ||
| 2272 | "name": "secret_prefix", | ||
| 2273 | "type": "text", | ||
| 2274 | "primaryKey": false, | ||
| 2275 | "notNull": false, | ||
| 2276 | "autoincrement": false | ||
| 2277 | } | ||
| 2278 | }, | ||
| 2279 | "indexes": { | ||
| 2280 | "google_discovery_source_credential_header_scope_id_idx": { | ||
| 2281 | "name": "google_discovery_source_credential_header_scope_id_idx", | ||
| 2282 | "columns": [ | ||
| 2283 | "scope_id" | ||
| 2284 | ], | ||
| 2285 | "isUnique": false | ||
| 2286 | }, | ||
| 2287 | "google_discovery_source_credential_header_source_id_idx": { | ||
| 2288 | "name": "google_discovery_source_credential_header_source_id_idx", | ||
| 2289 | "columns": [ | ||
| 2290 | "source_id" | ||
| 2291 | ], | ||
| 2292 | "isUnique": false | ||
| 2293 | }, | ||
| 2294 | "google_discovery_source_credential_header_secret_id_idx": { | ||
| 2295 | "name": "google_discovery_source_credential_header_secret_id_idx", | ||
| 2296 | "columns": [ | ||
| 2297 | "secret_id" | ||
| 2298 | ], | ||
| 2299 | "isUnique": false | ||
| 2300 | } | ||
| 2301 | }, | ||
| 2302 | "foreignKeys": {}, | ||
| 2303 | "compositePrimaryKeys": { | ||
| 2304 | "google_discovery_source_credential_header_scope_id_id_pk": { | ||
| 2305 | "columns": [ | ||
| 2306 | "scope_id", | ||
| 2307 | "id" | ||
| 2308 | ], | ||
| 2309 | "name": "google_discovery_source_credential_header_scope_id_id_pk" | ||
| 2310 | } | ||
| 2311 | }, | ||
| 2312 | "uniqueConstraints": {}, | ||
| 2313 | "checkConstraints": {} | ||
| 2314 | }, | ||
| 2315 | "google_discovery_source_credential_query_param": { | ||
| 2316 | "name": "google_discovery_source_credential_query_param", | ||
| 2317 | "columns": { | ||
| 2318 | "id": { | ||
| 2319 | "name": "id", | ||
| 2320 | "type": "text", | ||
| 2321 | "primaryKey": false, | ||
| 2322 | "notNull": true, | ||
| 2323 | "autoincrement": false | ||
| 2324 | }, | ||
| 2325 | "scope_id": { | ||
| 2326 | "name": "scope_id", | ||
| 2327 | "type": "text", | ||
| 2328 | "primaryKey": false, | ||
| 2329 | "notNull": true, | ||
| 2330 | "autoincrement": false | ||
| 2331 | }, | ||
| 2332 | "source_id": { | ||
| 2333 | "name": "source_id", | ||
| 2334 | "type": "text", | ||
| 2335 | "primaryKey": false, | ||
| 2336 | "notNull": true, | ||
| 2337 | "autoincrement": false | ||
| 2338 | }, | ||
| 2339 | "name": { | ||
| 2340 | "name": "name", | ||
| 2341 | "type": "text", | ||
| 2342 | "primaryKey": false, | ||
| 2343 | "notNull": true, | ||
| 2344 | "autoincrement": false | ||
| 2345 | }, | ||
| 2346 | "kind": { | ||
| 2347 | "name": "kind", | ||
| 2348 | "type": "text", | ||
| 2349 | "primaryKey": false, | ||
| 2350 | "notNull": true, | ||
| 2351 | "autoincrement": false | ||
| 2352 | }, | ||
| 2353 | "text_value": { | ||
| 2354 | "name": "text_value", | ||
| 2355 | "type": "text", | ||
| 2356 | "primaryKey": false, | ||
| 2357 | "notNull": false, | ||
| 2358 | "autoincrement": false | ||
| 2359 | }, | ||
| 2360 | "secret_id": { | ||
| 2361 | "name": "secret_id", | ||
| 2362 | "type": "text", | ||
| 2363 | "primaryKey": false, | ||
| 2364 | "notNull": false, | ||
| 2365 | "autoincrement": false | ||
| 2366 | }, | ||
| 2367 | "secret_prefix": { | ||
| 2368 | "name": "secret_prefix", | ||
| 2369 | "type": "text", | ||
| 2370 | "primaryKey": false, | ||
| 2371 | "notNull": false, | ||
| 2372 | "autoincrement": false | ||
| 2373 | } | ||
| 2374 | }, | ||
| 2375 | "indexes": { | ||
| 2376 | "google_discovery_source_credential_query_param_scope_id_idx": { | ||
| 2377 | "name": "google_discovery_source_credential_query_param_scope_id_idx", | ||
| 2378 | "columns": [ | ||
| 2379 | "scope_id" | ||
| 2380 | ], | ||
| 2381 | "isUnique": false | ||
| 2382 | }, | ||
| 2383 | "google_discovery_source_credential_query_param_source_id_idx": { | ||
| 2384 | "name": "google_discovery_source_credential_query_param_source_id_idx", | ||
| 2385 | "columns": [ | ||
| 2386 | "source_id" | ||
| 2387 | ], | ||
| 2388 | "isUnique": false | ||
| 2389 | }, | ||
| 2390 | "google_discovery_source_credential_query_param_secret_id_idx": { | ||
| 2391 | "name": "google_discovery_source_credential_query_param_secret_id_idx", | ||
| 2392 | "columns": [ | ||
| 2393 | "secret_id" | ||
| 2394 | ], | ||
| 2395 | "isUnique": false | ||
| 2396 | } | ||
| 2397 | }, | ||
| 2398 | "foreignKeys": {}, | ||
| 2399 | "compositePrimaryKeys": { | ||
| 2400 | "google_discovery_source_credential_query_param_scope_id_id_pk": { | ||
| 2401 | "columns": [ | ||
| 2402 | "scope_id", | ||
| 2403 | "id" | ||
| 2404 | ], | ||
| 2405 | "name": "google_discovery_source_credential_query_param_scope_id_id_pk" | ||
| 2406 | } | ||
| 2407 | }, | ||
| 2408 | "uniqueConstraints": {}, | ||
| 2409 | "checkConstraints": {} | ||
| 2410 | } | ||
| 2411 | }, | ||
| 2412 | "views": {}, | ||
| 2413 | "enums": {}, | ||
| 2414 | "_meta": { | ||
| 2415 | "schemas": {}, | ||
| 2416 | "tables": {}, | ||
| 2417 | "columns": {} | ||
| 2418 | }, | ||
| 2419 | "internal": { | ||
| 2420 | "indexes": {} | ||
| 2421 | } | ||
| 2422 | } | ||
| @@ -50,6 +50,13 @@ · 49 unmodified lines | |||
| 50 | "when": 1777850000001, | 50 | "when": 1777850000001, |
| 51 | "tag": "0006_neat_terror", | 51 | "tag": "0006_neat_terror", |
| 52 | "breakpoints": true | 52 | "breakpoints": true |
| 53 | }, | ||
| 54 | { | ||
| 55 | "idx": 7, | ||
| 56 | "version": "6", | ||
| 57 | "when": 1778100000000, | ||
| 58 | "tag": "0007_normalize_plugin_secret_refs", | ||
| 59 | "breakpoints": true | ||
| 53 | } | 60 | } |
| 54 | ] | 61 | ] |
| 55 | } | 62 | } |
| @@ -0,0 +1,107 @@ | |||
| 1 | // Shared pre-migration schema used by the four migrate-*-bindings.test.ts | ||
| 2 | // suites. Each test seeds this hand-rolled DDL (the DB shape immediately | ||
| 3 | // after 0006_neat_terror), then runs drizzle's migrator which executes | ||
| 4 | // only `0007_normalize_plugin_secret_refs.sql` thanks to the stamp. | ||
| 5 | |||
| 6 | import { Database } from "bun:sqlite"; | ||
| 7 | |||
| 8 | export const PRE_0007_SQL = ` | ||
| 9 | CREATE TABLE __drizzle_migrations ( | ||
| 10 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 11 | hash TEXT NOT NULL, | ||
| 12 | created_at NUMERIC | ||
| 13 | ); | ||
| 14 | |||
| 15 | CREATE TABLE graphql_source ( | ||
| 16 | id TEXT NOT NULL, | ||
| 17 | scope_id TEXT NOT NULL, | ||
| 18 | name TEXT NOT NULL, | ||
| 19 | endpoint TEXT NOT NULL, | ||
| 20 | headers TEXT, | ||
| 21 | query_params TEXT, | ||
| 22 | auth TEXT, | ||
| 23 | PRIMARY KEY (scope_id, id) | ||
| 24 | ); | ||
| 25 | |||
| 26 | CREATE TABLE graphql_operation ( | ||
| 27 | id TEXT NOT NULL, | ||
| 28 | scope_id TEXT NOT NULL, | ||
| 29 | source_id TEXT NOT NULL, | ||
| 30 | binding TEXT NOT NULL, | ||
| 31 | PRIMARY KEY (scope_id, id) | ||
| 32 | ); | ||
| 33 | |||
| 34 | CREATE TABLE openapi_source ( | ||
| 35 | id TEXT NOT NULL, | ||
| 36 | scope_id TEXT NOT NULL, | ||
| 37 | name TEXT NOT NULL, | ||
| 38 | spec TEXT NOT NULL, | ||
| 39 | source_url TEXT, | ||
| 40 | base_url TEXT, | ||
| 41 | headers TEXT, | ||
| 42 | query_params TEXT, | ||
| 43 | oauth2 TEXT, | ||
| 44 | invocation_config TEXT NOT NULL, | ||
| 45 | PRIMARY KEY (scope_id, id) | ||
| 46 | ); | ||
| 47 | |||
| 48 | CREATE TABLE openapi_source_binding ( | ||
| 49 | id TEXT PRIMARY KEY NOT NULL, | ||
| 50 | source_id TEXT NOT NULL, | ||
| 51 | source_scope_id TEXT NOT NULL, | ||
| 52 | target_scope_id TEXT NOT NULL, | ||
| 53 | slot TEXT NOT NULL, | ||
| 54 | value TEXT NOT NULL, | ||
| 55 | created_at INTEGER NOT NULL, | ||
| 56 | updated_at INTEGER NOT NULL | ||
| 57 | ); | ||
| 58 | |||
| 59 | CREATE TABLE mcp_source ( | ||
| 60 | id TEXT NOT NULL, | ||
| 61 | scope_id TEXT NOT NULL, | ||
| 62 | name TEXT NOT NULL, | ||
| 63 | config TEXT NOT NULL, | ||
| 64 | created_at INTEGER NOT NULL, | ||
| 65 | PRIMARY KEY (scope_id, id) | ||
| 66 | ); | ||
| 67 | |||
| 68 | CREATE TABLE mcp_binding ( | ||
| 69 | id TEXT NOT NULL, | ||
| 70 | scope_id TEXT NOT NULL, | ||
| 71 | source_id TEXT NOT NULL, | ||
| 72 | binding TEXT NOT NULL, | ||
| 73 | created_at INTEGER NOT NULL, | ||
| 74 | PRIMARY KEY (scope_id, id) | ||
| 75 | ); | ||
| 76 | |||
| 77 | CREATE TABLE google_discovery_source ( | ||
| 78 | id TEXT NOT NULL, | ||
| 79 | scope_id TEXT NOT NULL, | ||
| 80 | name TEXT NOT NULL, | ||
| 81 | config TEXT NOT NULL, | ||
| 82 | created_at INTEGER NOT NULL, | ||
| 83 | updated_at INTEGER NOT NULL, | ||
| 84 | PRIMARY KEY (scope_id, id) | ||
| 85 | ); | ||
| 86 | |||
| 87 | CREATE TABLE google_discovery_binding ( | ||
| 88 | id TEXT NOT NULL, | ||
| 89 | scope_id TEXT NOT NULL, | ||
| 90 | source_id TEXT NOT NULL, | ||
| 91 | binding TEXT NOT NULL, | ||
| 92 | created_at INTEGER NOT NULL, | ||
| 93 | PRIMARY KEY (scope_id, id) | ||
| 94 | ); | ||
| 95 | `; | ||
| 96 | |||
| 97 | // 0006_neat_terror.when. drizzle's sqlite migrator picks the latest | ||
| 98 | // `created_at` from __drizzle_migrations and skips any migration whose | ||
| 99 | // folderMillis (from the journal) is <= that timestamp. | ||
| 100 | export const STAMP_BEFORE = 1777850000001; | ||
| 101 | |||
| 102 | export const stampPriorMigrationsApplied = (db: Database) => { | ||
| 103 | db.prepare("INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)").run( | ||
| 104 | "pre-0007-marker", | ||
| 105 | STAMP_BEFORE, | ||
| 106 | ); | ||
| 107 | }; | ||
| @@ -122,9 +122,7 @@ export const openapi_source = sqliteTable("openapi_source", { · 121 unmodified lines | |||
| 122 | source_url: text('source_url'), | 122 | source_url: text('source_url'), |
| 123 | base_url: text('base_url'), | 123 | base_url: text('base_url'), |
| 124 | headers: text('headers', { mode: "json" }), | 124 | headers: text('headers', { mode: "json" }), |
| 125 | query_params: text('query_params', { mode: "json" }), | 125 | oauth2: text('oauth2', { mode: "json" }) |
| 126 | oauth2: text('oauth2', { mode: "json" }), | ||
| 127 | invocation_config: text('invocation_config', { mode: "json" }).notNull() | ||
| 128 | }, (table) => [ | 126 | }, (table) => [ |
| 129 | primaryKey({ columns: [table.scope_id, table.id] }), | 127 | primaryKey({ columns: [table.scope_id, table.id] }), |
| 130 | index("openapi_source_scope_id_idx").on(table.scope_id), | 128 | index("openapi_source_scope_id_idx").on(table.scope_id), |
| @@ -147,7 +145,10 @@ export const openapi_source_binding = sqliteTable("openapi_source_binding", { · 16 unmodified lines | |||
| 147 | source_scope_id: text('source_scope_id').notNull(), | 145 | source_scope_id: text('source_scope_id').notNull(), |
| 148 | target_scope_id: text('target_scope_id').notNull(), | 146 | target_scope_id: text('target_scope_id').notNull(), |
| 149 | slot: text('slot').notNull(), | 147 | slot: text('slot').notNull(), |
| 150 | value: text('value', { mode: "json" }).notNull(), | 148 | kind: text({ enum: ['secret', 'connection', 'text'] }).notNull(), |
| 149 | secret_id: text('secret_id'), | ||
| 150 | connection_id: text('connection_id'), | ||
| 151 | text_value: text('text_value'), | ||
| 151 | created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(), | 152 | created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(), |
| 152 | updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull() | 153 | updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull() |
| 153 | }, (table) => [ | 154 | }, (table) => [ |
| @@ -155,6 +156,56 @@ export const openapi_source_binding = sqliteTable("openapi_source_binding", { · 1 unmodified line | |||
| 155 | index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id), | 156 | index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id), |
| 156 | index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id), | 157 | index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id), |
| 157 | index("openapi_source_binding_slot_idx").on(table.slot), | 158 | index("openapi_source_binding_slot_idx").on(table.slot), |
| 159 | index("openapi_source_binding_secret_id_idx").on(table.secret_id), | ||
| 160 | index("openapi_source_binding_connection_id_idx").on(table.connection_id), | ||
| 161 | ]); | ||
| 162 | |||
| 163 | export const openapi_source_query_param = sqliteTable("openapi_source_query_param", { | ||
| 164 | id: text('id').notNull(), | ||
| 165 | scope_id: text('scope_id').notNull(), | ||
| 166 | source_id: text('source_id').notNull(), | ||
| 167 | name: text('name').notNull(), | ||
| 168 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 169 | text_value: text('text_value'), | ||
| 170 | secret_id: text('secret_id'), | ||
| 171 | secret_prefix: text('secret_prefix') | ||
| 172 | }, (table) => [ | ||
| 173 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 174 | index("openapi_source_query_param_scope_id_idx").on(table.scope_id), | ||
| 175 | index("openapi_source_query_param_source_id_idx").on(table.source_id), | ||
| 176 | index("openapi_source_query_param_secret_id_idx").on(table.secret_id), | ||
| 177 | ]); | ||
| 178 | |||
| 179 | export const openapi_source_spec_fetch_header = sqliteTable("openapi_source_spec_fetch_header", { | ||
| 180 | id: text('id').notNull(), | ||
| 181 | scope_id: text('scope_id').notNull(), | ||
| 182 | source_id: text('source_id').notNull(), | ||
| 183 | name: text('name').notNull(), | ||
| 184 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 185 | text_value: text('text_value'), | ||
| 186 | secret_id: text('secret_id'), | ||
| 187 | secret_prefix: text('secret_prefix') | ||
| 188 | }, (table) => [ | ||
| 189 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 190 | index("openapi_source_spec_fetch_header_scope_id_idx").on(table.scope_id), | ||
| 191 | index("openapi_source_spec_fetch_header_source_id_idx").on(table.source_id), | ||
| 192 | index("openapi_source_spec_fetch_header_secret_id_idx").on(table.secret_id), | ||
| 193 | ]); | ||
| 194 | |||
| 195 | export const openapi_source_spec_fetch_query_param = sqliteTable("openapi_source_spec_fetch_query_param", { | ||
| 196 | id: text('id').notNull(), | ||
| 197 | scope_id: text('scope_id').notNull(), | ||
| 198 | source_id: text('source_id').notNull(), | ||
| 199 | name: text('name').notNull(), | ||
| 200 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 201 | text_value: text('text_value'), | ||
| 202 | secret_id: text('secret_id'), | ||
| 203 | secret_prefix: text('secret_prefix') | ||
| 204 | }, (table) => [ | ||
| 205 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 206 | index("openapi_source_spec_fetch_query_param_scope_id_idx").on(table.scope_id), | ||
| 207 | index("openapi_source_spec_fetch_query_param_source_id_idx").on(table.source_id), | ||
| 208 | index("openapi_source_spec_fetch_query_param_secret_id_idx").on(table.secret_id), | ||
| 158 | ]); | 209 | ]); |
| 159 | 210 | ||
| 160 | export const mcp_source = sqliteTable("mcp_source", { | 211 | export const mcp_source = sqliteTable("mcp_source", { |
| @@ -162,10 +213,53 @@ export const mcp_source = sqliteTable("mcp_source", { · 1 unmodified line | |||
| 162 | scope_id: text('scope_id').notNull(), | 213 | scope_id: text('scope_id').notNull(), |
| 163 | name: text('name').notNull(), | 214 | name: text('name').notNull(), |
| 164 | config: text('config', { mode: "json" }).notNull(), | 215 | config: text('config', { mode: "json" }).notNull(), |
| 216 | auth_kind: text({ enum: ['none', 'header', 'oauth2'] }).default("none").notNull(), | ||
| 217 | auth_header_name: text('auth_header_name'), | ||
| 218 | auth_secret_id: text('auth_secret_id'), | ||
| 219 | auth_secret_prefix: text('auth_secret_prefix'), | ||
| 220 | auth_connection_id: text('auth_connection_id'), | ||
| 221 | auth_client_id_secret_id: text('auth_client_id_secret_id'), | ||
| 222 | auth_client_secret_secret_id: text('auth_client_secret_secret_id'), | ||
| 165 | created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull() | 223 | created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull() |
| 166 | }, (table) => [ | 224 | }, (table) => [ |
| 167 | primaryKey({ columns: [table.scope_id, table.id] }), | 225 | primaryKey({ columns: [table.scope_id, table.id] }), |
| 168 | index("mcp_source_scope_id_idx").on(table.scope_id), | 226 | index("mcp_source_scope_id_idx").on(table.scope_id), |
| 227 | index("mcp_source_auth_secret_id_idx").on(table.auth_secret_id), | ||
| 228 | index("mcp_source_auth_connection_id_idx").on(table.auth_connection_id), | ||
| 229 | index("mcp_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id), | ||
| 230 | index("mcp_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id), | ||
| 231 | ]); | ||
| 232 | |||
| 233 | export const mcp_source_header = sqliteTable("mcp_source_header", { | ||
| 234 | id: text('id').notNull(), | ||
| 235 | scope_id: text('scope_id').notNull(), | ||
| 236 | source_id: text('source_id').notNull(), | ||
| 237 | name: text('name').notNull(), | ||
| 238 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 239 | text_value: text('text_value'), | ||
| 240 | secret_id: text('secret_id'), | ||
| 241 | secret_prefix: text('secret_prefix') | ||
| 242 | }, (table) => [ | ||
| 243 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 244 | index("mcp_source_header_scope_id_idx").on(table.scope_id), | ||
| 245 | index("mcp_source_header_source_id_idx").on(table.source_id), | ||
| 246 | index("mcp_source_header_secret_id_idx").on(table.secret_id), | ||
| 247 | ]); | ||
| 248 | |||
| 249 | export const mcp_source_query_param = sqliteTable("mcp_source_query_param", { | ||
| 250 | id: text('id').notNull(), | ||
| 251 | scope_id: text('scope_id').notNull(), | ||
| 252 | source_id: text('source_id').notNull(), | ||
| 253 | name: text('name').notNull(), | ||
| 254 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 255 | text_value: text('text_value'), | ||
| 256 | secret_id: text('secret_id'), | ||
| 257 | secret_prefix: text('secret_prefix') | ||
| 258 | }, (table) => [ | ||
| 259 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 260 | index("mcp_source_query_param_scope_id_idx").on(table.scope_id), | ||
| 261 | index("mcp_source_query_param_source_id_idx").on(table.source_id), | ||
| 262 | index("mcp_source_query_param_secret_id_idx").on(table.secret_id), | ||
| 169 | ]); | 263 | ]); |
| 170 | 264 | ||
| 171 | export const mcp_binding = sqliteTable("mcp_binding", { | 265 | export const mcp_binding = sqliteTable("mcp_binding", { |
| @@ -185,11 +279,51 @@ export const google_discovery_source = sqliteTable("google_discovery_source", { · 13 unmodified lines | |||
| 185 | scope_id: text('scope_id').notNull(), | 279 | scope_id: text('scope_id').notNull(), |
| 186 | name: text('name').notNull(), | 280 | name: text('name').notNull(), |
| 187 | config: text('config', { mode: "json" }).notNull(), | 281 | config: text('config', { mode: "json" }).notNull(), |
| 282 | auth_kind: text({ enum: ['none', 'oauth2'] }).default("none").notNull(), | ||
| 283 | auth_connection_id: text('auth_connection_id'), | ||
| 284 | auth_client_id_secret_id: text('auth_client_id_secret_id'), | ||
| 285 | auth_client_secret_secret_id: text('auth_client_secret_secret_id'), | ||
| 286 | auth_scopes: text('auth_scopes', { mode: "json" }), | ||
| 188 | created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(), | 287 | created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(), |
| 189 | updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull() | 288 | updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull() |
| 190 | }, (table) => [ | 289 | }, (table) => [ |
| 191 | primaryKey({ columns: [table.scope_id, table.id] }), | 290 | primaryKey({ columns: [table.scope_id, table.id] }), |
| 192 | index("google_discovery_source_scope_id_idx").on(table.scope_id), | 291 | index("google_discovery_source_scope_id_idx").on(table.scope_id), |
| 292 | index("google_discovery_source_auth_connection_id_idx").on(table.auth_connection_id), | ||
| 293 | index("google_discovery_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id), | ||
| 294 | index("google_discovery_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id), | ||
| 295 | ]); | ||
| 296 | |||
| 297 | export const google_discovery_source_credential_header = sqliteTable("google_discovery_source_credential_header", { | ||
| 298 | id: text('id').notNull(), | ||
| 299 | scope_id: text('scope_id').notNull(), | ||
| 300 | source_id: text('source_id').notNull(), | ||
| 301 | name: text('name').notNull(), | ||
| 302 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 303 | text_value: text('text_value'), | ||
| 304 | secret_id: text('secret_id'), | ||
| 305 | secret_prefix: text('secret_prefix') | ||
| 306 | }, (table) => [ | ||
| 307 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 308 | index("google_discovery_source_credential_header_scope_id_idx").on(table.scope_id), | ||
| 309 | index("google_discovery_source_credential_header_source_id_idx").on(table.source_id), | ||
| 310 | index("google_discovery_source_credential_header_secret_id_idx").on(table.secret_id), | ||
| 311 | ]); | ||
| 312 | |||
| 313 | export const google_discovery_source_credential_query_param = sqliteTable("google_discovery_source_credential_query_param", { | ||
| 314 | id: text('id').notNull(), | ||
| 315 | scope_id: text('scope_id').notNull(), | ||
| 316 | source_id: text('source_id').notNull(), | ||
| 317 | name: text('name').notNull(), | ||
| 318 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 319 | text_value: text('text_value'), | ||
| 320 | secret_id: text('secret_id'), | ||
| 321 | secret_prefix: text('secret_prefix') | ||
| 322 | }, (table) => [ | ||
| 323 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 324 | index("google_discovery_source_credential_query_param_scope_id_idx").on(table.scope_id), | ||
| 325 | index("google_discovery_source_credential_query_param_source_id_idx").on(table.source_id), | ||
| 326 | index("google_discovery_source_credential_query_param_secret_id_idx").on(table.secret_id), | ||
| 193 | ]); | 327 | ]); |
| 194 | 328 | ||
| 195 | export const google_discovery_binding = sqliteTable("google_discovery_binding", { | 329 | export const google_discovery_binding = sqliteTable("google_discovery_binding", { |
| @@ -209,12 +343,44 @@ export const graphql_source = sqliteTable("graphql_source", { · 13 unmodified lines | |||
| 209 | scope_id: text('scope_id').notNull(), | 343 | scope_id: text('scope_id').notNull(), |
| 210 | name: text('name').notNull(), | 344 | name: text('name').notNull(), |
| 211 | endpoint: text('endpoint').notNull(), | 345 | endpoint: text('endpoint').notNull(), |
| 212 | headers: text('headers', { mode: "json" }), | 346 | auth_kind: text({ enum: ['none', 'oauth2'] }).default("none").notNull(), |
| 213 | query_params: text('query_params', { mode: "json" }), | 347 | auth_connection_id: text('auth_connection_id') |
| 214 | auth: text('auth', { mode: "json" }) | ||
| 215 | }, (table) => [ | 348 | }, (table) => [ |
| 216 | primaryKey({ columns: [table.scope_id, table.id] }), | 349 | primaryKey({ columns: [table.scope_id, table.id] }), |
| 217 | index("graphql_source_scope_id_idx").on(table.scope_id), | 350 | index("graphql_source_scope_id_idx").on(table.scope_id), |
| 351 | index("graphql_source_auth_connection_id_idx").on(table.auth_connection_id), | ||
| 352 | ]); | ||
| 353 | |||
| 354 | export const graphql_source_header = sqliteTable("graphql_source_header", { | ||
| 355 | id: text('id').notNull(), | ||
| 356 | scope_id: text('scope_id').notNull(), | ||
| 357 | source_id: text('source_id').notNull(), | ||
| 358 | name: text('name').notNull(), | ||
| 359 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 360 | text_value: text('text_value'), | ||
| 361 | secret_id: text('secret_id'), | ||
| 362 | secret_prefix: text('secret_prefix') | ||
| 363 | }, (table) => [ | ||
| 364 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 365 | index("graphql_source_header_scope_id_idx").on(table.scope_id), | ||
| 366 | index("graphql_source_header_source_id_idx").on(table.source_id), | ||
| 367 | index("graphql_source_header_secret_id_idx").on(table.secret_id), | ||
| 368 | ]); | ||
| 369 | |||
| 370 | export const graphql_source_query_param = sqliteTable("graphql_source_query_param", { | ||
| 371 | id: text('id').notNull(), | ||
| 372 | scope_id: text('scope_id').notNull(), | ||
| 373 | source_id: text('source_id').notNull(), | ||
| 374 | name: text('name').notNull(), | ||
| 375 | kind: text({ enum: ['text', 'secret'] }).notNull(), | ||
| 376 | text_value: text('text_value'), | ||
| 377 | secret_id: text('secret_id'), | ||
| 378 | secret_prefix: text('secret_prefix') | ||
| 379 | }, (table) => [ | ||
| 380 | primaryKey({ columns: [table.scope_id, table.id] }), | ||
| 381 | index("graphql_source_query_param_scope_id_idx").on(table.scope_id), | ||
| 382 | index("graphql_source_query_param_source_id_idx").on(table.source_id), | ||
| 383 | index("graphql_source_query_param_secret_id_idx").on(table.secret_id), | ||
| 218 | ]); | 384 | ]); |
| 219 | 385 | ||
| 220 | export const graphql_operation = sqliteTable("graphql_operation", { | 386 | export const graphql_operation = sqliteTable("graphql_operation", { |
| @@ -228,11 +394,3 @@ export const graphql_operation = sqliteTable("graphql_operation", { · 7 unmodified lines | |||
| 228 | index("graphql_operation_source_id_idx").on(table.source_id), | 394 | index("graphql_operation_source_id_idx").on(table.source_id), |
| 229 | ]); | 395 | ]); |
| 230 | 396 | ||
| 231 | export const blob = sqliteTable("blob", { | ||
| 232 | namespace: text('namespace').notNull(), | ||
| 233 | key: text('key').notNull(), | ||
| 234 | value: text('value').notNull() | ||
| 235 | }, (table) => [ | ||
| 236 | primaryKey({ columns: [table.namespace, table.key] }), | ||
| 237 | ]); | ||
| 238 | |||
| @@ -86,13 +86,21 @@ describe("migrateLegacyConnections", () => { · 85 unmodified lines | |||
| 86 | refresh_token_secret_id: "refresh-token", | 86 | refresh_token_secret_id: "refresh-token", |
| 87 | }); | 87 | }); |
| 88 | 88 | ||
| 89 | // Post-0009 the canonical auth lives in dedicated columns. The | ||
| 90 | // legacy migrator strips config.auth and writes the new pointer to | ||
| 91 | // auth_kind / auth_connection_id directly. | ||
| 89 | const source = db | 92 | const source = db |
| 90 | .prepare("SELECT config FROM mcp_source WHERE scope_id = ? AND id = ?") | 93 | .prepare( |
| 91 | .get("scope-1", "remote-mcp") as { readonly config: string }; | 94 | "SELECT config, auth_kind, auth_connection_id FROM mcp_source WHERE scope_id = ? AND id = ?", |
| 92 | expect(JSON.parse(source.config).auth).toEqual({ | 95 | ) |
| 93 | kind: "oauth2", | 96 | .get("scope-1", "remote-mcp") as { |
| 94 | connectionId: "mcp-oauth2-remote-mcp", | 97 | readonly config: string; |
| 95 | }); | 98 | readonly auth_kind: string; |
| 99 | readonly auth_connection_id: string; | ||
| 100 | }; | ||
| 101 | expect(JSON.parse(source.config).auth).toBeUndefined(); | ||
| 102 | expect(source.auth_kind).toBe("oauth2"); | ||
| 103 | expect(source.auth_connection_id).toBe("mcp-oauth2-remote-mcp"); | ||
| 96 | 104 | ||
| 97 | const ownedSecrets = db | 105 | const ownedSecrets = db |
| 98 | .prepare( | 106 | .prepare( |
| @@ -112,4 +120,86 @@ describe("migrateLegacyConnections", () => { · 13 unmodified lines | |||
| 112 | 120 | ||
| 113 | db.close(); | 121 | db.close(); |
| 114 | }); | 122 | }); |
| 123 | |||
| 124 | it("backfills legacy OpenAPI OAuth from oauth2 column after invocation_config has been dropped", async () => { | ||
| 125 | const db = new Database(join(workDir, "data.db")); | ||
| 126 | migrate(drizzle(db), { | ||
| 127 | migrationsFolder: join(import.meta.dirname, "../../drizzle"), | ||
| 128 | }); | ||
| 129 | |||
| 130 | expect(columnNames(db, "openapi_source")).not.toContain("invocation_config"); | ||
| 131 | |||
| 132 | const now = Date.now(); | ||
| 133 | db.prepare( | ||
| 134 | "INSERT INTO secret (scope_id, id, name, provider, created_at) VALUES (?, ?, ?, ?, ?)", | ||
| 135 | ).run("scope-1", "client-id", "Client ID", "keychain", now); | ||
| 136 | db.prepare( | ||
| 137 | "INSERT INTO secret (scope_id, id, name, provider, created_at) VALUES (?, ?, ?, ?, ?)", | ||
| 138 | ).run("scope-1", "client-secret", "Client secret", "keychain", now); | ||
| 139 | db.prepare( | ||
| 140 | "INSERT INTO secret (scope_id, id, name, provider, created_at) VALUES (?, ?, ?, ?, ?)", | ||
| 141 | ).run("scope-1", "access-token", "Access token", "keychain", now); | ||
| 142 | |||
| 143 | db.prepare( | ||
| 144 | "INSERT INTO openapi_source (scope_id, id, name, spec, oauth2) VALUES (?, ?, ?, ?, ?)", | ||
| 145 | ).run( | ||
| 146 | "scope-1", | ||
| 147 | "legacy-openapi", | ||
| 148 | "Legacy OpenAPI", | ||
| 149 | JSON.stringify({ | ||
| 150 | openapi: "3.0.0", | ||
| 151 | info: { title: "Legacy", version: "1" }, | ||
| 152 | paths: {}, | ||
| 153 | components: { | ||
| 154 | securitySchemes: { | ||
| 155 | oauth2: { | ||
| 156 | type: "oauth2", | ||
| 157 | flows: { | ||
| 158 | clientCredentials: { | ||
| 159 | tokenUrl: "https://example.com/oauth/token", | ||
| 160 | scopes: { read: "read" }, | ||
| 161 | }, | ||
| 162 | }, | ||
| 163 | }, | ||
| 164 | }, | ||
| 165 | }, | ||
| 166 | }), | ||
| 167 | JSON.stringify({ | ||
| 168 | kind: "oauth2", | ||
| 169 | securitySchemeName: "oauth2", | ||
| 170 | flow: "clientCredentials", | ||
| 171 | tokenUrl: "https://example.com/oauth/token", | ||
| 172 | clientIdSecretId: "client-id", | ||
| 173 | clientSecretSecretId: "client-secret", | ||
| 174 | accessTokenSecretId: "access-token", | ||
| 175 | refreshTokenSecretId: null, | ||
| 176 | tokenType: "Bearer", | ||
| 177 | expiresAt: null, | ||
| 178 | scope: "read", | ||
| 179 | scopes: ["read"], | ||
| 180 | }), | ||
| 181 | ); | ||
| 182 | |||
| 183 | await migrateLegacyConnections(db); | ||
| 184 | |||
| 185 | const connection = db | ||
| 186 | .prepare( | ||
| 187 | "SELECT id, provider, access_token_secret_id FROM connection WHERE scope_id = ?", | ||
| 188 | ) | ||
| 189 | .get("scope-1") as | ||
| 190 | | { readonly id: string; readonly provider: string; readonly access_token_secret_id: string } | ||
| 191 | | undefined; | ||
| 192 | expect(connection?.provider).toBe("openapi:oauth2"); | ||
| 193 | expect(connection?.access_token_secret_id).toBe("access-token"); | ||
| 194 | |||
| 195 | // The oauth2 column should now hold the new pointer shape with connectionId. | ||
| 196 | const source = db | ||
| 197 | .prepare("SELECT oauth2 FROM openapi_source WHERE scope_id = ? AND id = ?") | ||
| 198 | .get("scope-1", "legacy-openapi") as { readonly oauth2: string }; | ||
| 199 | const oauth2 = JSON.parse(source.oauth2); | ||
| 200 | expect(oauth2.kind).toBe("oauth2"); | ||
| 201 | expect(oauth2.connectionId).toBe(connection?.id); | ||
| 202 | |||
| 203 | db.close(); | ||
| 204 | }); | ||
| 115 | }); | 205 | }); |
| @@ -250,16 +250,32 @@ type OpenApiRow = { · 249 unmodified lines | |||
| 250 | 250 | ||
| 251 | const migrateOpenApi = async (sqlite: Database): Promise<void> => { | 251 | const migrateOpenApi = async (sqlite: Database): Promise<void> => { |
| 252 | if (!tableExists(sqlite, "openapi_source")) return; | 252 | if (!tableExists(sqlite, "openapi_source")) return; |
| 253 | // After the plugin normalization migration, `invocation_config` is | ||
| 254 | // gone (specFetchCredentials moved to child tables). The `oauth2` | ||
| 255 | // column stays JSON — that's the canonical source for the OAuth | ||
| 256 | // pointer. Pre-migration, both columns mirror each other; post- | ||
| 257 | // migration, only `oauth2` is left. We read both when available and | ||
| 258 | // fall back to `oauth2` so legacy data isn't silently skipped. | ||
| 259 | const hasInvocationConfig = columnExists( | ||
| 260 | sqlite, | ||
| 261 | "openapi_source", | ||
| 262 | "invocation_config", | ||
| 263 | ); | ||
| 264 | const selectCols = hasInvocationConfig | ||
| 265 | ? "scope_id, id, name, spec, invocation_config, oauth2" | ||
| 266 | : "scope_id, id, name, spec, NULL AS invocation_config, oauth2"; | ||
| 253 | const rows = sqlite | 267 | const rows = sqlite |
| 254 | .prepare( | 268 | .prepare(`SELECT ${selectCols} FROM openapi_source`) |
| 255 | "SELECT scope_id, id, name, spec, invocation_config, oauth2 FROM openapi_source", | ||
| 256 | ) | ||
| 257 | .all() as ReadonlyArray<OpenApiRow>; | 269 | .all() as ReadonlyArray<OpenApiRow>; |
| 258 | if (rows.length === 0) return; | 270 | if (rows.length === 0) return; |
| 259 | 271 | ||
| 260 | const updateSource = sqlite.prepare( | 272 | const updateSource = hasInvocationConfig |
| 261 | "UPDATE openapi_source SET oauth2 = ?, invocation_config = ? WHERE scope_id = ? AND id = ?", | 273 | ? sqlite.prepare( |
| 262 | ); | 274 | "UPDATE openapi_source SET oauth2 = ?, invocation_config = ? WHERE scope_id = ? AND id = ?", |
| 275 | ) | ||
| 276 | : sqlite.prepare( | ||
| 277 | "UPDATE openapi_source SET oauth2 = ? WHERE scope_id = ? AND id = ?", | ||
| 278 | ); | ||
| 263 | 279 | ||
| 264 | for (const row of rows) { | 280 | for (const row of rows) { |
| 265 | let invocation: Record<string, unknown> = {}; | 281 | let invocation: Record<string, unknown> = {}; |
| @@ -340,13 +356,21 @@ const migrateOpenApi = async (sqlite: Database): Promise<void> => { · 74 unmodified lines | |||
| 340 | }); | 356 | }); |
| 341 | const err = rewireSecrets(sqlite, row.scope_id, connectionId, secretIds, secretNames); | 357 | const err = rewireSecrets(sqlite, row.scope_id, connectionId, secretIds, secretNames); |
| 342 | if (err) throw new Error(err); | 358 | if (err) throw new Error(err); |
| 343 | const nextInvocation = { ...invocation, oauth2: oauth2Pointer }; | 359 | if (hasInvocationConfig) { |
| 344 | updateSource.run( | 360 | const nextInvocation = { ...invocation, oauth2: oauth2Pointer }; |
| 345 | JSON.stringify(oauth2Pointer), | 361 | updateSource.run( |
| 346 | JSON.stringify(nextInvocation), | 362 | JSON.stringify(oauth2Pointer), |
| 347 | row.scope_id, | 363 | JSON.stringify(nextInvocation), |
| 348 | row.id, | 364 | row.scope_id, |
| 349 | ); | 365 | row.id, |
| 366 | ); | ||
| 367 | } else { | ||
| 368 | updateSource.run( | ||
| 369 | JSON.stringify(oauth2Pointer), | ||
| 370 | row.scope_id, | ||
| 371 | row.id, | ||
| 372 | ); | ||
| 373 | } | ||
| 350 | }); | 374 | }); |
| 351 | try { | 375 | try { |
| 352 | txn(); | 376 | txn(); |
| @@ -407,9 +431,23 @@ const migrateMcp = (sqlite: Database): void => { · 54 unmodified lines | |||
| 407 | .all() as ReadonlyArray<McpRow>; | 431 | .all() as ReadonlyArray<McpRow>; |
| 408 | if (rows.length === 0) return; | 432 | if (rows.length === 0) return; |
| 409 | 433 | ||
| 410 | const updateSource = sqlite.prepare( | 434 | // Post-0009 mcp_source has dedicated auth_kind / auth_connection_id |
| 435 | // columns. The auth ETL inside drizzle migration 0009 only fires for | ||
| 436 | // rows whose config.auth carries `connectionId` — i.e., already | ||
| 437 | // post-Connection shape. Truly-legacy rows (inline OAuth shape with | ||
| 438 | // accessTokenSecretId) survive unchanged and fall to this script. | ||
| 439 | // Detect them, mint a Connection, rewire owned secrets, then write | ||
| 440 | // the result to the new columns (not back into config.auth, which | ||
| 441 | // 0009 stripped). | ||
| 442 | const hasAuthColumns = columnExists(sqlite, "mcp_source", "auth_kind"); | ||
| 443 | const updateConfig = sqlite.prepare( | ||
| 411 | "UPDATE mcp_source SET config = ? WHERE scope_id = ? AND id = ?", | 444 | "UPDATE mcp_source SET config = ? WHERE scope_id = ? AND id = ?", |
| 412 | ); | 445 | ); |
| 446 | const updateConfigAndAuth = hasAuthColumns | ||
| 447 | ? sqlite.prepare( | ||
| 448 | "UPDATE mcp_source SET config = ?, auth_kind = 'oauth2', auth_connection_id = ? WHERE scope_id = ? AND id = ?", | ||
| 449 | ) | ||
| 450 | : null; | ||
| 413 | 451 | ||
| 414 | for (const row of rows) { | 452 | for (const row of rows) { |
| 415 | let config: Record<string, unknown> = {}; | 453 | let config: Record<string, unknown> = {}; |
| @@ -447,8 +485,14 @@ const migrateMcp = (sqlite: Database): void => { · 31 unmodified lines | |||
| 447 | resourceMetadataUrl: legacy.resourceMetadataUrl, | 485 | resourceMetadataUrl: legacy.resourceMetadataUrl, |
| 448 | resourceMetadata: null, | 486 | resourceMetadata: null, |
| 449 | }; | 487 | }; |
| 450 | const authPointer = { kind: "oauth2" as const, connectionId }; | 488 | // Strip auth from config — post-0009 the canonical home is the |
| 451 | const nextConfig = { ...config, auth: authPointer }; | 489 | // auth_* columns. Pre-0009 we still write the pointer back into |
| 490 | // config.auth so the older code path keeps working. | ||
| 491 | const { auth: _unused, ...configWithoutAuth } = config; | ||
| 492 | void _unused; | ||
| 493 | const nextConfig = hasAuthColumns | ||
| 494 | ? configWithoutAuth | ||
| 495 | : { ...config, auth: { kind: "oauth2" as const, connectionId } }; | ||
| 452 | 496 | ||
| 453 | const secretIds = [legacy.accessTokenSecretId]; | 497 | const secretIds = [legacy.accessTokenSecretId]; |
| 454 | const secretNames = [`Connection ${connectionId} access token`]; | 498 | const secretNames = [`Connection ${connectionId} access token`]; |
| @@ -471,7 +515,16 @@ const migrateMcp = (sqlite: Database): void => { · 16 unmodified lines | |||
| 471 | }); | 515 | }); |
| 472 | const err = rewireSecrets(sqlite, row.scope_id, connectionId, secretIds, secretNames); | 516 | const err = rewireSecrets(sqlite, row.scope_id, connectionId, secretIds, secretNames); |
| 473 | if (err) throw new Error(err); | 517 | if (err) throw new Error(err); |
| 474 | updateSource.run(JSON.stringify(nextConfig), row.scope_id, row.id); | 518 | if (updateConfigAndAuth) { |
| 519 | updateConfigAndAuth.run( | ||
| 520 | JSON.stringify(nextConfig), | ||
| 521 | connectionId, | ||
| 522 | row.scope_id, | ||
| 523 | row.id, | ||
| 524 | ); | ||
| 525 | } else { | ||
| 526 | updateConfig.run(JSON.stringify(nextConfig), row.scope_id, row.id); | ||
| 527 | } | ||
| 475 | }); | 528 | }); |
| 476 | try { | 529 | try { |
| 477 | txn(); | 530 | txn(); |
| @@ -0,0 +1,204 @@ | |||
| 1 | // End-to-end test for the google-discovery portion of | ||
| 2 | // `0007_normalize_plugin_secret_refs.sql`. Seeds a | ||
| 3 | // google_discovery_source row with the legacy json shape (config | ||
| 4 | // containing auth/credentials), runs the migration, asserts the new | ||
| 5 | // columns and child tables are populated. | ||
| 6 | |||
| 7 | import { describe, expect, it } from "@effect/vitest"; | ||
| 8 | import { Database } from "bun:sqlite"; | ||
| 9 | import { mkdtempSync, rmSync } from "node:fs"; | ||
| 10 | import { join } from "node:path"; | ||
| 11 | import { tmpdir } from "node:os"; | ||
| 12 | import { drizzle } from "drizzle-orm/bun-sqlite"; | ||
| 13 | import { migrate } from "drizzle-orm/bun-sqlite/migrator"; | ||
| 14 | |||
| 15 | import { PRE_0007_SQL, stampPriorMigrationsApplied } from "./__test-helpers__/pre-0007-schema"; | ||
| 16 | |||
| 17 | const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); | ||
| 18 | |||
| 19 | describe("0007_normalize_plugin_secret_refs (google-discovery)", () => { | ||
| 20 | it("flattens oauth2 auth into columns", () => { | ||
| 21 | const dir = mkdtempSync(join(tmpdir(), "gd-mig-")); | ||
| 22 | const dbPath = join(dir, "test.sqlite"); | ||
| 23 | try { | ||
| 24 | const db = new Database(dbPath); | ||
| 25 | db.exec(PRE_0007_SQL); | ||
| 26 | stampPriorMigrationsApplied(db); | ||
| 27 | |||
| 28 | db.prepare( | ||
| 29 | "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", | ||
| 30 | ).run( | ||
| 31 | "default-scope", | ||
| 32 | "drive", | ||
| 33 | "Drive", | ||
| 34 | JSON.stringify({ | ||
| 35 | name: "Drive", | ||
| 36 | discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", | ||
| 37 | service: "drive", | ||
| 38 | version: "v3", | ||
| 39 | rootUrl: "https://www.googleapis.com/", | ||
| 40 | servicePath: "drive/v3/", | ||
| 41 | auth: { | ||
| 42 | kind: "oauth2", | ||
| 43 | connectionId: "conn-1", | ||
| 44 | clientIdSecretId: "client-id", | ||
| 45 | clientSecretSecretId: "client-secret", | ||
| 46 | scopes: ["https://www.googleapis.com/auth/drive"], | ||
| 47 | }, | ||
| 48 | }), | ||
| 49 | Date.now(), | ||
| 50 | Date.now(), | ||
| 51 | ); | ||
| 52 | |||
| 53 | db.close(); | ||
| 54 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 55 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 56 | |||
| 57 | const after = new Database(dbPath, { readonly: true }); | ||
| 58 | const row = after | ||
| 59 | .prepare( | ||
| 60 | "SELECT auth_kind, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id, auth_scopes, config FROM google_discovery_source WHERE id = ?", | ||
| 61 | ) | ||
| 62 | .get("drive") as Record<string, string | null>; | ||
| 63 | expect(row.auth_kind).toBe("oauth2"); | ||
| 64 | expect(row.auth_connection_id).toBe("conn-1"); | ||
| 65 | expect(row.auth_client_id_secret_id).toBe("client-id"); | ||
| 66 | expect(row.auth_client_secret_secret_id).toBe("client-secret"); | ||
| 67 | // auth_scopes column is text-typed (string[] gets stored as JSON in sqlite). | ||
| 68 | expect(row.auth_scopes).toContain("drive"); | ||
| 69 | // The auth key should be stripped from config json. | ||
| 70 | const config = JSON.parse(row.config!); | ||
| 71 | expect(config.auth).toBeUndefined(); | ||
| 72 | expect(config.service).toBe("drive"); | ||
| 73 | after.close(); | ||
| 74 | } finally { | ||
| 75 | rmSync(dir, { recursive: true, force: true }); | ||
| 76 | } | ||
| 77 | }); | ||
| 78 | |||
| 79 | it("explodes credentials.headers and queryParams into child rows", () => { | ||
| 80 | const dir = mkdtempSync(join(tmpdir(), "gd-mig-")); | ||
| 81 | const dbPath = join(dir, "test.sqlite"); | ||
| 82 | try { | ||
| 83 | const db = new Database(dbPath); | ||
| 84 | db.exec(PRE_0007_SQL); | ||
| 85 | stampPriorMigrationsApplied(db); | ||
| 86 | |||
| 87 | db.prepare( | ||
| 88 | "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", | ||
| 89 | ).run( | ||
| 90 | "default-scope", | ||
| 91 | "with-creds", | ||
| 92 | "With Creds", | ||
| 93 | JSON.stringify({ | ||
| 94 | name: "With Creds", | ||
| 95 | discoveryUrl: "https://example.com/discovery", | ||
| 96 | service: "svc", | ||
| 97 | version: "v1", | ||
| 98 | rootUrl: "https://example.com/", | ||
| 99 | servicePath: "svc/v1/", | ||
| 100 | auth: { kind: "none" }, | ||
| 101 | credentials: { | ||
| 102 | headers: { | ||
| 103 | "X-Static": "literal", | ||
| 104 | Authorization: { secretId: "tok-secret", prefix: "Bearer " }, | ||
| 105 | }, | ||
| 106 | queryParams: { | ||
| 107 | api_key: { secretId: "key-secret" }, | ||
| 108 | }, | ||
| 109 | }, | ||
| 110 | }), | ||
| 111 | Date.now(), | ||
| 112 | Date.now(), | ||
| 113 | ); | ||
| 114 | |||
| 115 | db.close(); | ||
| 116 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 117 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 118 | |||
| 119 | const after = new Database(dbPath, { readonly: true }); | ||
| 120 | const headers = after | ||
| 121 | .prepare( | ||
| 122 | "SELECT name, kind, text_value, secret_id, secret_prefix FROM google_discovery_source_credential_header WHERE source_id = ? ORDER BY name", | ||
| 123 | ) | ||
| 124 | .all("with-creds") as ReadonlyArray<Record<string, string | null>>; | ||
| 125 | expect(headers).toHaveLength(2); | ||
| 126 | const byName = new Map(headers.map((h) => [h.name!, h])); | ||
| 127 | expect(byName.get("X-Static")).toMatchObject({ | ||
| 128 | kind: "text", | ||
| 129 | text_value: "literal", | ||
| 130 | }); | ||
| 131 | expect(byName.get("Authorization")).toMatchObject({ | ||
| 132 | kind: "secret", | ||
| 133 | secret_id: "tok-secret", | ||
| 134 | secret_prefix: "Bearer ", | ||
| 135 | }); | ||
| 136 | |||
| 137 | const params = after | ||
| 138 | .prepare( | ||
| 139 | "SELECT name, secret_id FROM google_discovery_source_credential_query_param WHERE source_id = ?", | ||
| 140 | ) | ||
| 141 | .all("with-creds") as ReadonlyArray<Record<string, string>>; | ||
| 142 | expect(params).toHaveLength(1); | ||
| 143 | expect(params[0]).toMatchObject({ name: "api_key", secret_id: "key-secret" }); | ||
| 144 | |||
| 145 | after.close(); | ||
| 146 | } finally { | ||
| 147 | rmSync(dir, { recursive: true, force: true }); | ||
| 148 | } | ||
| 149 | }); | ||
| 150 | |||
| 151 | it("survives auth.kind=none with no credentials", () => { | ||
| 152 | const dir = mkdtempSync(join(tmpdir(), "gd-mig-")); | ||
| 153 | const dbPath = join(dir, "test.sqlite"); | ||
| 154 | try { | ||
| 155 | const db = new Database(dbPath); | ||
| 156 | db.exec(PRE_0007_SQL); | ||
| 157 | stampPriorMigrationsApplied(db); | ||
| 158 | |||
| 159 | db.prepare( | ||
| 160 | "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", | ||
| 161 | ).run( | ||
| 162 | "default-scope", | ||
| 163 | "bare", | ||
| 164 | "Bare", | ||
| 165 | JSON.stringify({ | ||
| 166 | name: "Bare", | ||
| 167 | discoveryUrl: "https://example.com/discovery", | ||
| 168 | service: "svc", | ||
| 169 | version: "v1", | ||
| 170 | rootUrl: "https://example.com/", | ||
| 171 | servicePath: "svc/v1/", | ||
| 172 | auth: { kind: "none" }, | ||
| 173 | }), | ||
| 174 | Date.now(), | ||
| 175 | Date.now(), | ||
| 176 | ); | ||
| 177 | |||
| 178 | db.close(); | ||
| 179 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 180 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 181 | |||
| 182 | const after = new Database(dbPath, { readonly: true }); | ||
| 183 | const row = after | ||
| 184 | .prepare( | ||
| 185 | "SELECT auth_kind, auth_connection_id, auth_scopes FROM google_discovery_source WHERE id = ?", | ||
| 186 | ) | ||
| 187 | .get("bare") as Record<string, string | null>; | ||
| 188 | expect(row.auth_kind).toBe("none"); | ||
| 189 | expect(row.auth_connection_id).toBeNull(); | ||
| 190 | |||
| 191 | const headerCount = ( | ||
| 192 | after | ||
| 193 | .prepare( | ||
| 194 | "SELECT count(*) as n FROM google_discovery_source_credential_header WHERE source_id = ?", | ||
| 195 | ) | ||
| 196 | .get("bare") as { n: number } | ||
| 197 | ).n; | ||
| 198 | expect(headerCount).toBe(0); | ||
| 199 | after.close(); | ||
| 200 | } finally { | ||
| 201 | rmSync(dir, { recursive: true, force: true }); | ||
| 202 | } | ||
| 203 | }); | ||
| 204 | }); | ||
| @@ -0,0 +1,246 @@ | |||
| 1 | // End-to-end test for the graphql portion of | ||
| 2 | // `0007_normalize_plugin_secret_refs.sql`: seed a DB at the | ||
| 3 | // pre-migration (0006) shape with json-blob headers/query_params/auth, | ||
| 4 | // run the migration, assert that the JSON unpacks into the new | ||
| 5 | // normalized columns / child tables and that the JSON columns are gone. | ||
| 6 | |||
| 7 | import { describe, expect, it } from "@effect/vitest"; | ||
| 8 | import { Database } from "bun:sqlite"; | ||
| 9 | import { mkdtempSync, rmSync } from "node:fs"; | ||
| 10 | import { join } from "node:path"; | ||
| 11 | import { tmpdir } from "node:os"; | ||
| 12 | import { drizzle } from "drizzle-orm/bun-sqlite"; | ||
| 13 | import { migrate } from "drizzle-orm/bun-sqlite/migrator"; | ||
| 14 | |||
| 15 | import { PRE_0007_SQL, stampPriorMigrationsApplied } from "./__test-helpers__/pre-0007-schema"; | ||
| 16 | |||
| 17 | const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); | ||
| 18 | |||
| 19 | describe("0007_normalize_plugin_secret_refs (graphql)", () => { | ||
| 20 | it("flattens auth json into auth_kind/auth_connection_id columns", () => { | ||
| 21 | const dir = mkdtempSync(join(tmpdir(), "graphql-mig-")); | ||
| 22 | const dbPath = join(dir, "test.sqlite"); | ||
| 23 | try { | ||
| 24 | const db = new Database(dbPath); | ||
| 25 | db.exec(PRE_0007_SQL); | ||
| 26 | stampPriorMigrationsApplied(db); | ||
| 27 | |||
| 28 | db.prepare( | ||
| 29 | "INSERT INTO graphql_source (scope_id, id, name, endpoint, auth) VALUES (?, ?, ?, ?, ?)", | ||
| 30 | ).run( | ||
| 31 | "default-scope", | ||
| 32 | "github", | ||
| 33 | "GitHub", | ||
| 34 | "https://api.github.com/graphql", | ||
| 35 | JSON.stringify({ kind: "oauth2", connectionId: "conn-1" }), | ||
| 36 | ); | ||
| 37 | |||
| 38 | db.close(); | ||
| 39 | |||
| 40 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 41 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 42 | |||
| 43 | const after = new Database(dbPath, { readonly: true }); | ||
| 44 | const row = after | ||
| 45 | .prepare( | ||
| 46 | "SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?", | ||
| 47 | ) | ||
| 48 | .get("github") as { auth_kind: string; auth_connection_id: string }; | ||
| 49 | expect(row.auth_kind).toBe("oauth2"); | ||
| 50 | expect(row.auth_connection_id).toBe("conn-1"); | ||
| 51 | // Old json column is gone. | ||
| 52 | const cols = after | ||
| 53 | .prepare("PRAGMA table_info('graphql_source')") | ||
| 54 | .all() as ReadonlyArray<{ name: string }>; | ||
| 55 | expect(cols.some((c) => c.name === "auth")).toBe(false); | ||
| 56 | expect(cols.some((c) => c.name === "headers")).toBe(false); | ||
| 57 | expect(cols.some((c) => c.name === "query_params")).toBe(false); | ||
| 58 | after.close(); | ||
| 59 | } finally { | ||
| 60 | rmSync(dir, { recursive: true, force: true }); | ||
| 61 | } | ||
| 62 | }); | ||
| 63 | |||
| 64 | it("explodes header/query_param json into child rows", () => { | ||
| 65 | const dir = mkdtempSync(join(tmpdir(), "graphql-mig-")); | ||
| 66 | const dbPath = join(dir, "test.sqlite"); | ||
| 67 | try { | ||
| 68 | const db = new Database(dbPath); | ||
| 69 | db.exec(PRE_0007_SQL); | ||
| 70 | stampPriorMigrationsApplied(db); | ||
| 71 | |||
| 72 | const headers = { | ||
| 73 | // Literal text header. | ||
| 74 | "X-Static": "literal-value", | ||
| 75 | // Secret-backed header without prefix. | ||
| 76 | Authorization: { secretId: "sec-token" }, | ||
| 77 | // Secret-backed with prefix. | ||
| 78 | "X-Bearer": { secretId: "sec-bearer", prefix: "Bearer " }, | ||
| 79 | }; | ||
| 80 | const queryParams = { | ||
| 81 | api_key: { secretId: "sec-key" }, | ||
| 82 | }; | ||
| 83 | |||
| 84 | db.prepare( | ||
| 85 | "INSERT INTO graphql_source (scope_id, id, name, endpoint, headers, query_params, auth) VALUES (?, ?, ?, ?, ?, ?, ?)", | ||
| 86 | ).run( | ||
| 87 | "default-scope", | ||
| 88 | "example", | ||
| 89 | "Example", | ||
| 90 | "https://example.com/graphql", | ||
| 91 | JSON.stringify(headers), | ||
| 92 | JSON.stringify(queryParams), | ||
| 93 | JSON.stringify({ kind: "none" }), | ||
| 94 | ); | ||
| 95 | |||
| 96 | db.close(); | ||
| 97 | |||
| 98 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 99 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 100 | |||
| 101 | const after = new Database(dbPath, { readonly: true }); | ||
| 102 | const headerRows = after | ||
| 103 | .prepare( | ||
| 104 | "SELECT name, kind, text_value, secret_id, secret_prefix FROM graphql_source_header WHERE source_id = ? ORDER BY name", | ||
| 105 | ) | ||
| 106 | .all("example") as ReadonlyArray<{ | ||
| 107 | name: string; | ||
| 108 | kind: string; | ||
| 109 | text_value: string | null; | ||
| 110 | secret_id: string | null; | ||
| 111 | secret_prefix: string | null; | ||
| 112 | }>; | ||
| 113 | expect(headerRows).toHaveLength(3); | ||
| 114 | |||
| 115 | const byName = new Map(headerRows.map((r) => [r.name, r])); | ||
| 116 | expect(byName.get("X-Static")).toMatchObject({ | ||
| 117 | kind: "text", | ||
| 118 | text_value: "literal-value", | ||
| 119 | secret_id: null, | ||
| 120 | }); | ||
| 121 | expect(byName.get("Authorization")).toMatchObject({ | ||
| 122 | kind: "secret", | ||
| 123 | text_value: null, | ||
| 124 | secret_id: "sec-token", | ||
| 125 | secret_prefix: null, | ||
| 126 | }); | ||
| 127 | expect(byName.get("X-Bearer")).toMatchObject({ | ||
| 128 | kind: "secret", | ||
| 129 | secret_id: "sec-bearer", | ||
| 130 | secret_prefix: "Bearer ", | ||
| 131 | }); | ||
| 132 | |||
| 133 | const paramRow = after | ||
| 134 | .prepare( | ||
| 135 | "SELECT kind, secret_id FROM graphql_source_query_param WHERE source_id = ?", | ||
| 136 | ) | ||
| 137 | .get("example") as { kind: string; secret_id: string }; | ||
| 138 | expect(paramRow).toMatchObject({ kind: "secret", secret_id: "sec-key" }); | ||
| 139 | |||
| 140 | after.close(); | ||
| 141 | } finally { | ||
| 142 | rmSync(dir, { recursive: true, force: true }); | ||
| 143 | } | ||
| 144 | }); | ||
| 145 | |||
| 146 | it("handles graphql_source rows with null json (empty config)", () => { | ||
| 147 | const dir = mkdtempSync(join(tmpdir(), "graphql-mig-")); | ||
| 148 | const dbPath = join(dir, "test.sqlite"); | ||
| 149 | try { | ||
| 150 | const db = new Database(dbPath); | ||
| 151 | db.exec(PRE_0007_SQL); | ||
| 152 | stampPriorMigrationsApplied(db); | ||
| 153 | |||
| 154 | db.prepare( | ||
| 155 | "INSERT INTO graphql_source (scope_id, id, name, endpoint) VALUES (?, ?, ?, ?)", | ||
| 156 | ).run("default-scope", "bare", "Bare", "https://bare.example/graphql"); | ||
| 157 | db.close(); | ||
| 158 | |||
| 159 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 160 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 161 | |||
| 162 | const after = new Database(dbPath, { readonly: true }); | ||
| 163 | const row = after | ||
| 164 | .prepare( | ||
| 165 | "SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?", | ||
| 166 | ) | ||
| 167 | .get("bare") as { auth_kind: string; auth_connection_id: string | null }; | ||
| 168 | expect(row.auth_kind).toBe("none"); | ||
| 169 | expect(row.auth_connection_id).toBeNull(); | ||
| 170 | |||
| 171 | const headerCount = ( | ||
| 172 | after | ||
| 173 | .prepare( | ||
| 174 | "SELECT count(*) as n FROM graphql_source_header WHERE source_id = ?", | ||
| 175 | ) | ||
| 176 | .get("bare") as { n: number } | ||
| 177 | ).n; | ||
| 178 | expect(headerCount).toBe(0); | ||
| 179 | after.close(); | ||
| 180 | } finally { | ||
| 181 | rmSync(dir, { recursive: true, force: true }); | ||
| 182 | } | ||
| 183 | }); | ||
| 184 | |||
| 185 | it("does not collapse child rows whose source/name pairs share colon-concatenated ids", () => { | ||
| 186 | const dir = mkdtempSync(join(tmpdir(), "graphql-mig-")); | ||
| 187 | const dbPath = join(dir, "test.sqlite"); | ||
| 188 | try { | ||
| 189 | const db = new Database(dbPath); | ||
| 190 | db.exec(PRE_0007_SQL); | ||
| 191 | stampPriorMigrationsApplied(db); | ||
| 192 | |||
| 193 | const insert = db.prepare( | ||
| 194 | "INSERT INTO graphql_source (scope_id, id, name, endpoint, headers) VALUES (?, ?, ?, ?, ?)", | ||
| 195 | ); | ||
| 196 | insert.run( | ||
| 197 | "default-scope", | ||
| 198 | "a:b", | ||
| 199 | "First", | ||
| 200 | "https://first.example/graphql", | ||
| 201 | JSON.stringify({ c: "first" }), | ||
| 202 | ); | ||
| 203 | insert.run( | ||
| 204 | "default-scope", | ||
| 205 | "a", | ||
| 206 | "Second", | ||
| 207 | "https://second.example/graphql", | ||
| 208 | JSON.stringify({ "b:c": "second" }), | ||
| 209 | ); | ||
| 210 | db.close(); | ||
| 211 | |||
| 212 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 213 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 214 | |||
| 215 | const after = new Database(dbPath, { readonly: true }); | ||
| 216 | const rows = after | ||
| 217 | .prepare( | ||
| 218 | "SELECT id, source_id, name, text_value FROM graphql_source_header ORDER BY source_id, name", | ||
| 219 | ) | ||
| 220 | .all() as ReadonlyArray<{ | ||
| 221 | id: string; | ||
| 222 | source_id: string; | ||
| 223 | name: string; | ||
| 224 | text_value: string; | ||
| 225 | }>; | ||
| 226 | expect(rows).toHaveLength(2); | ||
| 227 | expect(rows).toEqual([ | ||
| 228 | { | ||
| 229 | id: '["a","b:c"]', | ||
| 230 | source_id: "a", | ||
| 231 | name: "b:c", | ||
| 232 | text_value: "second", | ||
| 233 | }, | ||
| 234 | { | ||
| 235 | id: '["a:b","c"]', | ||
| 236 | source_id: "a:b", | ||
| 237 | name: "c", | ||
| 238 | text_value: "first", | ||
| 239 | }, | ||
| 240 | ]); | ||
| 241 | after.close(); | ||
| 242 | } finally { | ||
| 243 | rmSync(dir, { recursive: true, force: true }); | ||
| 244 | } | ||
| 245 | }); | ||
| 246 | }); | ||
| @@ -0,0 +1,203 @@ | |||
| 1 | // End-to-end test for the mcp portion of | ||
| 2 | // `0007_normalize_plugin_secret_refs.sql`. Seeds an mcp_source row | ||
| 3 | // with the legacy json shape (config containing auth/headers/ | ||
| 4 | // queryParams), runs the migration runner, asserts the auth columns | ||
| 5 | // are populated and the child tables hold the secret-backed entries. | ||
| 6 | |||
| 7 | import { describe, expect, it } from "@effect/vitest"; | ||
| 8 | import { Database } from "bun:sqlite"; | ||
| 9 | import { mkdtempSync, rmSync } from "node:fs"; | ||
| 10 | import { join } from "node:path"; | ||
| 11 | import { tmpdir } from "node:os"; | ||
| 12 | import { drizzle } from "drizzle-orm/bun-sqlite"; | ||
| 13 | import { migrate } from "drizzle-orm/bun-sqlite/migrator"; | ||
| 14 | |||
| 15 | import { PRE_0007_SQL, stampPriorMigrationsApplied } from "./__test-helpers__/pre-0007-schema"; | ||
| 16 | |||
| 17 | const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); | ||
| 18 | |||
| 19 | describe("0007_normalize_plugin_secret_refs (mcp)", () => { | ||
| 20 | it("flattens header auth into auth_kind/auth_secret_id columns", () => { | ||
| 21 | const dir = mkdtempSync(join(tmpdir(), "mcp-mig-")); | ||
| 22 | const dbPath = join(dir, "test.sqlite"); | ||
| 23 | try { | ||
| 24 | const db = new Database(dbPath); | ||
| 25 | db.exec(PRE_0007_SQL); | ||
| 26 | stampPriorMigrationsApplied(db); | ||
| 27 | |||
| 28 | db.prepare( | ||
| 29 | "INSERT INTO mcp_source (scope_id, id, name, config, created_at) VALUES (?, ?, ?, ?, ?)", | ||
| 30 | ).run( | ||
| 31 | "default-scope", | ||
| 32 | "remote-headers", | ||
| 33 | "Remote Headers", | ||
| 34 | JSON.stringify({ | ||
| 35 | transport: "remote", | ||
| 36 | endpoint: "https://example.com/mcp", | ||
| 37 | auth: { | ||
| 38 | kind: "header", | ||
| 39 | headerName: "X-API-Key", | ||
| 40 | secretId: "tok-secret", | ||
| 41 | prefix: "Bearer ", | ||
| 42 | }, | ||
| 43 | }), | ||
| 44 | Date.now(), | ||
| 45 | ); | ||
| 46 | |||
| 47 | db.close(); | ||
| 48 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 49 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 50 | |||
| 51 | const after = new Database(dbPath, { readonly: true }); | ||
| 52 | const row = after | ||
| 53 | .prepare( | ||
| 54 | "SELECT auth_kind, auth_header_name, auth_secret_id, auth_secret_prefix, config FROM mcp_source WHERE id = ?", | ||
| 55 | ) | ||
| 56 | .get("remote-headers") as { | ||
| 57 | auth_kind: string; | ||
| 58 | auth_header_name: string; | ||
| 59 | auth_secret_id: string; | ||
| 60 | auth_secret_prefix: string; | ||
| 61 | config: string; | ||
| 62 | }; | ||
| 63 | expect(row.auth_kind).toBe("header"); | ||
| 64 | expect(row.auth_header_name).toBe("X-API-Key"); | ||
| 65 | expect(row.auth_secret_id).toBe("tok-secret"); | ||
| 66 | expect(row.auth_secret_prefix).toBe("Bearer "); | ||
| 67 | // The auth key should be stripped from config json after migration. | ||
| 68 | const config = JSON.parse(row.config); | ||
| 69 | expect(config.auth).toBeUndefined(); | ||
| 70 | expect(config.transport).toBe("remote"); | ||
| 71 | expect(config.endpoint).toBe("https://example.com/mcp"); | ||
| 72 | after.close(); | ||
| 73 | } finally { | ||
| 74 | rmSync(dir, { recursive: true, force: true }); | ||
| 75 | } | ||
| 76 | }); | ||
| 77 | |||
| 78 | it("flattens oauth2 auth and explodes headers into child rows", () => { | ||
| 79 | const dir = mkdtempSync(join(tmpdir(), "mcp-mig-")); | ||
| 80 | const dbPath = join(dir, "test.sqlite"); | ||
| 81 | try { | ||
| 82 | const db = new Database(dbPath); | ||
| 83 | db.exec(PRE_0007_SQL); | ||
| 84 | stampPriorMigrationsApplied(db); | ||
| 85 | |||
| 86 | db.prepare( | ||
| 87 | "INSERT INTO mcp_source (scope_id, id, name, config, created_at) VALUES (?, ?, ?, ?, ?)", | ||
| 88 | ).run( | ||
| 89 | "default-scope", | ||
| 90 | "remote-oauth", | ||
| 91 | "Remote OAuth", | ||
| 92 | JSON.stringify({ | ||
| 93 | transport: "remote", | ||
| 94 | endpoint: "https://oauth.example/mcp", | ||
| 95 | headers: { | ||
| 96 | "X-Trace": "static", | ||
| 97 | "X-Token": { secretId: "extra-tok" }, | ||
| 98 | }, | ||
| 99 | queryParams: { | ||
| 100 | org: { secretId: "org-id-secret" }, | ||
| 101 | }, | ||
| 102 | auth: { | ||
| 103 | kind: "oauth2", | ||
| 104 | connectionId: "conn-1", | ||
| 105 | clientIdSecretId: "client-id-sec", | ||
| 106 | clientSecretSecretId: "client-secret-sec", | ||
| 107 | }, | ||
| 108 | }), | ||
| 109 | Date.now(), | ||
| 110 | ); | ||
| 111 | |||
| 112 | db.close(); | ||
| 113 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 114 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 115 | |||
| 116 | const after = new Database(dbPath, { readonly: true }); | ||
| 117 | const row = after | ||
| 118 | .prepare( | ||
| 119 | "SELECT auth_kind, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id FROM mcp_source WHERE id = ?", | ||
| 120 | ) | ||
| 121 | .get("remote-oauth") as Record<string, string | null>; | ||
| 122 | expect(row.auth_kind).toBe("oauth2"); | ||
| 123 | expect(row.auth_connection_id).toBe("conn-1"); | ||
| 124 | expect(row.auth_client_id_secret_id).toBe("client-id-sec"); | ||
| 125 | expect(row.auth_client_secret_secret_id).toBe("client-secret-sec"); | ||
| 126 | |||
| 127 | const headers = after | ||
| 128 | .prepare( | ||
| 129 | "SELECT name, kind, text_value, secret_id FROM mcp_source_header WHERE source_id = ? ORDER BY name", | ||
| 130 | ) | ||
| 131 | .all("remote-oauth") as ReadonlyArray<Record<string, string | null>>; | ||
| 132 | expect(headers).toHaveLength(2); | ||
| 133 | const byName = new Map(headers.map((h) => [h.name, h])); | ||
| 134 | expect(byName.get("X-Trace")).toMatchObject({ | ||
| 135 | kind: "text", | ||
| 136 | text_value: "static", | ||
| 137 | }); | ||
| 138 | expect(byName.get("X-Token")).toMatchObject({ | ||
| 139 | kind: "secret", | ||
| 140 | secret_id: "extra-tok", | ||
| 141 | }); | ||
| 142 | |||
| 143 | const params = after | ||
| 144 | .prepare( | ||
| 145 | "SELECT name, secret_id FROM mcp_source_query_param WHERE source_id = ?", | ||
| 146 | ) | ||
| 147 | .all("remote-oauth") as ReadonlyArray<Record<string, string>>; | ||
| 148 | expect(params).toHaveLength(1); | ||
| 149 | expect(params[0]).toMatchObject({ name: "org", secret_id: "org-id-secret" }); | ||
| 150 | |||
| 151 | after.close(); | ||
| 152 | } finally { | ||
| 153 | rmSync(dir, { recursive: true, force: true }); | ||
| 154 | } | ||
| 155 | }); | ||
| 156 | |||
| 157 | it("leaves stdio sources alone (no auth, no headers, no queryParams)", () => { | ||
| 158 | const dir = mkdtempSync(join(tmpdir(), "mcp-mig-")); | ||
| 159 | const dbPath = join(dir, "test.sqlite"); | ||
| 160 | try { | ||
| 161 | const db = new Database(dbPath); | ||
| 162 | db.exec(PRE_0007_SQL); | ||
| 163 | stampPriorMigrationsApplied(db); | ||
| 164 | |||
| 165 | db.prepare( | ||
| 166 | "INSERT INTO mcp_source (scope_id, id, name, config, created_at) VALUES (?, ?, ?, ?, ?)", | ||
| 167 | ).run( | ||
| 168 | "default-scope", | ||
| 169 | "stdio-only", | ||
| 170 | "Stdio", | ||
| 171 | JSON.stringify({ | ||
| 172 | transport: "stdio", | ||
| 173 | command: "/usr/bin/server", | ||
| 174 | args: ["--flag"], | ||
| 175 | }), | ||
| 176 | Date.now(), | ||
| 177 | ); | ||
| 178 | |||
| 179 | db.close(); | ||
| 180 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 181 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 182 | |||
| 183 | const after = new Database(dbPath, { readonly: true }); | ||
| 184 | const row = after | ||
| 185 | .prepare( | ||
| 186 | "SELECT auth_kind, auth_secret_id, config FROM mcp_source WHERE id = ?", | ||
| 187 | ) | ||
| 188 | .get("stdio-only") as { | ||
| 189 | auth_kind: string; | ||
| 190 | auth_secret_id: string | null; | ||
| 191 | config: string; | ||
| 192 | }; | ||
| 193 | expect(row.auth_kind).toBe("none"); | ||
| 194 | expect(row.auth_secret_id).toBeNull(); | ||
| 195 | const config = JSON.parse(row.config); | ||
| 196 | expect(config.transport).toBe("stdio"); | ||
| 197 | expect(config.command).toBe("/usr/bin/server"); | ||
| 198 | after.close(); | ||
| 199 | } finally { | ||
| 200 | rmSync(dir, { recursive: true, force: true }); | ||
| 201 | } | ||
| 202 | }); | ||
| 203 | }); | ||
| @@ -0,0 +1,251 @@ | |||
| 1 | // End-to-end test for the openapi portion of | ||
| 2 | // `0007_normalize_plugin_secret_refs.sql`. Seeds the pre-migration | ||
| 3 | // shape (json blobs on openapi_source.query_params, | ||
| 4 | // openapi_source.invocation_config.specFetchCredentials.*, and | ||
| 5 | // openapi_source_binding.value), runs the migration runner, asserts | ||
| 6 | // the new flat columns + child tables match. | ||
| 7 | |||
| 8 | import { describe, expect, it } from "@effect/vitest"; | ||
| 9 | import { Database } from "bun:sqlite"; | ||
| 10 | import { mkdtempSync, rmSync } from "node:fs"; | ||
| 11 | import { join } from "node:path"; | ||
| 12 | import { tmpdir } from "node:os"; | ||
| 13 | import { drizzle } from "drizzle-orm/bun-sqlite"; | ||
| 14 | import { migrate } from "drizzle-orm/bun-sqlite/migrator"; | ||
| 15 | |||
| 16 | import { PRE_0007_SQL, stampPriorMigrationsApplied } from "./__test-helpers__/pre-0007-schema"; | ||
| 17 | |||
| 18 | const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); | ||
| 19 | |||
| 20 | describe("0007_normalize_plugin_secret_refs (openapi)", () => { | ||
| 21 | it("flattens openapi_source_binding.value into kind/secret_id/connection_id/text_value", () => { | ||
| 22 | const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); | ||
| 23 | const dbPath = join(dir, "test.sqlite"); | ||
| 24 | try { | ||
| 25 | const db = new Database(dbPath); | ||
| 26 | db.exec(PRE_0007_SQL); | ||
| 27 | stampPriorMigrationsApplied(db); | ||
| 28 | |||
| 29 | // Seed three bindings, one per kind. | ||
| 30 | const insert = db.prepare( | ||
| 31 | "INSERT INTO openapi_source_binding (id, source_id, source_scope_id, target_scope_id, slot, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", | ||
| 32 | ); | ||
| 33 | const now = Date.now(); | ||
| 34 | insert.run( | ||
| 35 | "b1", | ||
| 36 | "src", | ||
| 37 | "default-scope", | ||
| 38 | "default-scope", | ||
| 39 | "header:authorization", | ||
| 40 | JSON.stringify({ kind: "secret", secretId: "tok-secret" }), | ||
| 41 | now, | ||
| 42 | now, | ||
| 43 | ); | ||
| 44 | insert.run( | ||
| 45 | "b2", | ||
| 46 | "src", | ||
| 47 | "default-scope", | ||
| 48 | "default-scope", | ||
| 49 | "oauth2:default:connection", | ||
| 50 | JSON.stringify({ kind: "connection", connectionId: "conn-1" }), | ||
| 51 | now, | ||
| 52 | now, | ||
| 53 | ); | ||
| 54 | insert.run( | ||
| 55 | "b3", | ||
| 56 | "src", | ||
| 57 | "default-scope", | ||
| 58 | "default-scope", | ||
| 59 | "header:x-static", | ||
| 60 | JSON.stringify({ kind: "text", text: "literal" }), | ||
| 61 | now, | ||
| 62 | now, | ||
| 63 | ); | ||
| 64 | |||
| 65 | // Need the parent openapi_source row so the source_id FK ergonomics | ||
| 66 | // are satisfied for any cascading delete logic — though the binding | ||
| 67 | // table has no DB-level FK, code paths assume the parent exists. | ||
| 68 | db.prepare( | ||
| 69 | "INSERT INTO openapi_source (scope_id, id, name, spec, invocation_config) VALUES (?, ?, ?, ?, ?)", | ||
| 70 | ).run("default-scope", "src", "Source", "{}", "{}"); | ||
| 71 | |||
| 72 | db.close(); | ||
| 73 | |||
| 74 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 75 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 76 | |||
| 77 | const after = new Database(dbPath, { readonly: true }); | ||
| 78 | const rows = after | ||
| 79 | .prepare( | ||
| 80 | "SELECT id, kind, secret_id, connection_id, text_value FROM openapi_source_binding ORDER BY id", | ||
| 81 | ) | ||
| 82 | .all() as ReadonlyArray<{ | ||
| 83 | id: string; | ||
| 84 | kind: string; | ||
| 85 | secret_id: string | null; | ||
| 86 | connection_id: string | null; | ||
| 87 | text_value: string | null; | ||
| 88 | }>; | ||
| 89 | expect(rows).toHaveLength(3); | ||
| 90 | expect(rows[0]).toMatchObject({ | ||
| 91 | id: "b1", | ||
| 92 | kind: "secret", | ||
| 93 | secret_id: "tok-secret", | ||
| 94 | connection_id: null, | ||
| 95 | text_value: null, | ||
| 96 | }); | ||
| 97 | expect(rows[1]).toMatchObject({ | ||
| 98 | id: "b2", | ||
| 99 | kind: "connection", | ||
| 100 | secret_id: null, | ||
| 101 | connection_id: "conn-1", | ||
| 102 | text_value: null, | ||
| 103 | }); | ||
| 104 | expect(rows[2]).toMatchObject({ | ||
| 105 | id: "b3", | ||
| 106 | kind: "text", | ||
| 107 | secret_id: null, | ||
| 108 | connection_id: null, | ||
| 109 | text_value: "literal", | ||
| 110 | }); | ||
| 111 | // value json column dropped. | ||
| 112 | const cols = after | ||
| 113 | .prepare("PRAGMA table_info('openapi_source_binding')") | ||
| 114 | .all() as ReadonlyArray<{ name: string }>; | ||
| 115 | expect(cols.some((c) => c.name === "value")).toBe(false); | ||
| 116 | after.close(); | ||
| 117 | } finally { | ||
| 118 | rmSync(dir, { recursive: true, force: true }); | ||
| 119 | } | ||
| 120 | }); | ||
| 121 | |||
| 122 | it("explodes query_params and specFetchCredentials json into child rows", () => { | ||
| 123 | const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); | ||
| 124 | const dbPath = join(dir, "test.sqlite"); | ||
| 125 | try { | ||
| 126 | const db = new Database(dbPath); | ||
| 127 | db.exec(PRE_0007_SQL); | ||
| 128 | stampPriorMigrationsApplied(db); | ||
| 129 | |||
| 130 | const queryParams = { | ||
| 131 | api_key: { secretId: "qp-secret" }, | ||
| 132 | flag: "true", | ||
| 133 | }; | ||
| 134 | const invocationConfig = { | ||
| 135 | specFetchCredentials: { | ||
| 136 | headers: { | ||
| 137 | Authorization: { secretId: "fetch-tok", prefix: "Bearer " }, | ||
| 138 | }, | ||
| 139 | queryParams: { token: { secretId: "fetch-qp" } }, | ||
| 140 | }, | ||
| 141 | }; | ||
| 142 | |||
| 143 | db.prepare( | ||
| 144 | "INSERT INTO openapi_source (scope_id, id, name, spec, query_params, invocation_config) VALUES (?, ?, ?, ?, ?, ?)", | ||
| 145 | ).run( | ||
| 146 | "default-scope", | ||
| 147 | "src", | ||
| 148 | "Source", | ||
| 149 | "{}", | ||
| 150 | JSON.stringify(queryParams), | ||
| 151 | JSON.stringify(invocationConfig), | ||
| 152 | ); | ||
| 153 | |||
| 154 | db.close(); | ||
| 155 | |||
| 156 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 157 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 158 | |||
| 159 | const after = new Database(dbPath, { readonly: true }); | ||
| 160 | |||
| 161 | const qpRows = after | ||
| 162 | .prepare( | ||
| 163 | "SELECT name, kind, text_value, secret_id FROM openapi_source_query_param WHERE source_id = ? ORDER BY name", | ||
| 164 | ) | ||
| 165 | .all("src") as ReadonlyArray<{ | ||
| 166 | name: string; | ||
| 167 | kind: string; | ||
| 168 | text_value: string | null; | ||
| 169 | secret_id: string | null; | ||
| 170 | }>; | ||
| 171 | expect(qpRows).toHaveLength(2); | ||
| 172 | const byName = new Map(qpRows.map((r) => [r.name, r])); | ||
| 173 | expect(byName.get("api_key")).toMatchObject({ | ||
| 174 | kind: "secret", | ||
| 175 | secret_id: "qp-secret", | ||
| 176 | }); | ||
| 177 | expect(byName.get("flag")).toMatchObject({ | ||
| 178 | kind: "text", | ||
| 179 | text_value: "true", | ||
| 180 | }); | ||
| 181 | |||
| 182 | const fetchHeaders = after | ||
| 183 | .prepare( | ||
| 184 | "SELECT name, kind, secret_id, secret_prefix FROM openapi_source_spec_fetch_header WHERE source_id = ?", | ||
| 185 | ) | ||
| 186 | .all("src") as ReadonlyArray<{ | ||
| 187 | name: string; | ||
| 188 | kind: string; | ||
| 189 | secret_id: string | null; | ||
| 190 | secret_prefix: string | null; | ||
| 191 | }>; | ||
| 192 | expect(fetchHeaders).toHaveLength(1); | ||
| 193 | expect(fetchHeaders[0]).toMatchObject({ | ||
| 194 | name: "Authorization", | ||
| 195 | kind: "secret", | ||
| 196 | secret_id: "fetch-tok", | ||
| 197 | secret_prefix: "Bearer ", | ||
| 198 | }); | ||
| 199 | |||
| 200 | const fetchQp = after | ||
| 201 | .prepare( | ||
| 202 | "SELECT name, secret_id FROM openapi_source_spec_fetch_query_param WHERE source_id = ?", | ||
| 203 | ) | ||
| 204 | .all("src") as ReadonlyArray<{ name: string; secret_id: string }>; | ||
| 205 | expect(fetchQp).toHaveLength(1); | ||
| 206 | expect(fetchQp[0]).toMatchObject({ name: "token", secret_id: "fetch-qp" }); | ||
| 207 | |||
| 208 | // Old json columns dropped. | ||
| 209 | const cols = after | ||
| 210 | .prepare("PRAGMA table_info('openapi_source')") | ||
| 211 | .all() as ReadonlyArray<{ name: string }>; | ||
| 212 | expect(cols.some((c) => c.name === "query_params")).toBe(false); | ||
| 213 | expect(cols.some((c) => c.name === "invocation_config")).toBe(false); | ||
| 214 | after.close(); | ||
| 215 | } finally { | ||
| 216 | rmSync(dir, { recursive: true, force: true }); | ||
| 217 | } | ||
| 218 | }); | ||
| 219 | |||
| 220 | it("survives empty / missing json on bindings and sources", () => { | ||
| 221 | const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); | ||
| 222 | const dbPath = join(dir, "test.sqlite"); | ||
| 223 | try { | ||
| 224 | const db = new Database(dbPath); | ||
| 225 | db.exec(PRE_0007_SQL); | ||
| 226 | stampPriorMigrationsApplied(db); | ||
| 227 | |||
| 228 | // Source with empty invocation_config and no query_params. | ||
| 229 | db.prepare( | ||
| 230 | "INSERT INTO openapi_source (scope_id, id, name, spec, invocation_config) VALUES (?, ?, ?, ?, ?)", | ||
| 231 | ).run("default-scope", "bare", "Bare", "{}", JSON.stringify({})); | ||
| 232 | |||
| 233 | db.close(); | ||
| 234 | const drizzleDb = drizzle(new Database(dbPath)); | ||
| 235 | migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | ||
| 236 | |||
| 237 | const after = new Database(dbPath, { readonly: true }); | ||
| 238 | const qpCount = ( | ||
| 239 | after | ||
| 240 | .prepare( | ||
| 241 | "SELECT count(*) as n FROM openapi_source_query_param WHERE source_id = ?", | ||
| 242 | ) | ||
| 243 | .get("bare") as { n: number } | ||
| 244 | ).n; | ||
| 245 | expect(qpCount).toBe(0); | ||
| 246 | after.close(); | ||
| 247 | } finally { | ||
| 248 | rmSync(dir, { recursive: true, force: true }); | ||
| 249 | } | ||
| 250 | }); | ||
| 251 | }); | ||
| @@ -1939,7 +1939,7 @@ · 1938 unmodified lines | |||
| 1939 | 1939 | ||
| 1940 | "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], | 1940 | "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], |
| 1941 | 1941 | ||
| 1942 | "@rhyssul/portless": ["@rhyssul/portless@0.13.0", "", { "os": [ "linux", "win32", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-6Hc5jcNVmEmbQUK4YT6hGNxNbDob8ZrB6izFLxZOq8P9ExS6eeBJQA9J55K67unNGWOiA77xUaSBut3SzqcYgA=="], | 1942 | "@rhyssul/portless": ["@rhyssul/portless@0.13.3", "", { "os": [ "linux", "win32", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-oPvwXJIIkRg2i1CUEUsz8sAdFSf0Fzzv+NmaacAsi6ZZTHxDOofbO6fGInVnbESIFOu4k8a/rXOZRF0aqLAUgw=="], |
| 1943 | 1943 | ||
| 1944 | "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], | 1944 | "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], |
| 1945 | 1945 | ||
| @@ -3,7 +3,9 @@ import { Schema } from "effect"; · 2 unmodified lines | |||
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 5 | ConnectionId, | 5 | ConnectionId, |
| 6 | ConnectionInUseError, | ||
| 6 | ScopeId, | 7 | ScopeId, |
| 8 | Usage, | ||
| 7 | } from "@executor-js/sdk"; | 9 | } from "@executor-js/sdk"; |
| 8 | 10 | ||
| 9 | import { InternalError } from "../observability"; | 11 | import { InternalError } from "../observability"; |
| @@ -34,6 +36,8 @@ const ConnectionRefResponse = Schema.Struct({ · 24 unmodified lines | |||
| 34 | // Group | 36 | // Group |
| 35 | // --------------------------------------------------------------------------- | 37 | // --------------------------------------------------------------------------- |
| 36 | 38 | ||
| 39 | const ConnectionInUse = ConnectionInUseError.annotate({ httpApiStatus: 409 }); | ||
| 40 | |||
| 37 | export const ConnectionsApi = HttpApiGroup.make("connections") | 41 | export const ConnectionsApi = HttpApiGroup.make("connections") |
| 38 | .add( | 42 | .add( |
| 39 | HttpApiEndpoint.get("list", "/scopes/:scopeId/connections", { | 43 | HttpApiEndpoint.get("list", "/scopes/:scopeId/connections", { |
| @@ -46,6 +50,17 @@ export const ConnectionsApi = HttpApiGroup.make("connections") · 6 unmodified lines | |||
| 46 | HttpApiEndpoint.delete("remove", "/scopes/:scopeId/connections/:connectionId", { | 50 | HttpApiEndpoint.delete("remove", "/scopes/:scopeId/connections/:connectionId", { |
| 47 | params: ConnectionParams, | 51 | params: ConnectionParams, |
| 48 | success: Schema.Struct({ removed: Schema.Boolean }), | 52 | success: Schema.Struct({ removed: Schema.Boolean }), |
| 49 | error: InternalError, | 53 | error: [InternalError, ConnectionInUse], |
| 50 | }), | 54 | }), |
| 55 | ) | ||
| 56 | .add( | ||
| 57 | HttpApiEndpoint.get( | ||
| 58 | "usages", | ||
| 59 | "/scopes/:scopeId/connections/:connectionId/usages", | ||
| 60 | { | ||
| 61 | params: ConnectionParams, | ||
| 62 | success: Schema.Array(Usage), | ||
| 63 | error: InternalError, | ||
| 64 | }, | ||
| 65 | ), | ||
| 51 | ); | 66 | ); |
| @@ -40,5 +40,13 @@ export const ConnectionsHandlers = HttpApiBuilder.group( · 39 unmodified lines | |||
| 40 | return { removed: true }; | 40 | return { removed: true }; |
| 41 | }), | 41 | }), |
| 42 | ), | 42 | ), |
| 43 | ) | ||
| 44 | .handle("usages", ({ params: path }) => | ||
| 45 | capture( | ||
| 46 | Effect.gen(function* () { | ||
| 47 | const executor = yield* ExecutorService; | ||
| 48 | return yield* executor.connections.usages(path.connectionId); | ||
| 49 | }), | ||
| 50 | ), | ||
| 43 | ), | 51 | ), |
| 44 | ); | 52 | ); |
| @@ -51,5 +51,11 @@ export const SecretsHandlers = HttpApiBuilder.group(ExecutorApi, "secrets", (han · 50 unmodified lines | |||
| 51 | yield* executor.secrets.remove(path.secretId); | 51 | yield* executor.secrets.remove(path.secretId); |
| 52 | return { removed: true }; | 52 | return { removed: true }; |
| 53 | })), | 53 | })), |
| 54 | ) | ||
| 55 | .handle("usages", ({ params: path }) => | ||
| 56 | capture(Effect.gen(function* () { | ||
| 57 | const executor = yield* ExecutorService; | ||
| 58 | return yield* executor.secrets.usages(path.secretId); | ||
| 59 | })), | ||
| 54 | ), | 60 | ), |
| 55 | ); | 61 | ); |
| @@ -3,9 +3,11 @@ import { Schema } from "effect"; · 2 unmodified lines | |||
| 3 | import { | 3 | import { |
| 4 | ScopeId, | 4 | ScopeId, |
| 5 | SecretId, | 5 | SecretId, |
| 6 | SecretInUseError, | ||
| 6 | SecretNotFoundError, | 7 | SecretNotFoundError, |
| 7 | SecretOwnedByConnectionError, | 8 | SecretOwnedByConnectionError, |
| 8 | SecretResolutionError, | 9 | SecretResolutionError, |
| 10 | Usage, | ||
| 9 | } from "@executor-js/sdk"; | 11 | } from "@executor-js/sdk"; |
| 10 | 12 | ||
| 11 | import { InternalError } from "../observability"; | 13 | import { InternalError } from "../observability"; |
| @@ -52,6 +54,7 @@ const SecretResolution = SecretResolutionError.annotate( · 40 unmodified lines | |||
| 52 | const SecretOwnedByConnection = SecretOwnedByConnectionError.annotate( | 54 | const SecretOwnedByConnection = SecretOwnedByConnectionError.annotate( |
| 53 | { httpApiStatus: 409 }, | 55 | { httpApiStatus: 409 }, |
| 54 | ); | 56 | ); |
| 57 | const SecretInUse = SecretInUseError.annotate({ httpApiStatus: 409 }); | ||
| 55 | 58 | ||
| 56 | // --------------------------------------------------------------------------- | 59 | // --------------------------------------------------------------------------- |
| 57 | // Group | 60 | // Group |
| @@ -84,6 +87,13 @@ export const SecretsApi = HttpApiGroup.make("secrets") · 26 unmodified lines | |||
| 84 | HttpApiEndpoint.delete("remove", "/scopes/:scopeId/secrets/:secretId", { | 87 | HttpApiEndpoint.delete("remove", "/scopes/:scopeId/secrets/:secretId", { |
| 85 | params: SecretParams, | 88 | params: SecretParams, |
| 86 | success: Schema.Struct({ removed: Schema.Boolean }), | 89 | success: Schema.Struct({ removed: Schema.Boolean }), |
| 87 | error: [InternalError, SecretNotFound, SecretOwnedByConnection], | 90 | error: [InternalError, SecretNotFound, SecretOwnedByConnection, SecretInUse], |
| 91 | }), | ||
| 92 | ) | ||
| 93 | .add( | ||
| 94 | HttpApiEndpoint.get("usages", "/scopes/:scopeId/secrets/:secretId/usages", { | ||
| 95 | params: SecretParams, | ||
| 96 | success: Schema.Array(Usage), | ||
| 97 | error: InternalError, | ||
| 88 | }), | 98 | }), |
| 89 | ); | 99 | ); |
| @@ -101,6 +101,7 @@ const wrapPromiseExecutor = (pe: PromiseExecutor): EffectExecutor => ({ · 100 unmodified lines | |||
| 101 | set: (input) => fromPromise(() => pe.secrets.set(input)), | 101 | set: (input) => fromPromise(() => pe.secrets.set(input)), |
| 102 | remove: (id) => fromPromise(() => pe.secrets.remove(id)), | 102 | remove: (id) => fromPromise(() => pe.secrets.remove(id)), |
| 103 | list: () => fromPromise(() => pe.secrets.list()), | 103 | list: () => fromPromise(() => pe.secrets.list()), |
| 104 | usages: (id) => fromPromise(() => pe.secrets.usages(id)), | ||
| 104 | providers: () => fromPromise(() => pe.secrets.providers()), | 105 | providers: () => fromPromise(() => pe.secrets.providers()), |
| 105 | }, | 106 | }, |
| 106 | connections: { | 107 | connections: { |
| @@ -111,6 +112,7 @@ const wrapPromiseExecutor = (pe: PromiseExecutor): EffectExecutor => ({ · 4 unmodified lines | |||
| 111 | setIdentityLabel: (id, label) => fromPromise(() => pe.connections.setIdentityLabel(id, label)), | 112 | setIdentityLabel: (id, label) => fromPromise(() => pe.connections.setIdentityLabel(id, label)), |
| 112 | accessToken: (id) => fromPromise(() => pe.connections.accessToken(id)), | 113 | accessToken: (id) => fromPromise(() => pe.connections.accessToken(id)), |
| 113 | remove: (id) => fromPromise(() => pe.connections.remove(id)), | 114 | remove: (id) => fromPromise(() => pe.connections.remove(id)), |
| 115 | usages: (id) => fromPromise(() => pe.connections.usages(id)), | ||
| 114 | providers: () => fromPromise(() => pe.connections.providers()), | 116 | providers: () => fromPromise(() => pe.connections.providers()), |
| 115 | }, | 117 | }, |
| 116 | oauth: { | 118 | oauth: { |
| @@ -98,6 +98,21 @@ export class SecretOwnedByConnectionError extends Schema.TaggedErrorClass<Secret · 97 unmodified lines | |||
| 98 | }, | 98 | }, |
| 99 | ) {} | 99 | ) {} |
| 100 | 100 | ||
| 101 | /** Raised when `secrets.remove(id)` is called on a secret that's still | ||
| 102 | * referenced by one or more sources / bindings across plugins. The UI's | ||
| 103 | * "Used by" list tells the user which sources to detach first. App- | ||
| 104 | * level RESTRICT — the codebase doesn't enforce DB-level FKs because | ||
| 105 | * composite `(scope_id, id)` PKs make single-column references | ||
| 106 | * impossible to constrain in sqlite. `usageCount` is a hint for the | ||
| 107 | * caller; the full list is queryable via `secrets.usages(id)`. */ | ||
| 108 | export class SecretInUseError extends Schema.TaggedErrorClass<SecretInUseError>()( | ||
| 109 | "SecretInUseError", | ||
| 110 | { | ||
| 111 | secretId: SecretId, | ||
| 112 | usageCount: Schema.Number, | ||
| 113 | }, | ||
| 114 | ) {} | ||
| 115 | |||
| 101 | // --------------------------------------------------------------------------- | 116 | // --------------------------------------------------------------------------- |
| 102 | // Connections | 117 | // Connections |
| 103 | // --------------------------------------------------------------------------- | 118 | // --------------------------------------------------------------------------- |
| @@ -141,6 +156,16 @@ export class ConnectionReauthRequiredError extends Schema.TaggedErrorClass<Conne · 37 unmodified lines | |||
| 141 | }, | 156 | }, |
| 142 | ) {} | 157 | ) {} |
| 143 | 158 | ||
| 159 | /** Raised when `connections.remove(id)` is called on a connection that's | ||
| 160 | * still referenced by sources / bindings. Mirrors `SecretInUseError`. */ | ||
| 161 | export class ConnectionInUseError extends Schema.TaggedErrorClass<ConnectionInUseError>()( | ||
| 162 | "ConnectionInUseError", | ||
| 163 | { | ||
| 164 | connectionId: ConnectionId, | ||
| 165 | usageCount: Schema.Number, | ||
| 166 | }, | ||
| 167 | ) {} | ||
| 168 | |||
| 144 | // --------------------------------------------------------------------------- | 169 | // --------------------------------------------------------------------------- |
| 145 | // Union type for convenience in signatures. | 170 | // Union type for convenience in signatures. |
| 146 | // --------------------------------------------------------------------------- | 171 | // --------------------------------------------------------------------------- |
| @@ -156,7 +181,9 @@ export type ExecutorError = · 9 unmodified lines | |||
| 156 | | SecretNotFoundError | 181 | | SecretNotFoundError |
| 157 | | SecretResolutionError | 182 | | SecretResolutionError |
| 158 | | SecretOwnedByConnectionError | 183 | | SecretOwnedByConnectionError |
| 184 | | SecretInUseError | ||
| 159 | | ConnectionNotFoundError | 185 | | ConnectionNotFoundError |
| 160 | | ConnectionProviderNotRegisteredError | 186 | | ConnectionProviderNotRegisteredError |
| 161 | | ConnectionRefreshNotSupportedError | 187 | | ConnectionRefreshNotSupportedError |
| 162 | | ConnectionReauthRequiredError; | 188 | | ConnectionReauthRequiredError |
| 189 | | ConnectionInUseError; | ||
| @@ -44,12 +44,14 @@ import { · 43 unmodified lines | |||
| 44 | type ElicitationRequest, | 44 | type ElicitationRequest, |
| 45 | } from "./elicitation"; | 45 | } from "./elicitation"; |
| 46 | import { | 46 | import { |
| 47 | ConnectionInUseError, | ||
| 47 | ConnectionNotFoundError, | 48 | ConnectionNotFoundError, |
| 48 | ConnectionProviderNotRegisteredError, | 49 | ConnectionProviderNotRegisteredError, |
| 49 | ConnectionReauthRequiredError, | 50 | ConnectionReauthRequiredError, |
| 50 | ConnectionRefreshNotSupportedError, | 51 | ConnectionRefreshNotSupportedError, |
| 51 | NoHandlerError, | 52 | NoHandlerError, |
| 52 | PluginNotLoadedError, | 53 | PluginNotLoadedError, |
| 54 | SecretInUseError, | ||
| 53 | SecretOwnedByConnectionError, | 55 | SecretOwnedByConnectionError, |
| 54 | SourceRemovalNotAllowedError, | 56 | SourceRemovalNotAllowedError, |
| 55 | ToolBlockedError, | 57 | ToolBlockedError, |
| @@ -84,6 +86,7 @@ import { · 28 unmodified lines | |||
| 84 | SetSecretInput, | 86 | SetSecretInput, |
| 85 | type SecretProvider, | 87 | type SecretProvider, |
| 86 | } from "./secrets"; | 88 | } from "./secrets"; |
| 89 | import { Usage } from "./usages"; | ||
| 87 | import { | 90 | import { |
| 88 | ToolSchema, | 91 | ToolSchema, |
| 89 | type Source, | 92 | type Source, |
| @@ -211,11 +214,22 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = []> = { · 121 unmodified lines | |||
| 211 | ) => Effect.Effect<SecretRef, StorageFailure>; | 214 | ) => Effect.Effect<SecretRef, StorageFailure>; |
| 212 | /** Delete a bare (non-connection-owned) secret. Connection-owned | 215 | /** Delete a bare (non-connection-owned) secret. Connection-owned |
| 213 | * secrets are rejected with `SecretOwnedByConnectionError` — use | 216 | * secrets are rejected with `SecretOwnedByConnectionError` — use |
| 214 | * `connections.remove` instead. */ | 217 | * `connections.remove` instead. Refuses with `SecretInUseError` |
| 218 | * if any plugin reports the secret as in use; the caller should | ||
| 219 | * show the `usages(id)` list and ask the user to detach first. */ | ||
| 215 | readonly remove: ( | 220 | readonly remove: ( |
| 216 | id: string, | 221 | id: string, |
| 217 | ) => Effect.Effect<void, SecretOwnedByConnectionError | StorageFailure>; | 222 | ) => Effect.Effect< |
| 223 | void, | ||
| 224 | SecretOwnedByConnectionError | SecretInUseError | StorageFailure | ||
| 225 | >; | ||
| 218 | readonly list: () => Effect.Effect<readonly SecretRef[], StorageFailure>; | 226 | readonly list: () => Effect.Effect<readonly SecretRef[], StorageFailure>; |
| 227 | /** All places this secret is referenced — fans out across every | ||
| 228 | * plugin's `usagesForSecret`. Used by the Secrets-tab "Used by" | ||
| 229 | * list and by `remove` for its RESTRICT check. */ | ||
| 230 | readonly usages: ( | ||
| 231 | id: string, | ||
| 232 | ) => Effect.Effect<readonly Usage[], StorageFailure>; | ||
| 219 | readonly providers: () => Effect.Effect<readonly string[]>; | 233 | readonly providers: () => Effect.Effect<readonly string[]>; |
| 220 | }; | 234 | }; |
| 221 | 235 | ||
| @@ -251,7 +265,16 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = []> = { · 29 unmodified lines | |||
| 251 | | ConnectionRefreshError | 265 | | ConnectionRefreshError |
| 252 | | StorageFailure | 266 | | StorageFailure |
| 253 | >; | 267 | >; |
| 254 | readonly remove: (id: string) => Effect.Effect<void, StorageFailure>; | 268 | /** Refuses with `ConnectionInUseError` if any plugin reports the |
| 269 | * connection as in use. */ | ||
| 270 | readonly remove: ( | ||
| 271 | id: string, | ||
| 272 | ) => Effect.Effect<void, ConnectionInUseError | StorageFailure>; | ||
| 273 | /** All places this connection is referenced — fans out across every | ||
| 274 | * plugin's `usagesForConnection`. */ | ||
| 275 | readonly usages: ( | ||
| 276 | id: string, | ||
| 277 | ) => Effect.Effect<readonly Usage[], StorageFailure>; | ||
| 255 | readonly providers: () => Effect.Effect<readonly string[]>; | 278 | readonly providers: () => Effect.Effect<readonly string[]>; |
| 256 | }; | 279 | }; |
| 257 | 280 | ||
| @@ -945,9 +968,129 @@ export const createExecutor = < · 687 unmodified lines | |||
| 945 | }); | 968 | }); |
| 946 | }); | 969 | }); |
| 947 | 970 | ||
| 971 | // Fan out across every plugin that contributes `usagesForSecret`. Each | ||
| 972 | // plugin queries its own normalized columns through its scoped adapter, | ||
| 973 | // so scope filtering is automatic. | ||
| 974 | // | ||
| 975 | // The display path (`secretsUsages` / `connectionsUsages` from the API) | ||
| 976 | // calls `*Lenient`: per-plugin errors become a logWarning so one buggy | ||
| 977 | // plugin can't break the UI footer. The delete RESTRICT path | ||
| 978 | // (`secretsRemove` / `connectionsRemove`) calls `*Strict`: per-plugin | ||
| 979 | // errors fail the whole call so a transient plugin failure can't be | ||
| 980 | // mistaken for "no usages" and let through a delete that creates | ||
| 981 | // dangling refs. | ||
| 982 | const secretsUsagesStrict = ( | ||
| 983 | id: string, | ||
| 984 | ): Effect.Effect<readonly Usage[], StorageFailure> => | ||
| 985 | Effect.gen(function* () { | ||
| 986 | const secretId = SecretId.make(id); | ||
| 987 | const perPlugin = yield* Effect.all( | ||
| 988 | [...runtimes.values()] | ||
| 989 | .filter((r) => r.plugin.usagesForSecret) | ||
| 990 | .map((r) => | ||
| 991 | r.plugin.usagesForSecret!({ | ||
| 992 | ctx: r.ctx, | ||
| 993 | args: { secretId }, | ||
| 994 | }).pipe( | ||
| 995 | Effect.mapError( | ||
| 996 | (cause): StorageFailure => | ||
| 997 | new StorageError({ | ||
| 998 | message: `usagesForSecret failed for plugin ${r.plugin.id}`, | ||
| 999 | cause, | ||
| 1000 | }), | ||
| 1001 | ), | ||
| 1002 | ), | ||
| 1003 | ), | ||
| 1004 | { concurrency: "unbounded" }, | ||
| 1005 | ); | ||
| 1006 | return perPlugin.flat(); | ||
| 1007 | }); | ||
| 1008 | |||
| 1009 | const secretsUsages = ( | ||
| 1010 | id: string, | ||
| 1011 | ): Effect.Effect<readonly Usage[], StorageFailure> => | ||
| 1012 | Effect.gen(function* () { | ||
| 1013 | const secretId = SecretId.make(id); | ||
| 1014 | const perPlugin = yield* Effect.all( | ||
| 1015 | [...runtimes.values()] | ||
| 1016 | .filter((r) => r.plugin.usagesForSecret) | ||
| 1017 | .map((r) => | ||
| 1018 | r.plugin.usagesForSecret!({ | ||
| 1019 | ctx: r.ctx, | ||
| 1020 | args: { secretId }, | ||
| 1021 | }).pipe( | ||
| 1022 | Effect.catchCause((cause: unknown) => | ||
| 1023 | Effect.logWarning( | ||
| 1024 | `usagesForSecret failed for plugin ${r.plugin.id}`, | ||
| 1025 | cause, | ||
| 1026 | ).pipe(Effect.as([] as readonly Usage[])), | ||
| 1027 | ), | ||
| 1028 | ), | ||
| 1029 | ), | ||
| 1030 | { concurrency: "unbounded" }, | ||
| 1031 | ); | ||
| 1032 | return perPlugin.flat(); | ||
| 1033 | }); | ||
| 1034 | |||
| 1035 | const connectionsUsagesStrict = ( | ||
| 1036 | id: string, | ||
| 1037 | ): Effect.Effect<readonly Usage[], StorageFailure> => | ||
| 1038 | Effect.gen(function* () { | ||
| 1039 | const connectionId = ConnectionId.make(id); | ||
| 1040 | const perPlugin = yield* Effect.all( | ||
| 1041 | [...runtimes.values()] | ||
| 1042 | .filter((r) => r.plugin.usagesForConnection) | ||
| 1043 | .map((r) => | ||
| 1044 | r.plugin.usagesForConnection!({ | ||
| 1045 | ctx: r.ctx, | ||
| 1046 | args: { connectionId }, | ||
| 1047 | }).pipe( | ||
| 1048 | Effect.mapError( | ||
| 1049 | (cause): StorageFailure => | ||
| 1050 | new StorageError({ | ||
| 1051 | message: `usagesForConnection failed for plugin ${r.plugin.id}`, | ||
| 1052 | cause, | ||
| 1053 | }), | ||
| 1054 | ), | ||
| 1055 | ), | ||
| 1056 | ), | ||
| 1057 | { concurrency: "unbounded" }, | ||
| 1058 | ); | ||
| 1059 | return perPlugin.flat(); | ||
| 1060 | }); | ||
| 1061 | |||
| 1062 | const connectionsUsages = ( | ||
| 1063 | id: string, | ||
| 1064 | ): Effect.Effect<readonly Usage[], StorageFailure> => | ||
| 1065 | Effect.gen(function* () { | ||
| 1066 | const connectionId = ConnectionId.make(id); | ||
| 1067 | const perPlugin = yield* Effect.all( | ||
| 1068 | [...runtimes.values()] | ||
| 1069 | .filter((r) => r.plugin.usagesForConnection) | ||
| 1070 | .map((r) => | ||
| 1071 | r.plugin.usagesForConnection!({ | ||
| 1072 | ctx: r.ctx, | ||
| 1073 | args: { connectionId }, | ||
| 1074 | }).pipe( | ||
| 1075 | Effect.catchCause((cause: unknown) => | ||
| 1076 | Effect.logWarning( | ||
| 1077 | `usagesForConnection failed for plugin ${r.plugin.id}`, | ||
| 1078 | cause, | ||
| 1079 | ).pipe(Effect.as([] as readonly Usage[])), | ||
| 1080 | ), | ||
| 1081 | ), | ||
| 1082 | ), | ||
| 1083 | { concurrency: "unbounded" }, | ||
| 1084 | ); | ||
| 1085 | return perPlugin.flat(); | ||
| 1086 | }); | ||
| 1087 | |||
| 948 | const secretsRemove = ( | 1088 | const secretsRemove = ( |
| 949 | id: string, | 1089 | id: string, |
| 950 | ): Effect.Effect<void, SecretOwnedByConnectionError | StorageFailure> => | 1090 | ): Effect.Effect< |
| 1091 | void, | ||
| 1092 | SecretOwnedByConnectionError | SecretInUseError | StorageFailure | ||
| 1093 | > => | ||
| 951 | Effect.gen(function* () { | 1094 | Effect.gen(function* () { |
| 952 | // Remove is shadowing-aware: drop only the innermost-scope row. | 1095 | // Remove is shadowing-aware: drop only the innermost-scope row. |
| 953 | // Removing a user-scope override on a secret that also has an | 1096 | // Removing a user-scope override on a secret that also has an |
| @@ -973,6 +1116,27 @@ export const createExecutor = < · 19 unmodified lines | |||
| 973 | }), | 1116 | }), |
| 974 | ); | 1117 | ); |
| 975 | } | 1118 | } |
| 1119 | // RESTRICT: refuse if any source/binding still references this | ||
| 1120 | // secret AND deleting the innermost row would leave the reference | ||
| 1121 | // dangling. With shadowing, deleting a user-scope override still | ||
| 1122 | // leaves outer-scope rows that the reference resolves to — that | ||
| 1123 | // case is safe to allow. Only block when this is the last row | ||
| 1124 | // with this id across the entire scope stack. | ||
| 1125 | // Strict variant: per-plugin failures fail the gate (vs. lenient | ||
| 1126 | // display path that swallows them) so we never silently let a | ||
| 1127 | // reference dangle on a transient error. | ||
| 1128 | const willDangle = rows.length <= 1; | ||
| 1129 | if (willDangle) { | ||
| 1130 | const usages = yield* secretsUsagesStrict(id); | ||
| 1131 | if (usages.length > 0) { | ||
| 1132 | return yield* Effect.fail( | ||
| 1133 | new SecretInUseError({ | ||
| 1134 | secretId: SecretId.make(id), | ||
| 1135 | usageCount: usages.length, | ||
| 1136 | }), | ||
| 1137 | ); | ||
| 1138 | } | ||
| 1139 | } | ||
| 976 | const targetScope = (target?.scope_id as string | undefined) ?? | 1140 | const targetScope = (target?.scope_id as string | undefined) ?? |
| 977 | scopeIds[0]!; | 1141 | scopeIds[0]!; |
| 978 | 1142 | ||
| @@ -1488,10 +1652,29 @@ export const createExecutor = < · 509 unmodified lines | |||
| 1488 | 1652 | ||
| 1489 | const connectionsRemove = ( | 1653 | const connectionsRemove = ( |
| 1490 | id: string, | 1654 | id: string, |
| 1491 | ): Effect.Effect<void, StorageFailure> => | 1655 | ): Effect.Effect<void, ConnectionInUseError | StorageFailure> => |
| 1492 | Effect.gen(function* () { | 1656 | Effect.gen(function* () { |
| 1493 | const row = yield* findInnermostConnectionRow(id); | 1657 | const allRows = yield* core.findMany({ |
| 1658 | model: "connection", | ||
| 1659 | where: [{ field: "id", value: id }], | ||
| 1660 | }); | ||
| 1661 | const row = findInnermost(allRows as readonly ConnectionRow[]); | ||
| 1494 | if (!row) return; | 1662 | if (!row) return; |
| 1663 | // RESTRICT: refuse if any source/binding still references this | ||
| 1664 | // connection AND deleting the innermost row would leave the | ||
| 1665 | // reference dangling. Same shadowing rationale as `secretsRemove`. | ||
| 1666 | const willDangle = allRows.length <= 1; | ||
| 1667 | if (willDangle) { | ||
| 1668 | const usages = yield* connectionsUsagesStrict(id); | ||
| 1669 | if (usages.length > 0) { | ||
| 1670 | return yield* Effect.fail( | ||
| 1671 | new ConnectionInUseError({ | ||
| 1672 | connectionId: ConnectionId.make(id), | ||
| 1673 | usageCount: usages.length, | ||
| 1674 | }), | ||
| 1675 | ); | ||
| 1676 | } | ||
| 1677 | } | ||
| 1495 | const scope = row.scope_id as string; | 1678 | const scope = row.scope_id as string; |
| 1496 | yield* adapter.transaction(() => | 1679 | yield* adapter.transaction(() => |
| 1497 | Effect.gen(function* () { | 1680 | Effect.gen(function* () { |
| @@ -2800,6 +2983,7 @@ export const createExecutor = < · 1302 unmodified lines | |||
| 2800 | set: secretsSet, | 2983 | set: secretsSet, |
| 2801 | remove: secretsRemove, | 2984 | remove: secretsRemove, |
| 2802 | list: secretsList, | 2985 | list: secretsList, |
| 2986 | usages: secretsUsages, | ||
| 2803 | providers: () => | 2987 | providers: () => |
| 2804 | Effect.sync( | 2988 | Effect.sync( |
| 2805 | () => Array.from(secretProviders.keys()) as readonly string[], | 2989 | () => Array.from(secretProviders.keys()) as readonly string[], |
| @@ -2813,6 +2997,7 @@ export const createExecutor = < · 7 unmodified lines | |||
| 2813 | setIdentityLabel: connectionsSetIdentityLabel, | 2997 | setIdentityLabel: connectionsSetIdentityLabel, |
| 2814 | accessToken: connectionsAccessToken, | 2998 | accessToken: connectionsAccessToken, |
| 2815 | remove: connectionsRemove, | 2999 | remove: connectionsRemove, |
| 3000 | usages: connectionsUsages, | ||
| 2816 | providers: () => | 3001 | providers: () => |
| 2817 | Effect.sync( | 3002 | Effect.sync( |
| 2818 | () => | 3003 | () => |
| @@ -55,10 +55,12 @@ export { · 54 unmodified lines | |||
| 55 | SecretNotFoundError, | 55 | SecretNotFoundError, |
| 56 | SecretResolutionError, | 56 | SecretResolutionError, |
| 57 | SecretOwnedByConnectionError, | 57 | SecretOwnedByConnectionError, |
| 58 | SecretInUseError, | ||
| 58 | ConnectionNotFoundError, | 59 | ConnectionNotFoundError, |
| 59 | ConnectionProviderNotRegisteredError, | 60 | ConnectionProviderNotRegisteredError, |
| 60 | ConnectionRefreshNotSupportedError, | 61 | ConnectionRefreshNotSupportedError, |
| 61 | ConnectionReauthRequiredError, | 62 | ConnectionReauthRequiredError, |
| 63 | ConnectionInUseError, | ||
| 62 | type ExecutorError, | 64 | type ExecutorError, |
| 63 | } from "./errors"; | 65 | } from "./errors"; |
| 64 | 66 | ||
| @@ -118,6 +120,13 @@ export { · 53 unmodified lines | |||
| 118 | type ResolveSecretBackedMapOptions, | 120 | type ResolveSecretBackedMapOptions, |
| 119 | } from "./secret-backed-value"; | 121 | } from "./secret-backed-value"; |
| 120 | 122 | ||
| 123 | // Usage tracking — secret/connection refs across plugins | ||
| 124 | export { | ||
| 125 | Usage, | ||
| 126 | type UsagesForSecretInput, | ||
| 127 | type UsagesForConnectionInput, | ||
| 128 | } from "./usages"; | ||
| 129 | |||
| 121 | // Connections | 130 | // Connections |
| 122 | export { | 131 | export { |
| 123 | ConnectionRef, | 132 | ConnectionRef, |
| @@ -29,15 +29,18 @@ import type { · 28 unmodified lines | |||
| 29 | ElicitationResponse, | 29 | ElicitationResponse, |
| 30 | } from "./elicitation"; | 30 | } from "./elicitation"; |
| 31 | import type { | 31 | import type { |
| 32 | ConnectionInUseError, | ||
| 32 | ConnectionNotFoundError, | 33 | ConnectionNotFoundError, |
| 33 | ConnectionProviderNotRegisteredError, | 34 | ConnectionProviderNotRegisteredError, |
| 34 | ConnectionReauthRequiredError, | 35 | ConnectionReauthRequiredError, |
| 35 | ConnectionRefreshNotSupportedError, | 36 | ConnectionRefreshNotSupportedError, |
| 37 | SecretInUseError, | ||
| 36 | SecretOwnedByConnectionError, | 38 | SecretOwnedByConnectionError, |
| 37 | } from "./errors"; | 39 | } from "./errors"; |
| 38 | import type { OAuthService } from "./oauth"; | 40 | import type { OAuthService } from "./oauth"; |
| 39 | import type { Scope } from "./scope"; | 41 | import type { Scope } from "./scope"; |
| 40 | import type { SecretProvider, SecretRef, SetSecretInput } from "./secrets"; | 42 | import type { SecretProvider, SecretRef, SetSecretInput } from "./secrets"; |
| 43 | import type { Usage, UsagesForConnectionInput, UsagesForSecretInput } from "./usages"; | ||
| 41 | 44 | ||
| 42 | // --------------------------------------------------------------------------- | 45 | // --------------------------------------------------------------------------- |
| 43 | // StorageDeps — backing passed to a plugin's `storage` factory. The only | 46 | // StorageDeps — backing passed to a plugin's `storage` factory. The only |
| @@ -160,10 +163,15 @@ export interface PluginCtx<TStore = unknown> { · 116 unmodified lines | |||
| 160 | /** Delete a secret from its pinned provider and the core table. | 163 | /** Delete a secret from its pinned provider and the core table. |
| 161 | * Rejects with `SecretOwnedByConnectionError` if the row is owned | 164 | * Rejects with `SecretOwnedByConnectionError` if the row is owned |
| 162 | * by a connection — callers must go through `connections.remove` | 165 | * by a connection — callers must go through `connections.remove` |
| 163 | * to drop the whole sign-in. */ | 166 | * to drop the whole sign-in. Rejects with `SecretInUseError` if |
| 167 | * any plugin reports the secret as in use; the caller should ask | ||
| 168 | * the user to detach the listed sources first. */ | ||
| 164 | readonly remove: ( | 169 | readonly remove: ( |
| 165 | id: string, | 170 | id: string, |
| 166 | ) => Effect.Effect<void, SecretOwnedByConnectionError | StorageFailure>; | 171 | ) => Effect.Effect< |
| 172 | void, | ||
| 173 | SecretOwnedByConnectionError | SecretInUseError | StorageFailure | ||
| 174 | >; | ||
| 167 | }; | 175 | }; |
| 168 | 176 | ||
| 169 | /** Connections — product-level sign-in state. Owns backing secret | 177 | /** Connections — product-level sign-in state. Owns backing secret |
| @@ -206,7 +214,12 @@ export interface PluginCtx<TStore = unknown> { · 36 unmodified lines | |||
| 206 | | ConnectionRefreshError | 214 | | ConnectionRefreshError |
| 207 | | StorageFailure | 215 | | StorageFailure |
| 208 | >; | 216 | >; |
| 209 | readonly remove: (id: string) => Effect.Effect<void, StorageFailure>; | 217 | /** Refuses with `ConnectionInUseError` if any plugin reports the |
| 218 | * connection as in use. Caller surfaces the `usages` list to the | ||
| 219 | * user. */ | ||
| 220 | readonly remove: ( | ||
| 221 | id: string, | ||
| 222 | ) => Effect.Effect<void, ConnectionInUseError | StorageFailure>; | ||
| 210 | }; | 223 | }; |
| 211 | 224 | ||
| 212 | /** Shared OAuth service. Plugins use this to probe/start/complete OAuth | 225 | /** Shared OAuth service. Plugins use this to probe/start/complete OAuth |
| @@ -460,6 +473,26 @@ export interface PluginSpec< · 247 unmodified lines | |||
| 460 | readonly toolRows: readonly ToolRow[]; | 473 | readonly toolRows: readonly ToolRow[]; |
| 461 | }) => Effect.Effect<Record<string, ToolAnnotations>, unknown>; | 474 | }) => Effect.Effect<Record<string, ToolAnnotations>, unknown>; |
| 462 | 475 | ||
| 476 | /** Find every place a secret id is referenced by this plugin's stored | ||
| 477 | * rows. Implementations query their normalized columns (e.g. | ||
| 478 | * `WHERE secret_id = $1`) and return one `Usage` per hit, with | ||
| 479 | * `ownerKind` / `slot` tagging the location. The executor fans out | ||
| 480 | * across all plugins and the result powers the Secrets-tab "Used | ||
| 481 | * by" list and the deletion-blocking check in `secrets.remove`. | ||
| 482 | * | ||
| 483 | * Plugins that never store secret refs (secret-provider-only | ||
| 484 | * plugins like keychain / file-secrets / 1password) omit this. */ | ||
| 485 | readonly usagesForSecret?: (input: { | ||
| 486 | readonly ctx: PluginCtx<TStore>; | ||
| 487 | readonly args: UsagesForSecretInput; | ||
| 488 | }) => Effect.Effect<readonly Usage[], unknown>; | ||
| 489 | |||
| 490 | /** Same shape as `usagesForSecret`, but for connection refs. */ | ||
| 491 | readonly usagesForConnection?: (input: { | ||
| 492 | readonly ctx: PluginCtx<TStore>; | ||
| 493 | readonly args: UsagesForConnectionInput; | ||
| 494 | }) => Effect.Effect<readonly Usage[], unknown>; | ||
| 495 | |||
| 463 | /** Called when `executor.sources.remove(id)` targets a source owned | 496 | /** Called when `executor.sources.remove(id)` targets a source owned |
| 464 | * by this plugin. Plugin-side cleanup only; the executor deletes | 497 | * by this plugin. Plugin-side cleanup only; the executor deletes |
| 465 | * the core source/tool rows after this callback returns, inside | 498 | * the core source/tool rows after this callback returns, inside |
| @@ -0,0 +1,42 @@ | |||
| 1 | import { Schema } from "effect"; | ||
| 2 | |||
| 3 | import { ScopeId, SecretId, ConnectionId } from "./ids"; | ||
| 4 | |||
| 5 | // --------------------------------------------------------------------------- | ||
| 6 | // Usage — one row per place a secret or connection is referenced. Each | ||
| 7 | // plugin contributes its own usages via `usagesForSecret` / | ||
| 8 | // `usagesForConnection`; the executor fans out and concatenates. | ||
| 9 | // | ||
| 10 | // `pluginId` identifies the plugin that owns the reference. `ownerKind` | ||
| 11 | // is plugin-defined (e.g. "openapi-source-oauth2", "mcp-source-auth", | ||
| 12 | // "graphql-source-header"); the UI groups by it for a "used in N | ||
| 13 | // sources / M bindings" summary. `slot` describes which field within | ||
| 14 | // the owner holds the ref ("oauth2.client_secret", "header:Authorization", | ||
| 15 | // "binding:value") so the user can locate it. | ||
| 16 | // | ||
| 17 | // `ownerName` is resolved by JOIN at query time from the parent source / | ||
| 18 | // binding row. It's nullable because a plugin may have an owner that has | ||
| 19 | // no human-readable name (e.g. an unnamed binding row). | ||
| 20 | // | ||
| 21 | // `scopeId` is the scope the owner row lives in — plugins query through | ||
| 22 | // their scoped adapter (which auto-filters by `scope_id IN (stack)`), so | ||
| 23 | // usages from outer scopes naturally surface alongside inner ones; the | ||
| 24 | // UI uses the scope to render a per-scope label next to each entry. | ||
| 25 | // --------------------------------------------------------------------------- | ||
| 26 | |||
| 27 | export class Usage extends Schema.Class<Usage>("Usage")({ | ||
| 28 | pluginId: Schema.String, | ||
| 29 | scopeId: ScopeId, | ||
| 30 | ownerKind: Schema.String, | ||
| 31 | ownerId: Schema.String, | ||
| 32 | ownerName: Schema.NullOr(Schema.String), | ||
| 33 | slot: Schema.String, | ||
| 34 | }) {} | ||
| 35 | |||
| 36 | export interface UsagesForSecretInput { | ||
| 37 | readonly secretId: SecretId; | ||
| 38 | } | ||
| 39 | |||
| 40 | export interface UsagesForConnectionInput { | ||
| 41 | readonly connectionId: ConnectionId; | ||
| 42 | } | ||
| @@ -21,7 +21,13 @@ import { · 20 unmodified lines | |||
| 21 | type StorageFailure, | 21 | type StorageFailure, |
| 22 | } from "@executor-js/sdk/core"; | 22 | } from "@executor-js/sdk/core"; |
| 23 | 23 | ||
| 24 | import { GoogleDiscoveryMethodBinding, GoogleDiscoveryStoredSourceData } from "./types"; | 24 | import { |
| 25 | GoogleDiscoveryMethodBinding, | ||
| 26 | GoogleDiscoveryStoredSourceData, | ||
| 27 | type GoogleDiscoveryAuth, | ||
| 28 | type GoogleDiscoveryCredentialValue, | ||
| 29 | type GoogleDiscoveryFetchCredentials, | ||
| 30 | } from "./types"; | ||
| 25 | 31 | ||
| 26 | // --------------------------------------------------------------------------- | 32 | // --------------------------------------------------------------------------- |
| 27 | // OAuth session TTL | 33 | // OAuth session TTL |
| @@ -39,11 +45,58 @@ export const googleDiscoverySchema = defineSchema({ · 11 unmodified lines | |||
| 39 | id: { type: "string", required: true }, | 45 | id: { type: "string", required: true }, |
| 40 | scope_id: { type: "string", required: true, index: true }, | 46 | scope_id: { type: "string", required: true, index: true }, |
| 41 | name: { type: "string", required: true }, | 47 | name: { type: "string", required: true }, |
| 48 | // Plugin-private structural config minus auth/credentials — | ||
| 49 | // discoveryUrl, service, version, rootUrl, servicePath. These | ||
| 50 | // never carry refs. | ||
| 42 | config: { type: "json", required: true }, | 51 | config: { type: "json", required: true }, |
| 52 | // Flattened GoogleDiscoveryAuth. | ||
| 53 | auth_kind: { | ||
| 54 | type: ["none", "oauth2"], | ||
| 55 | required: true, | ||
| 56 | defaultValue: "none", | ||
| 57 | }, | ||
| 58 | auth_connection_id: { type: "string", required: false, index: true }, | ||
| 59 | auth_client_id_secret_id: { | ||
| 60 | type: "string", | ||
| 61 | required: false, | ||
| 62 | index: true, | ||
| 63 | }, | ||
| 64 | auth_client_secret_secret_id: { | ||
| 65 | type: "string", | ||
| 66 | required: false, | ||
| 67 | index: true, | ||
| 68 | }, | ||
| 69 | // Stored as a string[] (JSON-backed but not a ref-bearing column). | ||
| 70 | // Empty array when auth_kind is "none". | ||
| 71 | auth_scopes: { type: "string[]", required: false }, | ||
| 43 | created_at: { type: "date", required: true }, | 72 | created_at: { type: "date", required: true }, |
| 44 | updated_at: { type: "date", required: true }, | 73 | updated_at: { type: "date", required: true }, |
| 45 | }, | 74 | }, |
| 46 | }, | 75 | }, |
| 76 | google_discovery_source_credential_header: { | ||
| 77 | fields: { | ||
| 78 | id: { type: "string", required: true }, | ||
| 79 | scope_id: { type: "string", required: true, index: true }, | ||
| 80 | source_id: { type: "string", required: true, index: true }, | ||
| 81 | name: { type: "string", required: true }, | ||
| 82 | kind: { type: ["text", "secret"], required: true }, | ||
| 83 | text_value: { type: "string", required: false }, | ||
| 84 | secret_id: { type: "string", required: false, index: true }, | ||
| 85 | secret_prefix: { type: "string", required: false }, | ||
| 86 | }, | ||
| 87 | }, | ||
| 88 | google_discovery_source_credential_query_param: { | ||
| 89 | fields: { | ||
| 90 | id: { type: "string", required: true }, | ||
| 91 | scope_id: { type: "string", required: true, index: true }, | ||
| 92 | source_id: { type: "string", required: true, index: true }, | ||
| 93 | name: { type: "string", required: true }, | ||
| 94 | kind: { type: ["text", "secret"], required: true }, | ||
| 95 | text_value: { type: "string", required: false }, | ||
| 96 | secret_id: { type: "string", required: false, index: true }, | ||
| 97 | secret_prefix: { type: "string", required: false }, | ||
| 98 | }, | ||
| 99 | }, | ||
| 47 | google_discovery_binding: { | 100 | google_discovery_binding: { |
| 48 | fields: { | 101 | fields: { |
| 49 | id: { type: "string", required: true }, | 102 | id: { type: "string", required: true }, |
| @@ -93,6 +146,112 @@ const decodeJson = (value: unknown): unknown => { · 43 unmodified lines | |||
| 93 | } | 146 | } |
| 94 | }; | 147 | }; |
| 95 | 148 | ||
| 149 | // --- auth column packing/unpacking ------------------------------------------ | ||
| 150 | |||
| 151 | interface AuthColumns { | ||
| 152 | readonly auth_kind: "none" | "oauth2"; | ||
| 153 | readonly auth_connection_id?: string; | ||
| 154 | readonly auth_client_id_secret_id?: string; | ||
| 155 | readonly auth_client_secret_secret_id?: string; | ||
| 156 | // Mutable rather than readonly so the typed adapter's RowInput shape | ||
| 157 | // (which expects `string[]`, not `readonly string[]`) is satisfied. | ||
| 158 | readonly auth_scopes?: string[]; | ||
| 159 | } | ||
| 160 | |||
| 161 | const authToColumns = (auth: GoogleDiscoveryAuth): AuthColumns => { | ||
| 162 | if (auth.kind === "oauth2") { | ||
| 163 | return { | ||
| 164 | auth_kind: "oauth2", | ||
| 165 | auth_connection_id: auth.connectionId, | ||
| 166 | auth_client_id_secret_id: auth.clientIdSecretId, | ||
| 167 | auth_client_secret_secret_id: auth.clientSecretSecretId ?? undefined, | ||
| 168 | auth_scopes: [...auth.scopes], | ||
| 169 | }; | ||
| 170 | } | ||
| 171 | return { auth_kind: "none" }; | ||
| 172 | }; | ||
| 173 | |||
| 174 | const columnsToAuth = (row: Record<string, unknown>): GoogleDiscoveryAuth => { | ||
| 175 | if ( | ||
| 176 | row.auth_kind === "oauth2" && | ||
| 177 | typeof row.auth_connection_id === "string" && | ||
| 178 | typeof row.auth_client_id_secret_id === "string" | ||
| 179 | ) { | ||
| 180 | const csec = row.auth_client_secret_secret_id as string | null | undefined; | ||
| 181 | const scopes = (row.auth_scopes as readonly string[] | null | undefined) ?? []; | ||
| 182 | return { | ||
| 183 | kind: "oauth2", | ||
| 184 | connectionId: row.auth_connection_id, | ||
| 185 | clientIdSecretId: row.auth_client_id_secret_id, | ||
| 186 | clientSecretSecretId: csec ?? null, | ||
| 187 | scopes: [...scopes], | ||
| 188 | }; | ||
| 189 | } | ||
| 190 | return { kind: "none" }; | ||
| 191 | }; | ||
| 192 | |||
| 193 | // --- SecretBackedValue maps <-> child rows ---------------------------------- | ||
| 194 | |||
| 195 | interface CredentialRow { | ||
| 196 | readonly id: string; | ||
| 197 | readonly scope_id: string; | ||
| 198 | readonly source_id: string; | ||
| 199 | readonly name: string; | ||
| 200 | readonly kind: "text" | "secret"; | ||
| 201 | readonly text_value?: string; | ||
| 202 | readonly secret_id?: string; | ||
| 203 | readonly secret_prefix?: string; | ||
| 204 | readonly [k: string]: unknown; | ||
| 205 | } | ||
| 206 | |||
| 207 | const valueMapToRows = ( | ||
| 208 | sourceId: string, | ||
| 209 | scope: string, | ||
| 210 | values: Record<string, GoogleDiscoveryCredentialValue> | undefined, | ||
| 211 | ): readonly CredentialRow[] => { | ||
| 212 | if (!values) return []; | ||
| 213 | return Object.entries(values).map(([name, value]) => { | ||
| 214 | const id = JSON.stringify([sourceId, name]); | ||
| 215 | if (typeof value === "string") { | ||
| 216 | return { | ||
| 217 | id, | ||
| 218 | scope_id: scope, | ||
| 219 | source_id: sourceId, | ||
| 220 | name, | ||
| 221 | kind: "text", | ||
| 222 | text_value: value, | ||
| 223 | }; | ||
| 224 | } | ||
| 225 | return { | ||
| 226 | id, | ||
| 227 | scope_id: scope, | ||
| 228 | source_id: sourceId, | ||
| 229 | name, | ||
| 230 | kind: "secret", | ||
| 231 | secret_id: value.secretId, | ||
| 232 | secret_prefix: value.prefix, | ||
| 233 | }; | ||
| 234 | }); | ||
| 235 | }; | ||
| 236 | |||
| 237 | const rowsToValueMap = ( | ||
| 238 | rows: readonly Record<string, unknown>[], | ||
| 239 | ): Record<string, GoogleDiscoveryCredentialValue> => { | ||
| 240 | const out: Record<string, GoogleDiscoveryCredentialValue> = {}; | ||
| 241 | for (const row of rows) { | ||
| 242 | const name = row.name as string; | ||
| 243 | if (row.kind === "secret" && typeof row.secret_id === "string") { | ||
| 244 | const prefix = row.secret_prefix as string | undefined | null; | ||
| 245 | out[name] = prefix | ||
| 246 | ? { secretId: row.secret_id, prefix } | ||
| 247 | : { secretId: row.secret_id }; | ||
| 248 | } else if (row.kind === "text" && typeof row.text_value === "string") { | ||
| 249 | out[name] = row.text_value; | ||
| 250 | } | ||
| 251 | } | ||
| 252 | return out; | ||
| 253 | }; | ||
| 254 | |||
| 96 | // --------------------------------------------------------------------------- | 255 | // --------------------------------------------------------------------------- |
| 97 | // Store interface | 256 | // Store interface |
| 98 | // --------------------------------------------------------------------------- | 257 | // --------------------------------------------------------------------------- |
| @@ -154,6 +313,52 @@ export interface GoogleDiscoveryStore { · 55 unmodified lines | |||
| 154 | sourceId: string, | 313 | sourceId: string, |
| 155 | scope: string, | 314 | scope: string, |
| 156 | ) => Effect.Effect<GoogleDiscoveryStoredSourceData | null, StorageFailure>; | 315 | ) => Effect.Effect<GoogleDiscoveryStoredSourceData | null, StorageFailure>; |
| 316 | |||
| 317 | // --------------------------------------------------------------------- | ||
| 318 | // Usage lookups — back `usagesForSecret` / `usagesForConnection`. | ||
| 319 | // --------------------------------------------------------------------- | ||
| 320 | |||
| 321 | /** Source rows whose oauth2 auth columns reference the given secret id. | ||
| 322 | * `slot` distinguishes client_id vs client_secret. */ | ||
| 323 | readonly findSourcesBySecret: ( | ||
| 324 | secretId: string, | ||
| 325 | ) => Effect.Effect< | ||
| 326 | readonly { | ||
| 327 | readonly namespace: string; | ||
| 328 | readonly scope_id: string; | ||
| 329 | readonly name: string; | ||
| 330 | readonly slot: string; | ||
| 331 | }[], | ||
| 332 | StorageFailure | ||
| 333 | >; | ||
| 334 | |||
| 335 | /** Source rows whose oauth2 auth points at the given connection id. */ | ||
| 336 | readonly findSourcesByConnection: ( | ||
| 337 | connectionId: string, | ||
| 338 | ) => Effect.Effect< | ||
| 339 | readonly { | ||
| 340 | readonly namespace: string; | ||
| 341 | readonly scope_id: string; | ||
| 342 | readonly name: string; | ||
| 343 | readonly slot: string; | ||
| 344 | }[], | ||
| 345 | StorageFailure | ||
| 346 | >; | ||
| 347 | |||
| 348 | /** Credential header / query_param child rows referencing the secret. */ | ||
| 349 | readonly findCredentialRowsBySecret: (secretId: string) => Effect.Effect< | ||
| 350 | readonly { | ||
| 351 | readonly kind: "credential_header" | "credential_query_param"; | ||
| 352 | readonly source_id: string; | ||
| 353 | readonly scope_id: string; | ||
| 354 | readonly name: string; | ||
| 355 | }[], | ||
| 356 | StorageFailure | ||
| 357 | >; | ||
| 358 | |||
| 359 | readonly lookupSourceNames: ( | ||
| 360 | keys: readonly string[], | ||
| 361 | ) => Effect.Effect<ReadonlyMap<string, string>, StorageFailure>; | ||
| 157 | } | 362 | } |
| 158 | 363 | ||
| 159 | // --------------------------------------------------------------------------- | 364 | // --------------------------------------------------------------------------- |
| @@ -245,6 +450,8 @@ export const makeGoogleDiscoveryStore = ( · 85 unmodified lines | |||
| 245 | putSource: (source) => | 450 | putSource: (source) => |
| 246 | Effect.gen(function* () { | 451 | Effect.gen(function* () { |
| 247 | const now = new Date(); | 452 | const now = new Date(); |
| 453 | // Wipe the source row + every child row before recreating — | ||
| 454 | // matches putSource's "fully replace" semantic. | ||
| 248 | yield* db.delete({ | 455 | yield* db.delete({ |
| 249 | model: "google_discovery_source", | 456 | model: "google_discovery_source", |
| 250 | where: [ | 457 | where: [ |
| @@ -252,18 +459,29 @@ export const makeGoogleDiscoveryStore = ( · 1 unmodified line | |||
| 252 | { field: "scope_id", value: source.scope }, | 459 | { field: "scope_id", value: source.scope }, |
| 253 | ], | 460 | ], |
| 254 | }); | 461 | }); |
| 462 | yield* deleteSourceChildren(source.namespace, source.scope); | ||
| 463 | |||
| 464 | const encoded = stripExtractedFields( | ||
| 465 | encodeStoredSourceData(source.config) as Record<string, unknown>, | ||
| 466 | ); | ||
| 255 | yield* db.create({ | 467 | yield* db.create({ |
| 256 | model: "google_discovery_source", | 468 | model: "google_discovery_source", |
| 257 | data: { | 469 | data: { |
| 258 | id: source.namespace, | 470 | id: source.namespace, |
| 259 | scope_id: source.scope, | 471 | scope_id: source.scope, |
| 260 | name: source.name, | 472 | name: source.name, |
| 261 | config: toJsonRecord(encodeStoredSourceData(source.config)), | 473 | config: toJsonRecord(encoded), |
| 262 | created_at: now, | 474 | created_at: now, |
| 263 | updated_at: now, | 475 | updated_at: now, |
| 476 | ...authToColumns(source.config.auth), | ||
| 264 | }, | 477 | }, |
| 265 | forceAllowId: true, | 478 | forceAllowId: true, |
| 266 | }); | 479 | }); |
| 480 | yield* writeCredentialRows( | ||
| 481 | source.namespace, | ||
| 482 | source.scope, | ||
| 483 | source.config.credentials, | ||
| 484 | ); | ||
| 267 | }), | 485 | }), |
| 268 | 486 | ||
| 269 | updateSourceMeta: (sourceId, scope, update) => | 487 | updateSourceMeta: (sourceId, scope, update) => |
| @@ -276,12 +494,7 @@ export const makeGoogleDiscoveryStore = ( · 6 unmodified lines | |||
| 276 | ], | 494 | ], |
| 277 | }); | 495 | }); |
| 278 | if (!row) return; | 496 | if (!row) return; |
| 279 | const config = decodeStoredSourceData(decodeJson(row.config)); | 497 | const auth = update.auth ?? columnsToAuth(row); |
| 280 | const nextConfig = new GoogleDiscoveryStoredSourceData({ | ||
| 281 | ...config, | ||
| 282 | name: update.name ?? config.name, | ||
| 283 | auth: update.auth ?? config.auth, | ||
| 284 | }); | ||
| 285 | yield* db.update({ | 498 | yield* db.update({ |
| 286 | model: "google_discovery_source", | 499 | model: "google_discovery_source", |
| 287 | where: [ | 500 | where: [ |
| @@ -290,22 +503,23 @@ export const makeGoogleDiscoveryStore = ( · 2 unmodified lines | |||
| 290 | ], | 503 | ], |
| 291 | update: { | 504 | update: { |
| 292 | name: update.name ?? (row.name as string), | 505 | name: update.name ?? (row.name as string), |
| 293 | config: toJsonRecord(encodeStoredSourceData(nextConfig)), | ||
| 294 | updated_at: new Date(), | 506 | updated_at: new Date(), |
| 507 | ...authToColumns(auth), | ||
| 295 | }, | 508 | }, |
| 296 | }); | 509 | }); |
| 297 | }), | 510 | }), |
| 298 | 511 | ||
| 299 | removeSource: (sourceId, scope) => | 512 | removeSource: (sourceId, scope) => |
| 300 | db | 513 | Effect.gen(function* () { |
| 301 | .delete({ | 514 | yield* deleteSourceChildren(sourceId, scope); |
| 515 | yield* db.delete({ | ||
| 302 | model: "google_discovery_source", | 516 | model: "google_discovery_source", |
| 303 | where: [ | 517 | where: [ |
| 304 | { field: "id", value: sourceId }, | 518 | { field: "id", value: sourceId }, |
| 305 | { field: "scope_id", value: scope }, | 519 | { field: "scope_id", value: scope }, |
| 306 | ], | 520 | ], |
| 307 | }) | 521 | }); |
| 308 | .pipe(Effect.asVoid), | 522 | }), |
| 309 | 523 | ||
| 310 | getSource: (sourceId, scope) => | 524 | getSource: (sourceId, scope) => |
| 311 | Effect.gen(function* () { | 525 | Effect.gen(function* () { |
| @@ -321,7 +535,7 @@ export const makeGoogleDiscoveryStore = ( · 9 unmodified lines | |||
| 321 | namespace: row.id as string, | 535 | namespace: row.id as string, |
| 322 | scope: row.scope_id as string, | 536 | scope: row.scope_id as string, |
| 323 | name: row.name as string, | 537 | name: row.name as string, |
| 324 | config: decodeStoredSourceData(decodeJson(row.config)), | 538 | config: yield* hydrateStoredSourceData(row, sourceId, scope), |
| 325 | }; | 539 | }; |
| 326 | }), | 540 | }), |
| 327 | 541 | ||
| @@ -335,7 +549,219 @@ export const makeGoogleDiscoveryStore = ( · 7 unmodified lines | |||
| 335 | ], | 549 | ], |
| 336 | }); | 550 | }); |
| 337 | if (!row) return null; | 551 | if (!row) return null; |
| 338 | return decodeStoredSourceData(decodeJson(row.config)); | 552 | return yield* hydrateStoredSourceData(row, sourceId, scope); |
| 553 | }), | ||
| 554 | |||
| 555 | findSourcesBySecret: (secretId) => | ||
| 556 | Effect.gen(function* () { | ||
| 557 | const [byClientId, byClientSecret] = yield* Effect.all( | ||
| 558 | [ | ||
| 559 | db.findMany({ | ||
| 560 | model: "google_discovery_source", | ||
| 561 | where: [ | ||
| 562 | { field: "auth_client_id_secret_id", value: secretId }, | ||
| 563 | ], | ||
| 564 | }), | ||
| 565 | db.findMany({ | ||
| 566 | model: "google_discovery_source", | ||
| 567 | where: [ | ||
| 568 | { field: "auth_client_secret_secret_id", value: secretId }, | ||
| 569 | ], | ||
| 570 | }), | ||
| 571 | ], | ||
| 572 | { concurrency: "unbounded" }, | ||
| 573 | ); | ||
| 574 | const out: { | ||
| 575 | readonly namespace: string; | ||
| 576 | readonly scope_id: string; | ||
| 577 | readonly name: string; | ||
| 578 | readonly slot: string; | ||
| 579 | }[] = []; | ||
| 580 | for (const r of byClientId) { | ||
| 581 | out.push({ | ||
| 582 | namespace: r.id as string, | ||
| 583 | scope_id: r.scope_id as string, | ||
| 584 | name: r.name as string, | ||
| 585 | slot: "auth.oauth2.client_id", | ||
| 586 | }); | ||
| 587 | } | ||
| 588 | for (const r of byClientSecret) { | ||
| 589 | out.push({ | ||
| 590 | namespace: r.id as string, | ||
| 591 | scope_id: r.scope_id as string, | ||
| 592 | name: r.name as string, | ||
| 593 | slot: "auth.oauth2.client_secret", | ||
| 594 | }); | ||
| 595 | } | ||
| 596 | return out; | ||
| 597 | }), | ||
| 598 | |||
| 599 | findSourcesByConnection: (connectionId) => | ||
| 600 | db | ||
| 601 | .findMany({ | ||
| 602 | model: "google_discovery_source", | ||
| 603 | where: [{ field: "auth_connection_id", value: connectionId }], | ||
| 604 | }) | ||
| 605 | .pipe( | ||
| 606 | Effect.map((rows) => | ||
| 607 | rows.map((r) => ({ | ||
| 608 | namespace: r.id as string, | ||
| 609 | scope_id: r.scope_id as string, | ||
| 610 | name: r.name as string, | ||
| 611 | slot: "auth.oauth2.connection", | ||
| 612 | })), | ||
| 613 | ), | ||
| 614 | ), | ||
| 615 | |||
| 616 | findCredentialRowsBySecret: (secretId) => | ||
| 617 | Effect.gen(function* () { | ||
| 618 | const [headers, params] = yield* Effect.all( | ||
| 619 | [ | ||
| 620 | db.findMany({ | ||
| 621 | model: "google_discovery_source_credential_header", | ||
| 622 | where: [{ field: "secret_id", value: secretId }], | ||
| 623 | }), | ||
| 624 | db.findMany({ | ||
| 625 | model: "google_discovery_source_credential_query_param", | ||
| 626 | where: [{ field: "secret_id", value: secretId }], | ||
| 627 | }), | ||
| 628 | ], | ||
| 629 | { concurrency: "unbounded" }, | ||
| 630 | ); | ||
| 631 | return [ | ||
| 632 | ...headers.map((r) => ({ | ||
| 633 | kind: "credential_header" as const, | ||
| 634 | source_id: r.source_id as string, | ||
| 635 | scope_id: r.scope_id as string, | ||
| 636 | name: r.name as string, | ||
| 637 | })), | ||
| 638 | ...params.map((r) => ({ | ||
| 639 | kind: "credential_query_param" as const, | ||
| 640 | source_id: r.source_id as string, | ||
| 641 | scope_id: r.scope_id as string, | ||
| 642 | name: r.name as string, | ||
| 643 | })), | ||
| 644 | ]; | ||
| 645 | }), | ||
| 646 | |||
| 647 | lookupSourceNames: (keys) => | ||
| 648 | Effect.gen(function* () { | ||
| 649 | if (keys.length === 0) return new Map<string, string>(); | ||
| 650 | const rows = yield* db.findMany({ model: "google_discovery_source" }); | ||
| 651 | const requested = new Set(keys); | ||
| 652 | const out = new Map<string, string>(); | ||
| 653 | for (const r of rows) { | ||
| 654 | const key = `${r.scope_id as string}:${r.id as string}`; | ||
| 655 | if (requested.has(key)) out.set(key, r.name as string); | ||
| 656 | } | ||
| 657 | return out; | ||
| 339 | }), | 658 | }), |
| 340 | }; | 659 | }; |
| 660 | |||
| 661 | // --------------------------------------------------------------------- | ||
| 662 | // Closure helpers (depend on `db`). | ||
| 663 | // --------------------------------------------------------------------- | ||
| 664 | |||
| 665 | function deleteSourceChildren(sourceId: string, scope: string) { | ||
| 666 | // Drop only credential child rows. Bindings live independently and | ||
| 667 | // are managed via putBinding / removeBindingsBySource — wiping them | ||
| 668 | // here would break putSource (which legitimately keeps existing | ||
| 669 | // bindings) and the test for "registers and invokes ... tools". | ||
| 670 | return Effect.gen(function* () { | ||
| 671 | for (const model of [ | ||
| 672 | "google_discovery_source_credential_header", | ||
| 673 | "google_discovery_source_credential_query_param", | ||
| 674 | ] as const) { | ||
| 675 | yield* db.deleteMany({ | ||
| 676 | model, | ||
| 677 | where: [ | ||
| 678 | { field: "source_id", value: sourceId }, | ||
| 679 | { field: "scope_id", value: scope }, | ||
| 680 | ], | ||
| 681 | }); | ||
| 682 | } | ||
| 683 | }); | ||
| 684 | } | ||
| 685 | |||
| 686 | function writeCredentialRows( | ||
| 687 | sourceId: string, | ||
| 688 | scope: string, | ||
| 689 | credentials: GoogleDiscoveryFetchCredentials | undefined, | ||
| 690 | ) { | ||
| 691 | return Effect.gen(function* () { | ||
| 692 | if (!credentials) return; | ||
| 693 | const headerRows = valueMapToRows(sourceId, scope, credentials.headers); | ||
| 694 | if (headerRows.length > 0) { | ||
| 695 | yield* db.createMany({ | ||
| 696 | model: "google_discovery_source_credential_header", | ||
| 697 | data: headerRows, | ||
| 698 | forceAllowId: true, | ||
| 699 | }); | ||
| 700 | } | ||
| 701 | const paramRows = valueMapToRows( | ||
| 702 | sourceId, | ||
| 703 | scope, | ||
| 704 | credentials.queryParams, | ||
| 705 | ); | ||
| 706 | if (paramRows.length > 0) { | ||
| 707 | yield* db.createMany({ | ||
| 708 | model: "google_discovery_source_credential_query_param", | ||
| 709 | data: paramRows, | ||
| 710 | forceAllowId: true, | ||
| 711 | }); | ||
| 712 | } | ||
| 713 | }); | ||
| 714 | } | ||
| 715 | |||
| 716 | function hydrateStoredSourceData( | ||
| 717 | row: Record<string, unknown>, | ||
| 718 | sourceId: string, | ||
| 719 | scope: string, | ||
| 720 | ): Effect.Effect<GoogleDiscoveryStoredSourceData, StorageFailure> { | ||
| 721 | return Effect.gen(function* () { | ||
| 722 | const partial = decodeJson(row.config) as Record<string, unknown>; | ||
| 723 | const headerRows = yield* db.findMany({ | ||
| 724 | model: "google_discovery_source_credential_header", | ||
| 725 | where: [ | ||
| 726 | { field: "source_id", value: sourceId }, | ||
| 727 | { field: "scope_id", value: scope }, | ||
| 728 | ], | ||
| 729 | }); | ||
| 730 | const paramRows = yield* db.findMany({ | ||
| 731 | model: "google_discovery_source_credential_query_param", | ||
| 732 | where: [ | ||
| 733 | { field: "source_id", value: sourceId }, | ||
| 734 | { field: "scope_id", value: scope }, | ||
| 735 | ], | ||
| 736 | }); | ||
| 737 | const headers = rowsToValueMap(headerRows); | ||
| 738 | const queryParams = rowsToValueMap(paramRows); | ||
| 739 | const credentials = | ||
| 740 | Object.keys(headers).length === 0 && | ||
| 741 | Object.keys(queryParams).length === 0 | ||
| 742 | ? undefined | ||
| 743 | : { | ||
| 744 | ...(Object.keys(headers).length > 0 ? { headers } : {}), | ||
| 745 | ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), | ||
| 746 | }; | ||
| 747 | const reassembled = { | ||
| 748 | ...partial, | ||
| 749 | auth: columnsToAuth(row), | ||
| 750 | ...(credentials ? { credentials } : {}), | ||
| 751 | }; | ||
| 752 | return decodeStoredSourceData(reassembled); | ||
| 753 | }); | ||
| 754 | } | ||
| 755 | }; | ||
| 756 | |||
| 757 | // Strip auth/credentials from the encoded source-data shape. Those | ||
| 758 | // moved to columns and child tables; the remaining structural fields | ||
| 759 | // live in the `config` JSON. | ||
| 760 | const stripExtractedFields = ( | ||
| 761 | encoded: Record<string, unknown>, | ||
| 762 | ): Record<string, unknown> => { | ||
| 763 | const { auth, credentials, ...rest } = encoded; | ||
| 764 | void auth; | ||
| 765 | void credentials; | ||
| 766 | return rest; | ||
| 341 | }; | 767 | }; |
| @@ -625,4 +625,82 @@ describe("Google Discovery plugin", () => { · 624 unmodified lines | |||
| 625 | } | 625 | } |
| 626 | }), | 626 | }), |
| 627 | ); | 627 | ); |
| 628 | |||
| 629 | // ------------------------------------------------------------------------- | ||
| 630 | // Usage tracking — refs land on auth_* columns and the credential | ||
| 631 | // child tables. `usagesForSecret` / `usagesForConnection` should | ||
| 632 | // surface them all. | ||
| 633 | // ------------------------------------------------------------------------- | ||
| 634 | |||
| 635 | it.effect("usagesForSecret returns refs across auth + credential rows", () => | ||
| 636 | Effect.gen(function* () { | ||
| 637 | const handle = yield* Effect.promise(() => startServer()); | ||
| 638 | try { | ||
| 639 | const executor = yield* createExecutor( | ||
| 640 | makeTestConfig({ | ||
| 641 | plugins: [ | ||
| 642 | makeMemorySecretsPlugin()(), | ||
| 643 | googleDiscoveryPlugin(), | ||
| 644 | ] as const, | ||
| 645 | }), | ||
| 646 | ); | ||
| 647 | try { | ||
| 648 | const connectionId = ConnectionId.make( | ||
| 649 | "google-discovery-oauth2-usages", | ||
| 650 | ); | ||
| 651 | yield* executor.connections.create( | ||
| 652 | new CreateConnectionInput({ | ||
| 653 | id: connectionId, | ||
| 654 | scope: ScopeId.make("test-scope"), | ||
| 655 | provider: "oauth2", | ||
| 656 | identityLabel: "Drive Usages", | ||
| 657 | accessToken: new TokenMaterial({ | ||
| 658 | secretId: SecretId.make(`${connectionId}.access_token`), | ||
| 659 | name: "Drive Access Token", | ||
| 660 | value: "secret-token", | ||
| 661 | }), | ||
| 662 | refreshToken: null, | ||
| 663 | expiresAt: null, | ||
| 664 | oauthScope: null, | ||
| 665 | providerState: null, | ||
| 666 | }), | ||
| 667 | ); | ||
| 668 | |||
| 669 | yield* executor.googleDiscovery.addSource({ | ||
| 670 | name: "Drive (Usages)", | ||
| 671 | scope: "test-scope", | ||
| 672 | discoveryUrl: handle.discoveryUrl, | ||
| 673 | namespace: "drive_u", | ||
| 674 | auth: { | ||
| 675 | kind: "oauth2", | ||
| 676 | connectionId, | ||
| 677 | clientIdSecretId: "shared-secret", | ||
| 678 | clientSecretSecretId: null, | ||
| 679 | scopes: [], | ||
| 680 | }, | ||
| 681 | }); | ||
| 682 | |||
| 683 | // The auth.client_id_secret_id alone holds `shared-secret`. | ||
| 684 | const usages = yield* executor.secrets.usages( | ||
| 685 | SecretId.make("shared-secret"), | ||
| 686 | ); | ||
| 687 | expect(usages.length).toBe(1); | ||
| 688 | expect(usages[0]).toMatchObject({ | ||
| 689 | pluginId: "google-discovery", | ||
| 690 | ownerKind: "google-discovery-source", | ||
| 691 | ownerId: "drive_u", | ||
| 692 | slot: "auth.oauth2.client_id", | ||
| 693 | }); | ||
| 694 | |||
| 695 | const connUsages = yield* executor.connections.usages(connectionId); | ||
| 696 | expect(connUsages.length).toBe(1); | ||
| 697 | expect(connUsages[0].slot).toBe("auth.oauth2.connection"); | ||
| 698 | } finally { | ||
| 699 | yield* executor.close(); | ||
| 700 | } | ||
| 701 | } finally { | ||
| 702 | yield* Effect.promise(() => handle.close()); | ||
| 703 | } | ||
| 704 | }), | ||
| 705 | ); | ||
| 628 | }); | 706 | }); |
| @@ -1,7 +1,9 @@ | |||
| 1 | import { Effect, Option } from "effect"; | 1 | import { Effect, Option } from "effect"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | ScopeId, | ||
| 4 | SourceDetectionResult, | 5 | SourceDetectionResult, |
| 6 | Usage, | ||
| 5 | definePlugin, | 7 | definePlugin, |
| 6 | resolveSecretBackedMap, | 8 | resolveSecretBackedMap, |
| 7 | type PluginCtx, | 9 | type PluginCtx, |
| @@ -435,6 +437,71 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 427 unmodified lines | |||
| 435 | yield* typedCtx.storage.removeSource(sourceId, scope); | 437 | yield* typedCtx.storage.removeSource(sourceId, scope); |
| 436 | }), | 438 | }), |
| 437 | 439 | ||
| 440 | // Aggregate usages across the auth columns and the credential child | ||
| 441 | // tables. Each is one indexed SELECT in the store; the merge plus a | ||
| 442 | // single source-name JOIN happens here. | ||
| 443 | usagesForSecret: ({ ctx, args }) => | ||
| 444 | Effect.gen(function* () { | ||
| 445 | const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>; | ||
| 446 | const sources = yield* typedCtx.storage.findSourcesBySecret( | ||
| 447 | args.secretId, | ||
| 448 | ); | ||
| 449 | const childRows = yield* typedCtx.storage.findCredentialRowsBySecret( | ||
| 450 | args.secretId, | ||
| 451 | ); | ||
| 452 | const sourceKeys = new Set<string>(); | ||
| 453 | for (const s of sources) sourceKeys.add(`${s.scope_id}:${s.namespace}`); | ||
| 454 | for (const r of childRows) | ||
| 455 | sourceKeys.add(`${r.scope_id}:${r.source_id}`); | ||
| 456 | const names = yield* typedCtx.storage.lookupSourceNames([...sourceKeys]); | ||
| 457 | |||
| 458 | const out: Usage[] = []; | ||
| 459 | for (const s of sources) { | ||
| 460 | out.push( | ||
| 461 | new Usage({ | ||
| 462 | pluginId: "google-discovery", | ||
| 463 | scopeId: ScopeId.make(s.scope_id), | ||
| 464 | ownerKind: "google-discovery-source", | ||
| 465 | ownerId: s.namespace, | ||
| 466 | ownerName: names.get(`${s.scope_id}:${s.namespace}`) ?? s.name, | ||
| 467 | slot: s.slot, | ||
| 468 | }), | ||
| 469 | ); | ||
| 470 | } | ||
| 471 | for (const r of childRows) { | ||
| 472 | out.push( | ||
| 473 | new Usage({ | ||
| 474 | pluginId: "google-discovery", | ||
| 475 | scopeId: ScopeId.make(r.scope_id), | ||
| 476 | ownerKind: `google-discovery-source-${r.kind.replace(/_/g, "-")}`, | ||
| 477 | ownerId: r.source_id, | ||
| 478 | ownerName: names.get(`${r.scope_id}:${r.source_id}`) ?? null, | ||
| 479 | slot: `${r.kind}:${r.name}`, | ||
| 480 | }), | ||
| 481 | ); | ||
| 482 | } | ||
| 483 | return out; | ||
| 484 | }), | ||
| 485 | |||
| 486 | usagesForConnection: ({ ctx, args }) => | ||
| 487 | Effect.gen(function* () { | ||
| 488 | const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>; | ||
| 489 | const sources = yield* typedCtx.storage.findSourcesByConnection( | ||
| 490 | args.connectionId, | ||
| 491 | ); | ||
| 492 | return sources.map( | ||
| 493 | (s) => | ||
| 494 | new Usage({ | ||
| 495 | pluginId: "google-discovery", | ||
| 496 | scopeId: ScopeId.make(s.scope_id), | ||
| 497 | ownerKind: "google-discovery-source", | ||
| 498 | ownerId: s.namespace, | ||
| 499 | ownerName: s.name, | ||
| 500 | slot: s.slot, | ||
| 501 | }), | ||
| 502 | ); | ||
| 503 | }), | ||
| 504 | |||
| 438 | detect: ({ url }) => | 505 | detect: ({ url }) => |
| 439 | Effect.gen(function* () { | 506 | Effect.gen(function* () { |
| 440 | const trimmed = url.trim(); | 507 | const trimmed = url.trim(); |
| @@ -507,4 +507,127 @@ describe("graphqlPlugin", () => { · 506 unmodified lines | |||
| 507 | expect(orgView?.endpoint).toBe("http://org.example.com/graphql"); | 507 | expect(orgView?.endpoint).toBe("http://org.example.com/graphql"); |
| 508 | }), | 508 | }), |
| 509 | ); | 509 | ); |
| 510 | |||
| 511 | // ------------------------------------------------------------------------- | ||
| 512 | // Usage tracking — `usagesForSecret` and `usagesForConnection` should | ||
| 513 | // surface every reference to a secret/connection across the plugin's | ||
| 514 | // normalized child tables, and `secrets.remove` / `connections.remove` | ||
| 515 | // should refuse while a reference exists. | ||
| 516 | // ------------------------------------------------------------------------- | ||
| 517 | |||
| 518 | it.effect("usagesForSecret returns one Usage per header/query_param ref", () => | ||
| 519 | Effect.gen(function* () { | ||
| 520 | const executor = yield* createExecutor( | ||
| 521 | makeTestConfig({ | ||
| 522 | plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const, | ||
| 523 | }), | ||
| 524 | ); | ||
| 525 | |||
| 526 | yield* executor.secrets.set({ | ||
| 527 | id: SecretId.make("api-key"), | ||
| 528 | scope: ScopeId.make(TEST_SCOPE), | ||
| 529 | name: "API Key", | ||
| 530 | value: "abc123", | ||
| 531 | provider: "memory", | ||
| 532 | }); | ||
| 533 | |||
| 534 | yield* executor.graphql.addSource({ | ||
| 535 | endpoint: "http://localhost:4000/graphql", | ||
| 536 | scope: TEST_SCOPE, | ||
| 537 | introspectionJson, | ||
| 538 | namespace: "with_secret", | ||
| 539 | headers: { Authorization: { secretId: "api-key", prefix: "Bearer " } }, | ||
| 540 | queryParams: { token: { secretId: "api-key" } }, | ||
| 541 | }); | ||
| 542 | |||
| 543 | const usages = yield* executor.secrets.usages(SecretId.make("api-key")); | ||
| 544 | // Two refs: one header, one query param. | ||
| 545 | expect(usages.length).toBe(2); | ||
| 546 | const slots = usages.map((u) => u.slot).sort(); | ||
| 547 | expect(slots).toEqual(["header:Authorization", "query_param:token"]); | ||
| 548 | expect(usages.every((u) => u.pluginId === "graphql")).toBe(true); | ||
| 549 | expect(usages.every((u) => u.ownerId === "with_secret")).toBe(true); | ||
| 550 | }), | ||
| 551 | ); | ||
| 552 | |||
| 553 | it.effect("secrets.remove refuses while a graphql source still uses it", () => | ||
| 554 | Effect.gen(function* () { | ||
| 555 | const executor = yield* createExecutor( | ||
| 556 | makeTestConfig({ | ||
| 557 | plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const, | ||
| 558 | }), | ||
| 559 | ); | ||
| 560 | |||
| 561 | yield* executor.secrets.set({ | ||
| 562 | id: SecretId.make("locked"), | ||
| 563 | scope: ScopeId.make(TEST_SCOPE), | ||
| 564 | name: "Locked", | ||
| 565 | value: "v", | ||
| 566 | provider: "memory", | ||
| 567 | }); | ||
| 568 | |||
| 569 | yield* executor.graphql.addSource({ | ||
| 570 | endpoint: "http://localhost:4000/graphql", | ||
| 571 | scope: TEST_SCOPE, | ||
| 572 | introspectionJson, | ||
| 573 | namespace: "ref", | ||
| 574 | headers: { "X-Token": { secretId: "locked" } }, | ||
| 575 | }); | ||
| 576 | |||
| 577 | const result = yield* executor.secrets.remove(SecretId.make("locked")).pipe( | ||
| 578 | Effect.flip, | ||
| 579 | ); | ||
| 580 | expect((result as { _tag: string })._tag).toBe("SecretInUseError"); | ||
| 581 | |||
| 582 | // After detaching the source, remove succeeds. | ||
| 583 | yield* executor.graphql.removeSource("ref", TEST_SCOPE); | ||
| 584 | yield* executor.secrets.remove(SecretId.make("locked")); | ||
| 585 | }), | ||
| 586 | ); | ||
| 587 | |||
| 588 | it.effect("usagesForConnection returns one Usage per source", () => | ||
| 589 | Effect.gen(function* () { | ||
| 590 | const executor = yield* createExecutor( | ||
| 591 | makeTestConfig({ | ||
| 592 | plugins: [makeMemorySecretsPlugin()(), graphqlPlugin()] as const, | ||
| 593 | }), | ||
| 594 | ); | ||
| 595 | |||
| 596 | const connectionId = ConnectionId.make("graphql-conn"); | ||
| 597 | yield* executor.connections.create( | ||
| 598 | new CreateConnectionInput({ | ||
| 599 | id: connectionId, | ||
| 600 | scope: ScopeId.make(TEST_SCOPE), | ||
| 601 | provider: "oauth2", | ||
| 602 | identityLabel: "Conn", | ||
| 603 | accessToken: new TokenMaterial({ | ||
| 604 | secretId: SecretId.make(`${connectionId}.access_token`), | ||
| 605 | name: "Access Token", | ||
| 606 | value: "tok", | ||
| 607 | }), | ||
| 608 | refreshToken: null, | ||
| 609 | expiresAt: null, | ||
| 610 | oauthScope: null, | ||
| 611 | providerState: null, | ||
| 612 | }), | ||
| 613 | ); | ||
| 614 | |||
| 615 | yield* executor.graphql.addSource({ | ||
| 616 | endpoint: "http://localhost:4000/graphql", | ||
| 617 | scope: TEST_SCOPE, | ||
| 618 | introspectionJson, | ||
| 619 | namespace: "oauth_ref", | ||
| 620 | auth: { kind: "oauth2", connectionId }, | ||
| 621 | }); | ||
| 622 | |||
| 623 | const usages = yield* executor.connections.usages(connectionId); | ||
| 624 | expect(usages.length).toBe(1); | ||
| 625 | expect(usages[0]).toMatchObject({ | ||
| 626 | pluginId: "graphql", | ||
| 627 | ownerKind: "graphql-source-auth", | ||
| 628 | ownerId: "oauth_ref", | ||
| 629 | slot: "auth.oauth2", | ||
| 630 | }); | ||
| 631 | }), | ||
| 632 | ); | ||
| 510 | }); | 633 | }); |
| @@ -7,7 +7,9 @@ import { GraphqlExtensionService, GraphqlHandlers } from "../api/handlers"; · 6 unmodified lines | |||
| 7 | 7 | ||
| 8 | import { | 8 | import { |
| 9 | definePlugin, | 9 | definePlugin, |
| 10 | ScopeId, | ||
| 10 | SourceDetectionResult, | 11 | SourceDetectionResult, |
| 12 | Usage, | ||
| 11 | type StorageFailure, | 13 | type StorageFailure, |
| 12 | type ToolAnnotations, | 14 | type ToolAnnotations, |
| 13 | type ToolRow, | 15 | type ToolRow, |
| @@ -632,6 +634,82 @@ export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => { · 618 unmodified lines | |||
| 632 | removeSource: ({ ctx, sourceId, scope }) => | 634 | removeSource: ({ ctx, sourceId, scope }) => |
| 633 | ctx.storage.removeSource(sourceId, scope), | 635 | ctx.storage.removeSource(sourceId, scope), |
| 634 | 636 | ||
| 637 | // Look up every place this secret appears across the plugin's two | ||
| 638 | // child tables (`graphql_source_header`, `graphql_source_query_param`). | ||
| 639 | // The store runs behind the scoped adapter so reads automatically | ||
| 640 | // walk the executor's scope stack — no scope arg needed. | ||
| 641 | usagesForSecret: ({ ctx, args }) => | ||
| 642 | Effect.gen(function* () { | ||
| 643 | // Adapter access via the underlying typed view on the store deps. | ||
| 644 | // We thread it through `ctx.storage` rather than re-grabbing it | ||
| 645 | // because the store already owns the typed adapter handle; expose | ||
| 646 | // a single helper rather than re-implementing the where/joins. | ||
| 647 | const headerRows = yield* ctx.storage.findHeaderRowsBySecret( | ||
| 648 | args.secretId, | ||
| 649 | ); | ||
| 650 | const paramRows = yield* ctx.storage.findQueryParamRowsBySecret( | ||
| 651 | args.secretId, | ||
| 652 | ); | ||
| 653 | |||
| 654 | // Resolve owner names by joining to graphql_source. We batch the | ||
| 655 | // distinct (source_id, scope_id) pairs to one findMany rather | ||
| 656 | // than N+1 lookups. | ||
| 657 | const sourceKeys = new Set<string>(); | ||
| 658 | for (const r of [...headerRows, ...paramRows]) { | ||
| 659 | sourceKeys.add(`${r.scope_id}:${r.source_id}`); | ||
| 660 | } | ||
| 661 | const sources = yield* ctx.storage.lookupSourceNames([...sourceKeys]); | ||
| 662 | |||
| 663 | const out: Usage[] = []; | ||
| 664 | for (const r of headerRows) { | ||
| 665 | out.push( | ||
| 666 | new Usage({ | ||
| 667 | pluginId: "graphql", | ||
| 668 | scopeId: ScopeId.make(r.scope_id), | ||
| 669 | ownerKind: "graphql-source-header", | ||
| 670 | ownerId: r.source_id, | ||
| 671 | ownerName: | ||
| 672 | sources.get(`${r.scope_id}:${r.source_id}`) ?? null, | ||
| 673 | slot: `header:${r.name}`, | ||
| 674 | }), | ||
| 675 | ); | ||
| 676 | } | ||
| 677 | for (const r of paramRows) { | ||
| 678 | out.push( | ||
| 679 | new Usage({ | ||
| 680 | pluginId: "graphql", | ||
| 681 | scopeId: ScopeId.make(r.scope_id), | ||
| 682 | ownerKind: "graphql-source-query-param", | ||
| 683 | ownerId: r.source_id, | ||
| 684 | ownerName: | ||
| 685 | sources.get(`${r.scope_id}:${r.source_id}`) ?? null, | ||
| 686 | slot: `query_param:${r.name}`, | ||
| 687 | }), | ||
| 688 | ); | ||
| 689 | } | ||
| 690 | return out; | ||
| 691 | }), | ||
| 692 | |||
| 693 | usagesForConnection: ({ ctx, args }) => | ||
| 694 | Effect.gen(function* () { | ||
| 695 | // OAuth refs only appear in graphql_source.auth_connection_id — | ||
| 696 | // one indexed lookup. No child tables to scan. | ||
| 697 | const sources = yield* ctx.storage.findSourcesByConnection( | ||
| 698 | args.connectionId, | ||
| 699 | ); | ||
| 700 | return sources.map( | ||
| 701 | (s) => | ||
| 702 | new Usage({ | ||
| 703 | pluginId: "graphql", | ||
| 704 | scopeId: ScopeId.make(s.scope), | ||
| 705 | ownerKind: "graphql-source-auth", | ||
| 706 | ownerId: s.namespace, | ||
| 707 | ownerName: s.name, | ||
| 708 | slot: "auth.oauth2", | ||
| 709 | }), | ||
| 710 | ); | ||
| 711 | }), | ||
| 712 | |||
| 635 | detect: ({ url }) => | 713 | detect: ({ url }) => |
| 636 | Effect.gen(function* () { | 714 | Effect.gen(function* () { |
| 637 | const trimmed = url.trim(); | 715 | const trimmed = url.trim(); |
| @@ -14,9 +14,20 @@ import { · 13 unmodified lines | |||
| 14 | } from "./types"; | 14 | } from "./types"; |
| 15 | 15 | ||
| 16 | // --------------------------------------------------------------------------- | 16 | // --------------------------------------------------------------------------- |
| 17 | // Schema — two tables: | 17 | // Schema — four tables: |
| 18 | // - graphql_source: endpoint + headers + display name per source | 18 | // - graphql_source: endpoint + auth + display name per source. Auth is |
| 19 | // - graphql_operation: per-tool OperationBinding blob keyed by tool id | 19 | // flattened (kind enum + nullable connection_id) so the |
| 20 | // `usagesForConnection` query is one indexed SELECT. | ||
| 21 | // - graphql_source_header / graphql_source_query_param: one row per | ||
| 22 | // header/param entry. `kind` discriminates literal text from a | ||
| 23 | // secret reference; `secret_id` is indexed so `usagesForSecret` | ||
| 24 | // reads the index directly. PK is `(scope_id, id)` where id is a | ||
| 25 | // JSON tuple `[source_id,name]` so user-provided separators cannot | ||
| 26 | // collide. | ||
| 27 | // - graphql_operation: per-tool OperationBinding blob. Operation | ||
| 28 | // bindings don't reference secrets/connections, so they stay as | ||
| 29 | // JSON — that's a legit JSON case (the binding shape is plugin- | ||
| 30 | // internal opaque data). | ||
| 20 | // --------------------------------------------------------------------------- | 31 | // --------------------------------------------------------------------------- |
| 21 | 32 | ||
| 22 | export const graphqlSchema = defineSchema({ | 33 | export const graphqlSchema = defineSchema({ |
| @@ -26,9 +37,46 @@ export const graphqlSchema = defineSchema({ · 3 unmodified lines | |||
| 26 | scope_id: { type: "string", required: true, index: true }, | 37 | scope_id: { type: "string", required: true, index: true }, |
| 27 | name: { type: "string", required: true }, | 38 | name: { type: "string", required: true }, |
| 28 | endpoint: { type: "string", required: true }, | 39 | endpoint: { type: "string", required: true }, |
| 29 | headers: { type: "json", required: false }, | 40 | auth_kind: { |
| 30 | query_params: { type: "json", required: false }, | 41 | type: ["none", "oauth2"], |
| 31 | auth: { type: "json", required: false }, | 42 | required: true, |
| 43 | defaultValue: "none", | ||
| 44 | }, | ||
| 45 | auth_connection_id: { | ||
| 46 | type: "string", | ||
| 47 | required: false, | ||
| 48 | index: true, | ||
| 49 | }, | ||
| 50 | }, | ||
| 51 | }, | ||
| 52 | graphql_source_header: { | ||
| 53 | fields: { | ||
| 54 | id: { type: "string", required: true }, | ||
| 55 | scope_id: { type: "string", required: true, index: true }, | ||
| 56 | source_id: { type: "string", required: true, index: true }, | ||
| 57 | name: { type: "string", required: true }, | ||
| 58 | kind: { | ||
| 59 | type: ["text", "secret"], | ||
| 60 | required: true, | ||
| 61 | }, | ||
| 62 | text_value: { type: "string", required: false }, | ||
| 63 | secret_id: { type: "string", required: false, index: true }, | ||
| 64 | secret_prefix: { type: "string", required: false }, | ||
| 65 | }, | ||
| 66 | }, | ||
| 67 | graphql_source_query_param: { | ||
| 68 | fields: { | ||
| 69 | id: { type: "string", required: true }, | ||
| 70 | scope_id: { type: "string", required: true, index: true }, | ||
| 71 | source_id: { type: "string", required: true, index: true }, | ||
| 72 | name: { type: "string", required: true }, | ||
| 73 | kind: { | ||
| 74 | type: ["text", "secret"], | ||
| 75 | required: true, | ||
| 76 | }, | ||
| 77 | text_value: { type: "string", required: false }, | ||
| 78 | secret_id: { type: "string", required: false, index: true }, | ||
| 79 | secret_prefix: { type: "string", required: false }, | ||
| 32 | }, | 80 | }, |
| 33 | }, | 81 | }, |
| 34 | graphql_operation: { | 82 | graphql_operation: { |
| @@ -98,29 +146,69 @@ const encodeBinding = (binding: OperationBinding): BindingJson => ({ · 63 unmodified lines | |||
| 98 | const toJsonRecord = (value: unknown): Record<string, unknown> => | 146 | const toJsonRecord = (value: unknown): Record<string, unknown> => |
| 99 | value as Record<string, unknown>; | 147 | value as Record<string, unknown>; |
| 100 | 148 | ||
| 101 | const decodeHeaders = (value: unknown): Record<string, HeaderValue> => { | 149 | // Header / query-param rows: collapse the flat columns back into a |
| 102 | if (value == null) return {}; | 150 | // `SecretBackedValue` map keyed by header name. `kind` discriminates the |
| 103 | if (typeof value === "string") | 151 | // shape; `secret_prefix` is optional and only populated when present in |
| 104 | return JSON.parse(value) as Record<string, HeaderValue>; | 152 | // the original config. |
| 105 | return value as Record<string, HeaderValue>; | 153 | const rowsToValueMap = ( |
| 154 | rows: readonly Record<string, unknown>[], | ||
| 155 | ): Record<string, HeaderValue> => { | ||
| 156 | const out: Record<string, HeaderValue> = {}; | ||
| 157 | for (const row of rows) { | ||
| 158 | const name = row.name as string; | ||
| 159 | if (row.kind === "secret" && typeof row.secret_id === "string") { | ||
| 160 | const prefix = row.secret_prefix as string | undefined | null; | ||
| 161 | out[name] = prefix | ||
| 162 | ? { secretId: row.secret_id, prefix } | ||
| 163 | : { secretId: row.secret_id }; | ||
| 164 | } else if (row.kind === "text" && typeof row.text_value === "string") { | ||
| 165 | out[name] = row.text_value; | ||
| 166 | } | ||
| 167 | } | ||
| 168 | return out; | ||
| 106 | }; | 169 | }; |
| 107 | 170 | ||
| 108 | const decodeQueryParams = (value: unknown): Record<string, QueryParamValue> => { | 171 | // Encode one entry of a SecretBackedValue map into a child row. Used by |
| 109 | if (value == null) return {}; | 172 | // the writer for both `graphql_source_header` and |
| 110 | if (typeof value === "string") | 173 | // `graphql_source_query_param`. Returns a `Record<string, unknown>` so |
| 111 | return JSON.parse(value) as Record<string, QueryParamValue>; | 174 | // the result is structurally assignable to the typed adapter's |
| 112 | return value as Record<string, QueryParamValue>; | 175 | // `RowInput` shape (which has its own index signature). |
| 176 | const valueToChildRow = ( | ||
| 177 | sourceId: string, | ||
| 178 | scope: string, | ||
| 179 | name: string, | ||
| 180 | value: HeaderValue, | ||
| 181 | ): Record<string, unknown> => { | ||
| 182 | const id = JSON.stringify([sourceId, name]); | ||
| 183 | if (typeof value === "string") { | ||
| 184 | return { | ||
| 185 | id, | ||
| 186 | scope_id: scope, | ||
| 187 | source_id: sourceId, | ||
| 188 | name, | ||
| 189 | kind: "text", | ||
| 190 | text_value: value, | ||
| 191 | }; | ||
| 192 | } | ||
| 193 | return { | ||
| 194 | id, | ||
| 195 | scope_id: scope, | ||
| 196 | source_id: sourceId, | ||
| 197 | name, | ||
| 198 | kind: "secret", | ||
| 199 | secret_id: value.secretId, | ||
| 200 | secret_prefix: value.prefix, | ||
| 201 | }; | ||
| 113 | }; | 202 | }; |
| 114 | 203 | ||
| 115 | const decodeAuth = (value: unknown): GraphqlSourceAuth => { | 204 | const rowToAuth = (row: Record<string, unknown>): GraphqlSourceAuth => { |
| 116 | if (value == null) return { kind: "none" }; | 205 | if ( |
| 117 | const parsed = | 206 | row.auth_kind === "oauth2" && |
| 118 | typeof value === "string" | 207 | typeof row.auth_connection_id === "string" |
| 119 | ? (JSON.parse(value) as GraphqlSourceAuth) | 208 | ) { |
| 120 | : (value as GraphqlSourceAuth); | 209 | return { kind: "oauth2", connectionId: row.auth_connection_id }; |
| 121 | return parsed?.kind === "oauth2" && typeof parsed.connectionId === "string" | 210 | } |
| 122 | ? { kind: "oauth2", connectionId: parsed.connectionId } | 211 | return { kind: "none" }; |
| 123 | : { kind: "none" }; | ||
| 124 | }; | 212 | }; |
| 125 | 213 | ||
| 126 | // --------------------------------------------------------------------------- | 214 | // --------------------------------------------------------------------------- |
| @@ -138,6 +226,15 @@ const decodeAuth = (value: unknown): GraphqlSourceAuth => { · 11 unmodified lines | |||
| 138 | // `path.scopeId` for HTTP, `toolRow.scope_id` / `input.scope` for | 226 | // `path.scopeId` for HTTP, `toolRow.scope_id` / `input.scope` for |
| 139 | // invokeTool/lifecycle) so every keyed mutation targets exactly one | 227 | // invokeTool/lifecycle) so every keyed mutation targets exactly one |
| 140 | // row. | 228 | // row. |
| 229 | /** Flat row shape returned by the usage-lookup helpers. Mirrors the new | ||
| 230 | * child-table columns so callers can build a `Usage` without | ||
| 231 | * re-decoding. */ | ||
| 232 | export interface ChildUsageRow { | ||
| 233 | readonly source_id: string; | ||
| 234 | readonly scope_id: string; | ||
| 235 | readonly name: string; | ||
| 236 | } | ||
| 237 | |||
| 141 | export interface GraphqlStore { | 238 | export interface GraphqlStore { |
| 142 | readonly upsertSource: ( | 239 | readonly upsertSource: ( |
| 143 | input: StoredGraphqlSource, | 240 | input: StoredGraphqlSource, |
| @@ -180,6 +277,34 @@ export interface GraphqlStore { · 36 unmodified lines | |||
| 180 | namespace: string, | 277 | namespace: string, |
| 181 | scope: string, | 278 | scope: string, |
| 182 | ) => Effect.Effect<void, StorageFailure>; | 279 | ) => Effect.Effect<void, StorageFailure>; |
| 280 | |||
| 281 | // --------------------------------------------------------------------- | ||
| 282 | // Usage lookups — power `usagesForSecret` / `usagesForConnection`. | ||
| 283 | // Each is one indexed SELECT against the new normalized columns. | ||
| 284 | // --------------------------------------------------------------------- | ||
| 285 | |||
| 286 | /** Header rows that reference the given secret id, across every scope | ||
| 287 | * visible to the executor. */ | ||
| 288 | readonly findHeaderRowsBySecret: ( | ||
| 289 | secretId: string, | ||
| 290 | ) => Effect.Effect<readonly ChildUsageRow[], StorageFailure>; | ||
| 291 | |||
| 292 | /** Query-param rows that reference the given secret id. */ | ||
| 293 | readonly findQueryParamRowsBySecret: ( | ||
| 294 | secretId: string, | ||
| 295 | ) => Effect.Effect<readonly ChildUsageRow[], StorageFailure>; | ||
| 296 | |||
| 297 | /** Source rows whose oauth2 auth points at the given connection id. */ | ||
| 298 | readonly findSourcesByConnection: ( | ||
| 299 | connectionId: string, | ||
| 300 | ) => Effect.Effect<readonly StoredGraphqlSource[], StorageFailure>; | ||
| 301 | |||
| 302 | /** Resolve the display name for one or more `(scope_id, source_id)` | ||
| 303 | * pairs in a single round trip. Returned map is keyed by | ||
| 304 | * `${scope_id}:${source_id}`; missing entries are simply absent. */ | ||
| 305 | readonly lookupSourceNames: ( | ||
| 306 | keys: readonly string[], | ||
| 307 | ) => Effect.Effect<ReadonlyMap<string, string>, StorageFailure>; | ||
| 183 | } | 308 | } |
| 184 | 309 | ||
| 185 | // --------------------------------------------------------------------------- | 310 | // --------------------------------------------------------------------------- |
| @@ -189,15 +314,46 @@ export interface GraphqlStore { · 3 unmodified lines | |||
| 189 | export const makeDefaultGraphqlStore = ({ | 314 | export const makeDefaultGraphqlStore = ({ |
| 190 | adapter: db, | 315 | adapter: db, |
| 191 | }: StorageDeps<GraphqlSchema>): GraphqlStore => { | 316 | }: StorageDeps<GraphqlSchema>): GraphqlStore => { |
| 192 | const rowToSource = (row: Record<string, unknown>): StoredGraphqlSource => ({ | 317 | const loadHeaders = (sourceId: string, scope: string) => |
| 193 | namespace: row.id as string, | 318 | db |
| 194 | scope: row.scope_id as string, | 319 | .findMany({ |
| 195 | name: row.name as string, | 320 | model: "graphql_source_header", |
| 196 | endpoint: row.endpoint as string, | 321 | where: [ |
| 197 | headers: decodeHeaders(row.headers), | 322 | { field: "source_id", value: sourceId }, |
| 198 | queryParams: decodeQueryParams(row.query_params), | 323 | { field: "scope_id", value: scope }, |
| 199 | auth: decodeAuth(row.auth), | 324 | ], |
| 200 | }); | 325 | }) |
| 326 | .pipe(Effect.map(rowsToValueMap)); | ||
| 327 | |||
| 328 | const loadQueryParams = (sourceId: string, scope: string) => | ||
| 329 | db | ||
| 330 | .findMany({ | ||
| 331 | model: "graphql_source_query_param", | ||
| 332 | where: [ | ||
| 333 | { field: "source_id", value: sourceId }, | ||
| 334 | { field: "scope_id", value: scope }, | ||
| 335 | ], | ||
| 336 | }) | ||
| 337 | .pipe(Effect.map(rowsToValueMap)); | ||
| 338 | |||
| 339 | const rowToSourceWithChildren = ( | ||
| 340 | row: Record<string, unknown>, | ||
| 341 | ): Effect.Effect<StoredGraphqlSource, StorageFailure> => | ||
| 342 | Effect.gen(function* () { | ||
| 343 | const sourceId = row.id as string; | ||
| 344 | const scope = row.scope_id as string; | ||
| 345 | const headers = yield* loadHeaders(sourceId, scope); | ||
| 346 | const queryParams = yield* loadQueryParams(sourceId, scope); | ||
| 347 | return { | ||
| 348 | namespace: sourceId, | ||
| 349 | scope, | ||
| 350 | name: row.name as string, | ||
| 351 | endpoint: row.endpoint as string, | ||
| 352 | headers, | ||
| 353 | queryParams, | ||
| 354 | auth: rowToAuth(row), | ||
| 355 | }; | ||
| 356 | }); | ||
| 201 | 357 | ||
| 202 | const rowToOperation = (row: Record<string, unknown>): StoredOperation => ({ | 358 | const rowToOperation = (row: Record<string, unknown>): StoredOperation => ({ |
| 203 | toolId: row.id as string, | 359 | toolId: row.id as string, |
| @@ -205,6 +361,34 @@ export const makeDefaultGraphqlStore = ({ · 1 unmodified line | |||
| 205 | binding: decodeBinding(row.binding), | 361 | binding: decodeBinding(row.binding), |
| 206 | }); | 362 | }); |
| 207 | 363 | ||
| 364 | // Replace child rows for a source by deleting then bulk-inserting. Used | ||
| 365 | // by both upsertSource (full rewrite) and updateSourceMeta (partial | ||
| 366 | // patch when headers/queryParams is supplied). | ||
| 367 | const replaceChildren = ( | ||
| 368 | model: "graphql_source_header" | "graphql_source_query_param", | ||
| 369 | sourceId: string, | ||
| 370 | scope: string, | ||
| 371 | values: Record<string, HeaderValue>, | ||
| 372 | ) => | ||
| 373 | Effect.gen(function* () { | ||
| 374 | yield* db.deleteMany({ | ||
| 375 | model, | ||
| 376 | where: [ | ||
| 377 | { field: "source_id", value: sourceId }, | ||
| 378 | { field: "scope_id", value: scope }, | ||
| 379 | ], | ||
| 380 | }); | ||
| 381 | const entries = Object.entries(values); | ||
| 382 | if (entries.length === 0) return; | ||
| 383 | yield* db.createMany({ | ||
| 384 | model, | ||
| 385 | data: entries.map(([name, value]) => | ||
| 386 | valueToChildRow(sourceId, scope, name, value), | ||
| 387 | ), | ||
| 388 | forceAllowId: true, | ||
| 389 | }); | ||
| 390 | }); | ||
| 391 | |||
| 208 | const deleteSource = (namespace: string, scope: string) => | 392 | const deleteSource = (namespace: string, scope: string) => |
| 209 | Effect.gen(function* () { | 393 | Effect.gen(function* () { |
| 210 | yield* db.deleteMany({ | 394 | yield* db.deleteMany({ |
| @@ -214,6 +398,20 @@ export const makeDefaultGraphqlStore = ({ · 3 unmodified lines | |||
| 214 | { field: "scope_id", value: scope }, | 398 | { field: "scope_id", value: scope }, |
| 215 | ], | 399 | ], |
| 216 | }); | 400 | }); |
| 401 | yield* db.deleteMany({ | ||
| 402 | model: "graphql_source_header", | ||
| 403 | where: [ | ||
| 404 | { field: "source_id", value: namespace }, | ||
| 405 | { field: "scope_id", value: scope }, | ||
| 406 | ], | ||
| 407 | }); | ||
| 408 | yield* db.deleteMany({ | ||
| 409 | model: "graphql_source_query_param", | ||
| 410 | where: [ | ||
| 411 | { field: "source_id", value: namespace }, | ||
| 412 | { field: "scope_id", value: scope }, | ||
| 413 | ], | ||
| 414 | }); | ||
| 217 | yield* db.delete({ | 415 | yield* db.delete({ |
| 218 | model: "graphql_source", | 416 | model: "graphql_source", |
| 219 | where: [ | 417 | where: [ |
| @@ -234,12 +432,24 @@ export const makeDefaultGraphqlStore = ({ · 14 unmodified lines | |||
| 234 | scope_id: input.scope, | 432 | scope_id: input.scope, |
| 235 | name: input.name, | 433 | name: input.name, |
| 236 | endpoint: input.endpoint, | 434 | endpoint: input.endpoint, |
| 237 | headers: input.headers, | 435 | auth_kind: input.auth.kind, |
| 238 | query_params: input.queryParams, | 436 | auth_connection_id: |
| 239 | auth: toJsonRecord(input.auth), | 437 | input.auth.kind === "oauth2" ? input.auth.connectionId : undefined, |
| 240 | }, | 438 | }, |
| 241 | forceAllowId: true, | 439 | forceAllowId: true, |
| 242 | }); | 440 | }); |
| 441 | yield* replaceChildren( | ||
| 442 | "graphql_source_header", | ||
| 443 | input.namespace, | ||
| 444 | input.scope, | ||
| 445 | input.headers, | ||
| 446 | ); | ||
| 447 | yield* replaceChildren( | ||
| 448 | "graphql_source_query_param", | ||
| 449 | input.namespace, | ||
| 450 | input.scope, | ||
| 451 | input.queryParams, | ||
| 452 | ); | ||
| 243 | if (operations.length > 0) { | 453 | if (operations.length > 0) { |
| 244 | yield* db.createMany({ | 454 | yield* db.createMany({ |
| 245 | model: "graphql_operation", | 455 | model: "graphql_operation", |
| @@ -267,41 +477,59 @@ export const makeDefaultGraphqlStore = ({ · 21 unmodified lines | |||
| 267 | const update: Record<string, unknown> = {}; | 477 | const update: Record<string, unknown> = {}; |
| 268 | if (patch.name !== undefined) update.name = patch.name; | 478 | if (patch.name !== undefined) update.name = patch.name; |
| 269 | if (patch.endpoint !== undefined) update.endpoint = patch.endpoint; | 479 | if (patch.endpoint !== undefined) update.endpoint = patch.endpoint; |
| 480 | if (patch.auth !== undefined) { | ||
| 481 | update.auth_kind = patch.auth.kind; | ||
| 482 | update.auth_connection_id = | ||
| 483 | patch.auth.kind === "oauth2" ? patch.auth.connectionId : null; | ||
| 484 | } | ||
| 485 | if (Object.keys(update).length > 0) { | ||
| 486 | yield* db.update({ | ||
| 487 | model: "graphql_source", | ||
| 488 | where: [ | ||
| 489 | { field: "id", value: namespace }, | ||
| 490 | { field: "scope_id", value: scope }, | ||
| 491 | ], | ||
| 492 | update, | ||
| 493 | }); | ||
| 494 | } | ||
| 270 | if (patch.headers !== undefined) { | 495 | if (patch.headers !== undefined) { |
| 271 | update.headers = patch.headers; | 496 | yield* replaceChildren( |
| 497 | "graphql_source_header", | ||
| 498 | namespace, | ||
| 499 | scope, | ||
| 500 | patch.headers, | ||
| 501 | ); | ||
| 272 | } | 502 | } |
| 273 | if (patch.queryParams !== undefined) { | 503 | if (patch.queryParams !== undefined) { |
| 274 | update.query_params = patch.queryParams; | 504 | yield* replaceChildren( |
| 505 | "graphql_source_query_param", | ||
| 506 | namespace, | ||
| 507 | scope, | ||
| 508 | patch.queryParams, | ||
| 509 | ); | ||
| 275 | } | 510 | } |
| 276 | if (patch.auth !== undefined) { | ||
| 277 | update.auth = toJsonRecord(patch.auth); | ||
| 278 | } | ||
| 279 | if (Object.keys(update).length === 0) return; | ||
| 280 | yield* db.update({ | ||
| 281 | model: "graphql_source", | ||
| 282 | where: [ | ||
| 283 | { field: "id", value: namespace }, | ||
| 284 | { field: "scope_id", value: scope }, | ||
| 285 | ], | ||
| 286 | update, | ||
| 287 | }); | ||
| 288 | }), | 511 | }), |
| 289 | 512 | ||
| 290 | getSource: (namespace, scope) => | 513 | getSource: (namespace, scope) => |
| 291 | db | 514 | Effect.gen(function* () { |
| 292 | .findOne({ | 515 | const row = yield* db.findOne({ |
| 293 | model: "graphql_source", | 516 | model: "graphql_source", |
| 294 | where: [ | 517 | where: [ |
| 295 | { field: "id", value: namespace }, | 518 | { field: "id", value: namespace }, |
| 296 | { field: "scope_id", value: scope }, | 519 | { field: "scope_id", value: scope }, |
| 297 | ], | 520 | ], |
| 298 | }) | 521 | }); |
| 299 | .pipe(Effect.map((row) => (row ? rowToSource(row) : null))), | 522 | if (!row) return null; |
| 523 | return yield* rowToSourceWithChildren(row); | ||
| 524 | }), | ||
| 300 | 525 | ||
| 301 | listSources: () => | 526 | listSources: () => |
| 302 | db | 527 | Effect.gen(function* () { |
| 303 | .findMany({ model: "graphql_source" }) | 528 | const rows = yield* db.findMany({ model: "graphql_source" }); |
| 304 | .pipe(Effect.map((rows) => rows.map(rowToSource))), | 529 | return yield* Effect.forEach(rows, rowToSourceWithChildren, { |
| 530 | concurrency: "unbounded", | ||
| 531 | }); | ||
| 532 | }), | ||
| 305 | 533 | ||
| 306 | getOperationByToolId: (toolId, scope) => | 534 | getOperationByToolId: (toolId, scope) => |
| 307 | db | 535 | db |
| @@ -326,5 +554,83 @@ export const makeDefaultGraphqlStore = ({ · 18 unmodified lines | |||
| 326 | .pipe(Effect.map((rows) => rows.map(rowToOperation))), | 554 | .pipe(Effect.map((rows) => rows.map(rowToOperation))), |
| 327 | 555 | ||
| 328 | removeSource: (namespace, scope) => deleteSource(namespace, scope), | 556 | removeSource: (namespace, scope) => deleteSource(namespace, scope), |
| 557 | |||
| 558 | findHeaderRowsBySecret: (secretId) => | ||
| 559 | db | ||
| 560 | .findMany({ | ||
| 561 | model: "graphql_source_header", | ||
| 562 | where: [{ field: "secret_id", value: secretId }], | ||
| 563 | }) | ||
| 564 | .pipe( | ||
| 565 | Effect.map((rows) => | ||
| 566 | rows.map( | ||
| 567 | (r): ChildUsageRow => ({ | ||
| 568 | source_id: r.source_id as string, | ||
| 569 | scope_id: r.scope_id as string, | ||
| 570 | name: r.name as string, | ||
| 571 | }), | ||
| 572 | ), | ||
| 573 | ), | ||
| 574 | ), | ||
| 575 | |||
| 576 | findQueryParamRowsBySecret: (secretId) => | ||
| 577 | db | ||
| 578 | .findMany({ | ||
| 579 | model: "graphql_source_query_param", | ||
| 580 | where: [{ field: "secret_id", value: secretId }], | ||
| 581 | }) | ||
| 582 | .pipe( | ||
| 583 | Effect.map((rows) => | ||
| 584 | rows.map( | ||
| 585 | (r): ChildUsageRow => ({ | ||
| 586 | source_id: r.source_id as string, | ||
| 587 | scope_id: r.scope_id as string, | ||
| 588 | name: r.name as string, | ||
| 589 | }), | ||
| 590 | ), | ||
| 591 | ), | ||
| 592 | ), | ||
| 593 | |||
| 594 | findSourcesByConnection: (connectionId) => | ||
| 595 | Effect.gen(function* () { | ||
| 596 | const rows = yield* db.findMany({ | ||
| 597 | model: "graphql_source", | ||
| 598 | where: [ | ||
| 599 | { field: "auth_connection_id", value: connectionId }, | ||
| 600 | ], | ||
| 601 | }); | ||
| 602 | // Skip the children load — usage callers only need the parent | ||
| 603 | // row's name + scope. Synthesize a minimal StoredGraphqlSource | ||
| 604 | // shape with empty headers/params so the type matches without | ||
| 605 | // a wasted child fetch. | ||
| 606 | return rows.map( | ||
| 607 | (row): StoredGraphqlSource => ({ | ||
| 608 | namespace: row.id as string, | ||
| 609 | scope: row.scope_id as string, | ||
| 610 | name: row.name as string, | ||
| 611 | endpoint: row.endpoint as string, | ||
| 612 | headers: {}, | ||
| 613 | queryParams: {}, | ||
| 614 | auth: rowToAuth(row), | ||
| 615 | }), | ||
| 616 | ); | ||
| 617 | }), | ||
| 618 | |||
| 619 | lookupSourceNames: (keys) => | ||
| 620 | Effect.gen(function* () { | ||
| 621 | if (keys.length === 0) return new Map<string, string>(); | ||
| 622 | // Pull every source the executor's scope walk surfaces, then | ||
| 623 | // index by composite key. Cheaper than per-key findOne and the | ||
| 624 | // graphql source table is small in practice (one row per | ||
| 625 | // endpoint). | ||
| 626 | const rows = yield* db.findMany({ model: "graphql_source" }); | ||
| 627 | const requested = new Set(keys); | ||
| 628 | const out = new Map<string, string>(); | ||
| 629 | for (const r of rows) { | ||
| 630 | const key = `${r.scope_id as string}:${r.id as string}`; | ||
| 631 | if (requested.has(key)) out.set(key, r.name as string); | ||
| 632 | } | ||
| 633 | return out; | ||
| 634 | }), | ||
| 329 | }; | 635 | }; |
| 330 | }; | 636 | }; |
| @@ -1,7 +1,19 @@ | |||
| 1 | // --------------------------------------------------------------------------- | 1 | // --------------------------------------------------------------------------- |
| 2 | // MCP plugin storage — two tables (mcp_source, mcp_binding). OAuth | 2 | // MCP plugin storage — four tables: |
| 3 | // session storage lives at the core level in `oauth2_session` and is | 3 | // - mcp_source: per-source structural data (transport, endpoint, |
| 4 | // owned by `ctx.oauth`. | 4 | // stdio command/args/env, etc.) plus the auth flattened into |
| 5 | // columns so secret/connection refs are queryable. The non-ref | ||
| 6 | // structural data still lives in `config` as JSON because it's | ||
| 7 | // plugin-private and varies by transport (`remote` vs `stdio` | ||
| 8 | // have different shapes). | ||
| 9 | // - mcp_source_header / mcp_source_query_param: child tables for | ||
| 10 | // remote sources' headers and query_params SecretBackedMap entries. | ||
| 11 | // - mcp_binding: per-tool McpToolBinding (toolId/toolName/description/ | ||
| 12 | // input+output schemas/annotations). Stays JSON: it carries no | ||
| 13 | // refs, and `inputSchema` / `outputSchema` are arbitrary | ||
| 14 | // user-supplied JSON Schemas — a legitimate JSON case. | ||
| 15 | // OAuth session storage lives at the core level in `oauth2_session` | ||
| 16 | // and is owned by `ctx.oauth`. | ||
| 5 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
| 6 | 18 | ||
| 7 | import { Effect, Schema } from "effect"; | 19 | import { Effect, Schema } from "effect"; |
| @@ -12,7 +24,12 @@ import { · 4 unmodified lines | |||
| 12 | type StorageFailure, | 24 | type StorageFailure, |
| 13 | } from "@executor-js/sdk/core"; | 25 | } from "@executor-js/sdk/core"; |
| 14 | 26 | ||
| 15 | import { McpToolBinding, McpStoredSourceData } from "./types"; | 27 | import { |
| 28 | McpToolBinding, | ||
| 29 | McpStoredSourceData, | ||
| 30 | type McpConnectionAuth, | ||
| 31 | type SecretBackedValue, | ||
| 32 | } from "./types"; | ||
| 16 | 33 | ||
| 17 | // --------------------------------------------------------------------------- | 34 | // --------------------------------------------------------------------------- |
| 18 | // Schema | 35 | // Schema |
| @@ -24,10 +41,61 @@ export const mcpSchema = defineSchema({ · 5 unmodified lines | |||
| 24 | id: { type: "string", required: true }, | 41 | id: { type: "string", required: true }, |
| 25 | scope_id: { type: "string", required: true, index: true }, | 42 | scope_id: { type: "string", required: true, index: true }, |
| 26 | name: { type: "string", required: true }, | 43 | name: { type: "string", required: true }, |
| 44 | // Plugin-private structural data minus the ref-bearing fields | ||
| 45 | // (auth, headers, queryParams). For remote sources: transport, | ||
| 46 | // endpoint, remoteTransport. For stdio: transport, command, | ||
| 47 | // args, env, cwd. | ||
| 27 | config: { type: "json", required: true }, | 48 | config: { type: "json", required: true }, |
| 49 | // Flattened McpConnectionAuth. Exactly one of the kind-tagged | ||
| 50 | // groups is populated for non-`none` auths. | ||
| 51 | auth_kind: { | ||
| 52 | type: ["none", "header", "oauth2"], | ||
| 53 | required: true, | ||
| 54 | defaultValue: "none", | ||
| 55 | }, | ||
| 56 | // Header-auth fields. | ||
| 57 | auth_header_name: { type: "string", required: false }, | ||
| 58 | auth_secret_id: { type: "string", required: false, index: true }, | ||
| 59 | auth_secret_prefix: { type: "string", required: false }, | ||
| 60 | // OAuth2 auth fields. | ||
| 61 | auth_connection_id: { type: "string", required: false, index: true }, | ||
| 62 | auth_client_id_secret_id: { | ||
| 63 | type: "string", | ||
| 64 | required: false, | ||
| 65 | index: true, | ||
| 66 | }, | ||
| 67 | auth_client_secret_secret_id: { | ||
| 68 | type: "string", | ||
| 69 | required: false, | ||
| 70 | index: true, | ||
| 71 | }, | ||
| 28 | created_at: { type: "date", required: true }, | 72 | created_at: { type: "date", required: true }, |
| 29 | }, | 73 | }, |
| 30 | }, | 74 | }, |
| 75 | mcp_source_header: { | ||
| 76 | fields: { | ||
| 77 | id: { type: "string", required: true }, | ||
| 78 | scope_id: { type: "string", required: true, index: true }, | ||
| 79 | source_id: { type: "string", required: true, index: true }, | ||
| 80 | name: { type: "string", required: true }, | ||
| 81 | kind: { type: ["text", "secret"], required: true }, | ||
| 82 | text_value: { type: "string", required: false }, | ||
| 83 | secret_id: { type: "string", required: false, index: true }, | ||
| 84 | secret_prefix: { type: "string", required: false }, | ||
| 85 | }, | ||
| 86 | }, | ||
| 87 | mcp_source_query_param: { | ||
| 88 | fields: { | ||
| 89 | id: { type: "string", required: true }, | ||
| 90 | scope_id: { type: "string", required: true, index: true }, | ||
| 91 | source_id: { type: "string", required: true, index: true }, | ||
| 92 | name: { type: "string", required: true }, | ||
| 93 | kind: { type: ["text", "secret"], required: true }, | ||
| 94 | text_value: { type: "string", required: false }, | ||
| 95 | secret_id: { type: "string", required: false, index: true }, | ||
| 96 | secret_prefix: { type: "string", required: false }, | ||
| 97 | }, | ||
| 98 | }, | ||
| 31 | mcp_binding: { | 99 | mcp_binding: { |
| 32 | fields: { | 100 | fields: { |
| 33 | id: { type: "string", required: true }, | 101 | id: { type: "string", required: true }, |
| @@ -61,6 +129,126 @@ const coerceJson = (value: unknown): unknown => { · 27 unmodified lines | |||
| 61 | } | 129 | } |
| 62 | }; | 130 | }; |
| 63 | 131 | ||
| 132 | // --- auth column packing/unpacking ------------------------------------------ | ||
| 133 | |||
| 134 | interface AuthColumns { | ||
| 135 | readonly auth_kind: "none" | "header" | "oauth2"; | ||
| 136 | readonly auth_header_name?: string; | ||
| 137 | readonly auth_secret_id?: string; | ||
| 138 | readonly auth_secret_prefix?: string; | ||
| 139 | readonly auth_connection_id?: string; | ||
| 140 | readonly auth_client_id_secret_id?: string; | ||
| 141 | readonly auth_client_secret_secret_id?: string; | ||
| 142 | } | ||
| 143 | |||
| 144 | const authToColumns = (auth: McpConnectionAuth): AuthColumns => { | ||
| 145 | if (auth.kind === "header") { | ||
| 146 | return { | ||
| 147 | auth_kind: "header", | ||
| 148 | auth_header_name: auth.headerName, | ||
| 149 | auth_secret_id: auth.secretId, | ||
| 150 | auth_secret_prefix: auth.prefix, | ||
| 151 | }; | ||
| 152 | } | ||
| 153 | if (auth.kind === "oauth2") { | ||
| 154 | return { | ||
| 155 | auth_kind: "oauth2", | ||
| 156 | auth_connection_id: auth.connectionId, | ||
| 157 | auth_client_id_secret_id: auth.clientIdSecretId, | ||
| 158 | auth_client_secret_secret_id: auth.clientSecretSecretId ?? undefined, | ||
| 159 | }; | ||
| 160 | } | ||
| 161 | return { auth_kind: "none" }; | ||
| 162 | }; | ||
| 163 | |||
| 164 | const columnsToAuth = (row: Record<string, unknown>): McpConnectionAuth => { | ||
| 165 | const kind = row.auth_kind as string; | ||
| 166 | if (kind === "header" && typeof row.auth_secret_id === "string") { | ||
| 167 | const prefix = row.auth_secret_prefix as string | null | undefined; | ||
| 168 | return { | ||
| 169 | kind: "header", | ||
| 170 | headerName: (row.auth_header_name as string | null) ?? "", | ||
| 171 | secretId: row.auth_secret_id, | ||
| 172 | ...(prefix ? { prefix } : {}), | ||
| 173 | }; | ||
| 174 | } | ||
| 175 | if (kind === "oauth2" && typeof row.auth_connection_id === "string") { | ||
| 176 | const cid = row.auth_client_id_secret_id as string | null | undefined; | ||
| 177 | const csec = row.auth_client_secret_secret_id as string | null | undefined; | ||
| 178 | return { | ||
| 179 | kind: "oauth2", | ||
| 180 | connectionId: row.auth_connection_id, | ||
| 181 | ...(cid ? { clientIdSecretId: cid } : {}), | ||
| 182 | ...(csec !== undefined && csec !== null | ||
| 183 | ? { clientSecretSecretId: csec } | ||
| 184 | : {}), | ||
| 185 | }; | ||
| 186 | } | ||
| 187 | return { kind: "none" }; | ||
| 188 | }; | ||
| 189 | |||
| 190 | // --- SecretBackedMap <-> child rows (mcp_source_header / query_param) ------- | ||
| 191 | |||
| 192 | interface SecretBackedRow { | ||
| 193 | readonly id: string; | ||
| 194 | readonly scope_id: string; | ||
| 195 | readonly source_id: string; | ||
| 196 | readonly name: string; | ||
| 197 | readonly kind: "text" | "secret"; | ||
| 198 | readonly text_value?: string; | ||
| 199 | readonly secret_id?: string; | ||
| 200 | readonly secret_prefix?: string; | ||
| 201 | readonly [k: string]: unknown; | ||
| 202 | } | ||
| 203 | |||
| 204 | const valueMapToRows = ( | ||
| 205 | sourceId: string, | ||
| 206 | scope: string, | ||
| 207 | values: Record<string, SecretBackedValue> | undefined, | ||
| 208 | ): readonly SecretBackedRow[] => { | ||
| 209 | if (!values) return []; | ||
| 210 | return Object.entries(values).map(([name, value]) => { | ||
| 211 | const id = JSON.stringify([sourceId, name]); | ||
| 212 | if (typeof value === "string") { | ||
| 213 | return { | ||
| 214 | id, | ||
| 215 | scope_id: scope, | ||
| 216 | source_id: sourceId, | ||
| 217 | name, | ||
| 218 | kind: "text", | ||
| 219 | text_value: value, | ||
| 220 | }; | ||
| 221 | } | ||
| 222 | return { | ||
| 223 | id, | ||
| 224 | scope_id: scope, | ||
| 225 | source_id: sourceId, | ||
| 226 | name, | ||
| 227 | kind: "secret", | ||
| 228 | secret_id: value.secretId, | ||
| 229 | secret_prefix: value.prefix, | ||
| 230 | }; | ||
| 231 | }); | ||
| 232 | }; | ||
| 233 | |||
| 234 | const rowsToValueMap = ( | ||
| 235 | rows: readonly Record<string, unknown>[], | ||
| 236 | ): Record<string, SecretBackedValue> => { | ||
| 237 | const out: Record<string, SecretBackedValue> = {}; | ||
| 238 | for (const row of rows) { | ||
| 239 | const name = row.name as string; | ||
| 240 | if (row.kind === "secret" && typeof row.secret_id === "string") { | ||
| 241 | const prefix = row.secret_prefix as string | undefined | null; | ||
| 242 | out[name] = prefix | ||
| 243 | ? { secretId: row.secret_id, prefix } | ||
| 244 | : { secretId: row.secret_id }; | ||
| 245 | } else if (row.kind === "text" && typeof row.text_value === "string") { | ||
| 246 | out[name] = row.text_value; | ||
| 247 | } | ||
| 248 | } | ||
| 249 | return out; | ||
| 250 | }; | ||
| 251 | |||
| 64 | // --------------------------------------------------------------------------- | 252 | // --------------------------------------------------------------------------- |
| 65 | // Stored source (decoded) — what callers see | 253 | // Stored source (decoded) — what callers see |
| 66 | // --------------------------------------------------------------------------- | 254 | // --------------------------------------------------------------------------- |
| @@ -136,6 +324,55 @@ export interface McpBindingStore { · 69 unmodified lines | |||
| 136 | namespace: string, | 324 | namespace: string, |
| 137 | scope: string, | 325 | scope: string, |
| 138 | ) => Effect.Effect<void, StorageFailure>; | 326 | ) => Effect.Effect<void, StorageFailure>; |
| 327 | |||
| 328 | // --------------------------------------------------------------------- | ||
| 329 | // Usage lookups — back `usagesForSecret` / `usagesForConnection`. | ||
| 330 | // --------------------------------------------------------------------- | ||
| 331 | |||
| 332 | /** Source rows whose flattened auth columns reference the given | ||
| 333 | * secret id. The `slot` field on each result tags which column | ||
| 334 | * matched so the caller can produce a precise Usage.slot. */ | ||
| 335 | readonly findSourcesBySecret: ( | ||
| 336 | secretId: string, | ||
| 337 | ) => Effect.Effect< | ||
| 338 | readonly { | ||
| 339 | readonly namespace: string; | ||
| 340 | readonly scope_id: string; | ||
| 341 | readonly name: string; | ||
| 342 | readonly slot: string; | ||
| 343 | }[], | ||
| 344 | StorageFailure | ||
| 345 | >; | ||
| 346 | |||
| 347 | /** Source rows whose oauth2 auth points at the given connection id. */ | ||
| 348 | readonly findSourcesByConnection: ( | ||
| 349 | connectionId: string, | ||
| 350 | ) => Effect.Effect< | ||
| 351 | readonly { | ||
| 352 | readonly namespace: string; | ||
| 353 | readonly scope_id: string; | ||
| 354 | readonly name: string; | ||
| 355 | readonly slot: string; | ||
| 356 | }[], | ||
| 357 | StorageFailure | ||
| 358 | >; | ||
| 359 | |||
| 360 | /** Header / query_param child rows that reference the given secret id. */ | ||
| 361 | readonly findChildRowsBySecret: (secretId: string) => Effect.Effect< | ||
| 362 | readonly { | ||
| 363 | readonly kind: "header" | "query_param"; | ||
| 364 | readonly source_id: string; | ||
| 365 | readonly scope_id: string; | ||
| 366 | readonly name: string; | ||
| 367 | }[], | ||
| 368 | StorageFailure | ||
| 369 | >; | ||
| 370 | |||
| 371 | /** Resolve display names for `(scope_id, source_id)` pairs in one | ||
| 372 | * round trip. Keys: `${scope_id}:${source_id}`. */ | ||
| 373 | readonly lookupSourceNames: ( | ||
| 374 | keys: readonly string[], | ||
| 375 | ) => Effect.Effect<ReadonlyMap<string, string>, StorageFailure>; | ||
| 139 | } | 376 | } |
| 140 | 377 | ||
| 141 | // --------------------------------------------------------------------------- | 378 | // --------------------------------------------------------------------------- |
| @@ -217,7 +454,7 @@ export const makeMcpStore = ({ · 75 unmodified lines | |||
| 217 | namespace: row.id, | 454 | namespace: row.id, |
| 218 | scope: row.scope_id, | 455 | scope: row.scope_id, |
| 219 | name: row.name, | 456 | name: row.name, |
| 220 | config: decodeSourceData(coerceJson(row.config)), | 457 | config: yield* hydrateSourceData(row, namespace, scope), |
| 221 | }; | 458 | }; |
| 222 | }), | 459 | }), |
| 223 | 460 | ||
| @@ -231,12 +468,14 @@ export const makeMcpStore = ({ · 7 unmodified lines | |||
| 231 | ], | 468 | ], |
| 232 | }); | 469 | }); |
| 233 | if (!row) return null; | 470 | if (!row) return null; |
| 234 | return decodeSourceData(coerceJson(row.config)); | 471 | return yield* hydrateSourceData(row, namespace, scope); |
| 235 | }), | 472 | }), |
| 236 | 473 | ||
| 237 | putSource: (source) => | 474 | putSource: (source) => |
| 238 | Effect.gen(function* () { | 475 | Effect.gen(function* () { |
| 239 | const now = new Date(); | 476 | const now = new Date(); |
| 477 | // Drop the source row and its child rows; recreate. Two-step | ||
| 478 | // matches the existing put-overwrites-existing semantic. | ||
| 240 | yield* db.delete({ | 479 | yield* db.delete({ |
| 241 | model: "mcp_source", | 480 | model: "mcp_source", |
| 242 | where: [ | 481 | where: [ |
| @@ -244,17 +483,67 @@ export const makeMcpStore = ({ · 1 unmodified line | |||
| 244 | { field: "scope_id", value: source.scope }, | 483 | { field: "scope_id", value: source.scope }, |
| 245 | ], | 484 | ], |
| 246 | }); | 485 | }); |
| 486 | yield* deleteSourceChildren(source.namespace, source.scope); | ||
| 487 | |||
| 488 | const auth: McpConnectionAuth = | ||
| 489 | source.config.transport === "remote" | ||
| 490 | ? source.config.auth | ||
| 491 | : { kind: "none" }; | ||
| 492 | const authCols = authToColumns(auth); | ||
| 493 | const headers = | ||
| 494 | source.config.transport === "remote" | ||
| 495 | ? source.config.headers | ||
| 496 | : undefined; | ||
| 497 | const queryParams = | ||
| 498 | source.config.transport === "remote" | ||
| 499 | ? source.config.queryParams | ||
| 500 | : undefined; | ||
| 501 | |||
| 502 | // The encoded config keeps every plugin-private field but | ||
| 503 | // strips auth/headers/queryParams — those moved to columns/ | ||
| 504 | // child tables. We round-trip through encodeSourceData so the | ||
| 505 | // remaining fields stay in the same JSON shape decode expects. | ||
| 506 | const encodedConfig = stripExtractedFields( | ||
| 507 | encodeSourceData(source.config) as Record<string, unknown>, | ||
| 508 | ); | ||
| 509 | |||
| 247 | yield* db.create({ | 510 | yield* db.create({ |
| 248 | model: "mcp_source", | 511 | model: "mcp_source", |
| 249 | data: { | 512 | data: { |
| 250 | id: source.namespace, | 513 | id: source.namespace, |
| 251 | scope_id: source.scope, | 514 | scope_id: source.scope, |
| 252 | name: source.name, | 515 | name: source.name, |
| 253 | config: encodeSourceData(source.config), | 516 | config: encodedConfig, |
| 254 | created_at: now, | 517 | created_at: now, |
| 518 | ...authCols, | ||
| 255 | }, | 519 | }, |
| 256 | forceAllowId: true, | 520 | forceAllowId: true, |
| 257 | }); | 521 | }); |
| 522 | |||
| 523 | const headerRows = valueMapToRows( | ||
| 524 | source.namespace, | ||
| 525 | source.scope, | ||
| 526 | headers, | ||
| 527 | ); | ||
| 528 | if (headerRows.length > 0) { | ||
| 529 | yield* db.createMany({ | ||
| 530 | model: "mcp_source_header", | ||
| 531 | data: headerRows, | ||
| 532 | forceAllowId: true, | ||
| 533 | }); | ||
| 534 | } | ||
| 535 | const paramRows = valueMapToRows( | ||
| 536 | source.namespace, | ||
| 537 | source.scope, | ||
| 538 | queryParams, | ||
| 539 | ); | ||
| 540 | if (paramRows.length > 0) { | ||
| 541 | yield* db.createMany({ | ||
| 542 | model: "mcp_source_query_param", | ||
| 543 | data: paramRows, | ||
| 544 | forceAllowId: true, | ||
| 545 | }); | ||
| 546 | } | ||
| 258 | }), | 547 | }), |
| 259 | 548 | ||
| 260 | removeSource: (namespace, scope) => | 549 | removeSource: (namespace, scope) => |
| @@ -266,6 +555,7 @@ export const makeMcpStore = ({ · 5 unmodified lines | |||
| 266 | { field: "scope_id", value: scope }, | 555 | { field: "scope_id", value: scope }, |
| 267 | ], | 556 | ], |
| 268 | }); | 557 | }); |
| 558 | yield* deleteSourceChildren(namespace, scope); | ||
| 269 | yield* db.delete({ | 559 | yield* db.delete({ |
| 270 | model: "mcp_source", | 560 | model: "mcp_source", |
| 271 | where: [ | 561 | where: [ |
| @@ -274,5 +564,189 @@ export const makeMcpStore = ({ · 2 unmodified lines | |||
| 274 | ], | 564 | ], |
| 275 | }); | 565 | }); |
| 276 | }), | 566 | }), |
| 567 | |||
| 568 | findSourcesBySecret: (secretId) => | ||
| 569 | Effect.gen(function* () { | ||
| 570 | // Three places a secret id can land on an mcp_source row: the | ||
| 571 | // header-auth secret, and the two oauth2 client_*_secret_id | ||
| 572 | // columns. Run all three lookups in parallel and dedupe by | ||
| 573 | // (scope_id, id). | ||
| 574 | const [byHeader, byClientId, byClientSecret] = yield* Effect.all( | ||
| 575 | [ | ||
| 576 | db.findMany({ | ||
| 577 | model: "mcp_source", | ||
| 578 | where: [{ field: "auth_secret_id", value: secretId }], | ||
| 579 | }), | ||
| 580 | db.findMany({ | ||
| 581 | model: "mcp_source", | ||
| 582 | where: [ | ||
| 583 | { field: "auth_client_id_secret_id", value: secretId }, | ||
| 584 | ], | ||
| 585 | }), | ||
| 586 | db.findMany({ | ||
| 587 | model: "mcp_source", | ||
| 588 | where: [ | ||
| 589 | { field: "auth_client_secret_secret_id", value: secretId }, | ||
| 590 | ], | ||
| 591 | }), | ||
| 592 | ], | ||
| 593 | { concurrency: "unbounded" }, | ||
| 594 | ); | ||
| 595 | const dedup = new Map<string, Record<string, unknown>>(); | ||
| 596 | for (const r of [...byHeader, ...byClientId, ...byClientSecret]) { | ||
| 597 | dedup.set(`${r.scope_id}:${r.id}`, r); | ||
| 598 | } | ||
| 599 | return [...dedup.values()].map((row) => ({ | ||
| 600 | namespace: row.id as string, | ||
| 601 | scope_id: row.scope_id as string, | ||
| 602 | name: row.name as string, | ||
| 603 | slot: | ||
| 604 | (byHeader as readonly Record<string, unknown>[]).includes(row) | ||
| 605 | ? "auth.header" | ||
| 606 | : (byClientId as readonly Record<string, unknown>[]).includes( | ||
| 607 | row, | ||
| 608 | ) | ||
| 609 | ? "auth.oauth2.client_id" | ||
| 610 | : "auth.oauth2.client_secret", | ||
| 611 | })); | ||
| 612 | }), | ||
| 613 | |||
| 614 | findSourcesByConnection: (connectionId) => | ||
| 615 | db | ||
| 616 | .findMany({ | ||
| 617 | model: "mcp_source", | ||
| 618 | where: [{ field: "auth_connection_id", value: connectionId }], | ||
| 619 | }) | ||
| 620 | .pipe( | ||
| 621 | Effect.map((rows) => | ||
| 622 | rows.map((r) => ({ | ||
| 623 | namespace: r.id as string, | ||
| 624 | scope_id: r.scope_id as string, | ||
| 625 | name: r.name as string, | ||
| 626 | slot: "auth.oauth2.connection", | ||
| 627 | })), | ||
| 628 | ), | ||
| 629 | ), | ||
| 630 | |||
| 631 | findChildRowsBySecret: (secretId) => | ||
| 632 | Effect.gen(function* () { | ||
| 633 | const [headers, params] = yield* Effect.all( | ||
| 634 | [ | ||
| 635 | db.findMany({ | ||
| 636 | model: "mcp_source_header", | ||
| 637 | where: [{ field: "secret_id", value: secretId }], | ||
| 638 | }), | ||
| 639 | db.findMany({ | ||
| 640 | model: "mcp_source_query_param", | ||
| 641 | where: [{ field: "secret_id", value: secretId }], | ||
| 642 | }), | ||
| 643 | ], | ||
| 644 | { concurrency: "unbounded" }, | ||
| 645 | ); | ||
| 646 | return [ | ||
| 647 | ...headers.map((r) => ({ | ||
| 648 | kind: "header" as const, | ||
| 649 | source_id: r.source_id as string, | ||
| 650 | scope_id: r.scope_id as string, | ||
| 651 | name: r.name as string, | ||
| 652 | })), | ||
| 653 | ...params.map((r) => ({ | ||
| 654 | kind: "query_param" as const, | ||
| 655 | source_id: r.source_id as string, | ||
| 656 | scope_id: r.scope_id as string, | ||
| 657 | name: r.name as string, | ||
| 658 | })), | ||
| 659 | ]; | ||
| 660 | }), | ||
| 661 | |||
| 662 | lookupSourceNames: (keys) => | ||
| 663 | Effect.gen(function* () { | ||
| 664 | if (keys.length === 0) return new Map<string, string>(); | ||
| 665 | const rows = yield* db.findMany({ model: "mcp_source" }); | ||
| 666 | const requested = new Set(keys); | ||
| 667 | const out = new Map<string, string>(); | ||
| 668 | for (const r of rows) { | ||
| 669 | const key = `${r.scope_id as string}:${r.id as string}`; | ||
| 670 | if (requested.has(key)) out.set(key, r.name as string); | ||
| 671 | } | ||
| 672 | return out; | ||
| 673 | }), | ||
| 277 | }; | 674 | }; |
| 675 | |||
| 676 | // --------------------------------------------------------------------- | ||
| 677 | // Private helpers — depend on `db` so they live inside the closure. | ||
| 678 | // --------------------------------------------------------------------- | ||
| 679 | |||
| 680 | function deleteSourceChildren(namespace: string, scope: string) { | ||
| 681 | return Effect.gen(function* () { | ||
| 682 | for (const model of [ | ||
| 683 | "mcp_source_header", | ||
| 684 | "mcp_source_query_param", | ||
| 685 | ] as const) { | ||
| 686 | yield* db.deleteMany({ | ||
| 687 | model, | ||
| 688 | where: [ | ||
| 689 | { field: "source_id", value: namespace }, | ||
| 690 | { field: "scope_id", value: scope }, | ||
| 691 | ], | ||
| 692 | }); | ||
| 693 | } | ||
| 694 | }); | ||
| 695 | } | ||
| 696 | |||
| 697 | function hydrateSourceData( | ||
| 698 | row: Record<string, unknown>, | ||
| 699 | namespace: string, | ||
| 700 | scope: string, | ||
| 701 | ): Effect.Effect<McpStoredSourceData, StorageFailure> { | ||
| 702 | return Effect.gen(function* () { | ||
| 703 | // The stored JSON has auth/headers/queryParams stripped (those | ||
| 704 | // moved to columns / child tables). We must rehydrate the full | ||
| 705 | // shape BEFORE handing it to the schema decoder, because | ||
| 706 | // `McpRemoteSourceData.auth` is required. | ||
| 707 | const partial = coerceJson(row.config) as Record<string, unknown>; | ||
| 708 | if (partial.transport !== "remote") { | ||
| 709 | // stdio sources have no extracted fields — decode as-is. | ||
| 710 | return decodeSourceData(partial); | ||
| 711 | } | ||
| 712 | const headerRows = yield* db.findMany({ | ||
| 713 | model: "mcp_source_header", | ||
| 714 | where: [ | ||
| 715 | { field: "source_id", value: namespace }, | ||
| 716 | { field: "scope_id", value: scope }, | ||
| 717 | ], | ||
| 718 | }); | ||
| 719 | const paramRows = yield* db.findMany({ | ||
| 720 | model: "mcp_source_query_param", | ||
| 721 | where: [ | ||
| 722 | { field: "source_id", value: namespace }, | ||
| 723 | { field: "scope_id", value: scope }, | ||
| 724 | ], | ||
| 725 | }); | ||
| 726 | const headers = rowsToValueMap(headerRows); | ||
| 727 | const queryParams = rowsToValueMap(paramRows); | ||
| 728 | const reassembled = { | ||
| 729 | ...partial, | ||
| 730 | ...(Object.keys(headers).length > 0 ? { headers } : {}), | ||
| 731 | ...(Object.keys(queryParams).length > 0 ? { queryParams } : {}), | ||
| 732 | auth: columnsToAuth(row), | ||
| 733 | }; | ||
| 734 | return decodeSourceData(reassembled); | ||
| 735 | }); | ||
| 736 | } | ||
| 737 | }; | ||
| 738 | |||
| 739 | // Strip auth/headers/queryParams from the encoded source-data shape. | ||
| 740 | // Keeps the remaining structural fields (transport, endpoint, etc.) in | ||
| 741 | // the JSON config column. Per-transport: only the remote variant has | ||
| 742 | // these fields, so this is a no-op for stdio. | ||
| 743 | const stripExtractedFields = ( | ||
| 744 | encoded: Record<string, unknown>, | ||
| 745 | ): Record<string, unknown> => { | ||
| 746 | if (encoded.transport !== "remote") return encoded; | ||
| 747 | const { auth, headers, queryParams, ...rest } = encoded; | ||
| 748 | void auth; | ||
| 749 | void headers; | ||
| 750 | void queryParams; | ||
| 751 | return rest; | ||
| 278 | }; | 752 | }; |
| @@ -635,6 +635,82 @@ describe("mcpPlugin", () => { · 634 unmodified lines | |||
| 635 | ); | 635 | ); |
| 636 | }), | 636 | }), |
| 637 | ); | 637 | ); |
| 638 | |||
| 639 | // ------------------------------------------------------------------------- | ||
| 640 | // Usage tracking — refs land on auth_* columns + child tables and the | ||
| 641 | // plugin's `usagesForSecret` / `usagesForConnection` should surface | ||
| 642 | // every one. addSource against an unreachable endpoint still persists | ||
| 643 | // the source row so the assertion runs without needing a live server. | ||
| 644 | // ------------------------------------------------------------------------- | ||
| 645 | |||
| 646 | it.effect("usagesForSecret aggregates header-auth + headers child rows", () => | ||
| 647 | Effect.gen(function* () { | ||
| 648 | const executor = yield* createExecutor( | ||
| 649 | makeTestConfig({ plugins: [mcpPlugin()] as const }), | ||
| 650 | ); | ||
| 651 | |||
| 652 | yield* executor.mcp | ||
| 653 | .addSource({ | ||
| 654 | transport: "remote", | ||
| 655 | scope: "test-scope", | ||
| 656 | name: "header-auth", | ||
| 657 | endpoint: "http://127.0.0.1:1/mcp", | ||
| 658 | namespace: "header_auth_source", | ||
| 659 | auth: { | ||
| 660 | kind: "header", | ||
| 661 | headerName: "X-API-Key", | ||
| 662 | secretId: "shared-key", | ||
| 663 | }, | ||
| 664 | headers: { "X-Trace": { secretId: "shared-key" } }, | ||
| 665 | queryParams: { ping: { secretId: "other-secret" } }, | ||
| 666 | }) | ||
| 667 | .pipe(Effect.result); | ||
| 668 | |||
| 669 | const usages = yield* executor.secrets.usages( | ||
| 670 | SecretId.make("shared-key"), | ||
| 671 | ); | ||
| 672 | expect(usages.length).toBe(2); | ||
| 673 | const slots = usages.map((u) => u.slot).sort(); | ||
| 674 | expect(slots).toEqual(["auth.header", "header:X-Trace"]); | ||
| 675 | expect(usages.every((u) => u.pluginId === "mcp")).toBe(true); | ||
| 676 | |||
| 677 | const otherUsages = yield* executor.secrets.usages( | ||
| 678 | SecretId.make("other-secret"), | ||
| 679 | ); | ||
| 680 | expect(otherUsages.length).toBe(1); | ||
| 681 | expect(otherUsages[0].slot).toBe("query_param:ping"); | ||
| 682 | }), | ||
| 683 | ); | ||
| 684 | |||
| 685 | it.effect("usagesForConnection finds oauth2-bound mcp sources", () => | ||
| 686 | Effect.gen(function* () { | ||
| 687 | const executor = yield* createExecutor( | ||
| 688 | makeTestConfig({ plugins: [mcpPlugin()] as const }), | ||
| 689 | ); | ||
| 690 | |||
| 691 | yield* executor.mcp | ||
| 692 | .addSource({ | ||
| 693 | transport: "remote", | ||
| 694 | scope: "test-scope", | ||
| 695 | name: "oauth-source", | ||
| 696 | endpoint: "http://127.0.0.1:1/mcp", | ||
| 697 | namespace: "oauth_ref", | ||
| 698 | auth: { kind: "oauth2", connectionId: "conn-xyz" }, | ||
| 699 | }) | ||
| 700 | .pipe(Effect.result); | ||
| 701 | |||
| 702 | const usages = yield* executor.connections.usages( | ||
| 703 | ConnectionId.make("conn-xyz"), | ||
| 704 | ); | ||
| 705 | expect(usages.length).toBe(1); | ||
| 706 | expect(usages[0]).toMatchObject({ | ||
| 707 | pluginId: "mcp", | ||
| 708 | ownerKind: "mcp-source", | ||
| 709 | ownerId: "oauth_ref", | ||
| 710 | slot: "auth.oauth2.connection", | ||
| 711 | }); | ||
| 712 | }), | ||
| 713 | ); | ||
| 638 | }); | 714 | }); |
| 639 | 715 | ||
| 640 | // --------------------------------------------------------------------------- | 716 | // --------------------------------------------------------------------------- |
| @@ -6,7 +6,9 @@ import { McpGroup } from "../api/group"; · 5 unmodified lines | |||
| 6 | import { McpExtensionService, McpHandlers } from "../api/handlers"; | 6 | import { McpExtensionService, McpHandlers } from "../api/handlers"; |
| 7 | 7 | ||
| 8 | import { | 8 | import { |
| 9 | ScopeId, | ||
| 9 | SourceDetectionResult, | 10 | SourceDetectionResult, |
| 11 | Usage, | ||
| 10 | definePlugin, | 12 | definePlugin, |
| 11 | resolveSecretBackedMap as resolveSharedSecretBackedMap, | 13 | resolveSecretBackedMap as resolveSharedSecretBackedMap, |
| 12 | type PluginCtx, | 14 | type PluginCtx, |
| @@ -993,6 +995,71 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 980 unmodified lines | |||
| 993 | yield* ctx.storage.removeSource(sourceId, scope); | 995 | yield* ctx.storage.removeSource(sourceId, scope); |
| 994 | }), | 996 | }), |
| 995 | 997 | ||
| 998 | // Aggregate usages across the four places mcp can hold a secret ref: | ||
| 999 | // header-auth secret, oauth2 client_id_secret, oauth2 client_secret, | ||
| 1000 | // and the per-entry rows in mcp_source_header / mcp_source_query_param. | ||
| 1001 | usagesForSecret: ({ ctx, args }) => | ||
| 1002 | Effect.gen(function* () { | ||
| 1003 | const sources = yield* ctx.storage.findSourcesBySecret(args.secretId); | ||
| 1004 | const childRows = yield* ctx.storage.findChildRowsBySecret( | ||
| 1005 | args.secretId, | ||
| 1006 | ); | ||
| 1007 | |||
| 1008 | const sourceKeys = new Set<string>(); | ||
| 1009 | for (const s of sources) { | ||
| 1010 | sourceKeys.add(`${s.scope_id}:${s.namespace}`); | ||
| 1011 | } | ||
| 1012 | for (const r of childRows) { | ||
| 1013 | sourceKeys.add(`${r.scope_id}:${r.source_id}`); | ||
| 1014 | } | ||
| 1015 | const names = yield* ctx.storage.lookupSourceNames([...sourceKeys]); | ||
| 1016 | |||
| 1017 | const out: Usage[] = []; | ||
| 1018 | for (const s of sources) { | ||
| 1019 | out.push( | ||
| 1020 | new Usage({ | ||
| 1021 | pluginId: "mcp", | ||
| 1022 | scopeId: ScopeId.make(s.scope_id), | ||
| 1023 | ownerKind: "mcp-source", | ||
| 1024 | ownerId: s.namespace, | ||
| 1025 | ownerName: names.get(`${s.scope_id}:${s.namespace}`) ?? s.name, | ||
| 1026 | slot: s.slot, | ||
| 1027 | }), | ||
| 1028 | ); | ||
| 1029 | } | ||
| 1030 | for (const r of childRows) { | ||
| 1031 | out.push( | ||
| 1032 | new Usage({ | ||
| 1033 | pluginId: "mcp", | ||
| 1034 | scopeId: ScopeId.make(r.scope_id), | ||
| 1035 | ownerKind: `mcp-source-${r.kind.replace(/_/g, "-")}`, | ||
| 1036 | ownerId: r.source_id, | ||
| 1037 | ownerName: names.get(`${r.scope_id}:${r.source_id}`) ?? null, | ||
| 1038 | slot: `${r.kind}:${r.name}`, | ||
| 1039 | }), | ||
| 1040 | ); | ||
| 1041 | } | ||
| 1042 | return out; | ||
| 1043 | }), | ||
| 1044 | |||
| 1045 | usagesForConnection: ({ ctx, args }) => | ||
| 1046 | Effect.gen(function* () { | ||
| 1047 | const sources = yield* ctx.storage.findSourcesByConnection( | ||
| 1048 | args.connectionId, | ||
| 1049 | ); | ||
| 1050 | return sources.map( | ||
| 1051 | (s) => | ||
| 1052 | new Usage({ | ||
| 1053 | pluginId: "mcp", | ||
| 1054 | scopeId: ScopeId.make(s.scope_id), | ||
| 1055 | ownerKind: "mcp-source", | ||
| 1056 | ownerId: s.namespace, | ||
| 1057 | ownerName: s.name, | ||
| 1058 | slot: s.slot, | ||
| 1059 | }), | ||
| 1060 | ); | ||
| 1061 | }), | ||
| 1062 | |||
| 996 | refreshSource: () => Effect.void, | 1063 | refreshSource: () => Effect.void, |
| 997 | 1064 | ||
| 998 | // Connection refresh for oauth2-minted sources is owned by the | 1065 | // Connection refresh for oauth2-minted sources is owned by the |
| @@ -20,7 +20,7 @@ import { · 19 unmodified lines | |||
| 20 | 20 | ||
| 21 | const TEST_SCOPE = "test-scope"; | 21 | const TEST_SCOPE = "test-scope"; |
| 22 | import { openApiPlugin } from "./plugin"; | 22 | import { openApiPlugin } from "./plugin"; |
| 23 | import { OAuth2Auth } from "./types"; | 23 | import { OAuth2Auth, OpenApiSourceBindingInput } from "./types"; |
| 24 | 24 | ||
| 25 | const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; | 25 | const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; |
| 26 | 26 | ||
| @@ -847,4 +847,108 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 820 unmodified lines | |||
| 847 | expect(tools.some((t) => t.id.startsWith("deferred."))).toBe(true); | 847 | expect(tools.some((t) => t.id.startsWith("deferred."))).toBe(true); |
| 848 | }), | 848 | }), |
| 849 | ); | 849 | ); |
| 850 | |||
| 851 | // ------------------------------------------------------------------------- | ||
| 852 | // Usage tracking — once openapi normalizes its bindings, query_params, | ||
| 853 | // and specFetchCredentials, `usagesForSecret` and `usagesForConnection` | ||
| 854 | // should surface every reference and `secrets.remove` should RESTRICT. | ||
| 855 | // ------------------------------------------------------------------------- | ||
| 856 | |||
| 857 | it.effect("usagesForSecret aggregates bindings and child rows", () => | ||
| 858 | Effect.gen(function* () { | ||
| 859 | const executor = yield* createExecutor( | ||
| 860 | makeTestConfig({ | ||
| 861 | plugins: [memorySecretsPlugin(), openApiPlugin()] as const, | ||
| 862 | }), | ||
| 863 | ); | ||
| 864 | |||
| 865 | yield* executor.secrets.set( | ||
| 866 | new SetSecretInput({ | ||
| 867 | id: SecretId.make("api-key"), | ||
| 868 | scope: ScopeId.make(TEST_SCOPE), | ||
| 869 | name: "API Key", | ||
| 870 | value: "abc123", | ||
| 871 | provider: "memory", | ||
| 872 | }), | ||
| 873 | ); | ||
| 874 | |||
| 875 | // Add a source whose query_params reference the secret directly. | ||
| 876 | yield* executor.openapi.addSpec({ | ||
| 877 | spec: specJson, | ||
| 878 | scope: TEST_SCOPE, | ||
| 879 | namespace: "with_secret", | ||
| 880 | baseUrl: "http://example.com", | ||
| 881 | queryParams: { token: { secretId: "api-key" } }, | ||
| 882 | }); | ||
| 883 | |||
| 884 | // Configure a slot binding pointing at the same secret. | ||
| 885 | yield* executor.openapi.setSourceBinding( | ||
| 886 | new OpenApiSourceBindingInput({ | ||
| 887 | sourceId: "with_secret", | ||
| 888 | sourceScope: ScopeId.make(TEST_SCOPE), | ||
| 889 | scope: ScopeId.make(TEST_SCOPE), | ||
| 890 | slot: "header:authorization", | ||
| 891 | value: { kind: "secret", secretId: SecretId.make("api-key") }, | ||
| 892 | }), | ||
| 893 | ); | ||
| 894 | |||
| 895 | const usages = yield* executor.secrets.usages(SecretId.make("api-key")); | ||
| 896 | expect(usages.length).toBe(2); | ||
| 897 | const slots = usages.map((u) => u.slot).sort(); | ||
| 898 | expect(slots).toEqual([ | ||
| 899 | "binding:header:authorization", | ||
| 900 | "query_param:token", | ||
| 901 | ]); | ||
| 902 | expect(usages.every((u) => u.pluginId === "openapi")).toBe(true); | ||
| 903 | }), | ||
| 904 | ); | ||
| 905 | |||
| 906 | it.effect("secrets.remove refuses while an openapi binding still uses it", () => | ||
| 907 | Effect.gen(function* () { | ||
| 908 | const executor = yield* createExecutor( | ||
| 909 | makeTestConfig({ | ||
| 910 | plugins: [memorySecretsPlugin(), openApiPlugin()] as const, | ||
| 911 | }), | ||
| 912 | ); | ||
| 913 | yield* executor.secrets.set( | ||
| 914 | new SetSecretInput({ | ||
| 915 | id: SecretId.make("locked"), | ||
| 916 | scope: ScopeId.make(TEST_SCOPE), | ||
| 917 | name: "Locked", | ||
| 918 | value: "v", | ||
| 919 | provider: "memory", | ||
| 920 | }), | ||
| 921 | ); | ||
| 922 | |||
| 923 | yield* executor.openapi.addSpec({ | ||
| 924 | spec: specJson, | ||
| 925 | scope: TEST_SCOPE, | ||
| 926 | namespace: "ref", | ||
| 927 | baseUrl: "http://example.com", | ||
| 928 | }); | ||
| 929 | yield* executor.openapi.setSourceBinding( | ||
| 930 | new OpenApiSourceBindingInput({ | ||
| 931 | sourceId: "ref", | ||
| 932 | sourceScope: ScopeId.make(TEST_SCOPE), | ||
| 933 | scope: ScopeId.make(TEST_SCOPE), | ||
| 934 | slot: "header:authorization", | ||
| 935 | value: { kind: "secret", secretId: SecretId.make("locked") }, | ||
| 936 | }), | ||
| 937 | ); | ||
| 938 | |||
| 939 | const failure = yield* executor.secrets | ||
| 940 | .remove(SecretId.make("locked")) | ||
| 941 | .pipe(Effect.flip); | ||
| 942 | expect((failure as { _tag: string })._tag).toBe("SecretInUseError"); | ||
| 943 | |||
| 944 | // Detach the binding, then remove succeeds. | ||
| 945 | yield* executor.openapi.removeSourceBinding( | ||
| 946 | "ref", | ||
| 947 | ScopeId.make(TEST_SCOPE), | ||
| 948 | "header:authorization", | ||
| 949 | ScopeId.make(TEST_SCOPE), | ||
| 950 | ); | ||
| 951 | yield* executor.secrets.remove(SecretId.make("locked")); | ||
| 952 | }), | ||
| 953 | ); | ||
| 850 | }); | 954 | }); |
| @@ -10,6 +10,7 @@ import { · 9 unmodified lines | |||
| 10 | ScopeId, | 10 | ScopeId, |
| 11 | SecretId, | 11 | SecretId, |
| 12 | SourceDetectionResult, | 12 | SourceDetectionResult, |
| 13 | Usage, | ||
| 13 | definePlugin, | 14 | definePlugin, |
| 14 | resolveSecretBackedMap, | 15 | resolveSecretBackedMap, |
| 15 | type PluginCtx, | 16 | type PluginCtx, |
| @@ -1030,6 +1031,82 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 1014 unmodified lines | |||
| 1030 | 1031 | ||
| 1031 | removeSource: ({ ctx, sourceId, scope }) => ctx.storage.removeSource(sourceId, scope), | 1032 | removeSource: ({ ctx, sourceId, scope }) => ctx.storage.removeSource(sourceId, scope), |
| 1032 | 1033 | ||
| 1034 | // Aggregate usages across the four places openapi can hold a direct | ||
| 1035 | // secret ref: | ||
| 1036 | // - openapi_source_binding.secret_id (kind=secret slot bindings) | ||
| 1037 | // - openapi_source_query_param | ||
| 1038 | // - openapi_source_spec_fetch_header / spec_fetch_query_param | ||
| 1039 | // Each is one indexed SELECT in the store; the merge plus a single | ||
| 1040 | // source-name JOIN happens here. | ||
| 1041 | usagesForSecret: ({ ctx, args }) => | ||
| 1042 | Effect.gen(function* () { | ||
| 1043 | const bindings = yield* ctx.storage.findBindingsBySecret(args.secretId); | ||
| 1044 | const childRows = yield* ctx.storage.findChildRowsBySecret( | ||
| 1045 | args.secretId, | ||
| 1046 | ); | ||
| 1047 | |||
| 1048 | const sourceKeys = new Set<string>(); | ||
| 1049 | for (const b of bindings) { | ||
| 1050 | sourceKeys.add(`${b.sourceScopeId}:${b.sourceId}`); | ||
| 1051 | } | ||
| 1052 | for (const r of childRows) { | ||
| 1053 | sourceKeys.add(`${r.scope_id}:${r.source_id}`); | ||
| 1054 | } | ||
| 1055 | const sources = yield* ctx.storage.lookupSourceNames([...sourceKeys]); | ||
| 1056 | |||
| 1057 | const out: Usage[] = []; | ||
| 1058 | for (const b of bindings) { | ||
| 1059 | out.push( | ||
| 1060 | new Usage({ | ||
| 1061 | pluginId: "openapi", | ||
| 1062 | scopeId: ScopeId.make(b.scopeId), | ||
| 1063 | ownerKind: "openapi-source-binding", | ||
| 1064 | ownerId: b.sourceId, | ||
| 1065 | ownerName: | ||
| 1066 | sources.get(`${b.sourceScopeId}:${b.sourceId}`) ?? null, | ||
| 1067 | slot: `binding:${b.slot}`, | ||
| 1068 | }), | ||
| 1069 | ); | ||
| 1070 | } | ||
| 1071 | for (const r of childRows) { | ||
| 1072 | out.push( | ||
| 1073 | new Usage({ | ||
| 1074 | pluginId: "openapi", | ||
| 1075 | scopeId: ScopeId.make(r.scope_id), | ||
| 1076 | ownerKind: `openapi-source-${r.kind.replace(/_/g, "-")}`, | ||
| 1077 | ownerId: r.source_id, | ||
| 1078 | ownerName: sources.get(`${r.scope_id}:${r.source_id}`) ?? null, | ||
| 1079 | slot: `${r.kind}:${r.name}`, | ||
| 1080 | }), | ||
| 1081 | ); | ||
| 1082 | } | ||
| 1083 | return out; | ||
| 1084 | }), | ||
| 1085 | |||
| 1086 | usagesForConnection: ({ ctx, args }) => | ||
| 1087 | Effect.gen(function* () { | ||
| 1088 | const bindings = yield* ctx.storage.findBindingsByConnection( | ||
| 1089 | args.connectionId, | ||
| 1090 | ); | ||
| 1091 | const sourceKeys = new Set<string>(); | ||
| 1092 | for (const b of bindings) { | ||
| 1093 | sourceKeys.add(`${b.sourceScopeId}:${b.sourceId}`); | ||
| 1094 | } | ||
| 1095 | const sources = yield* ctx.storage.lookupSourceNames([...sourceKeys]); | ||
| 1096 | return bindings.map( | ||
| 1097 | (b) => | ||
| 1098 | new Usage({ | ||
| 1099 | pluginId: "openapi", | ||
| 1100 | scopeId: ScopeId.make(b.scopeId), | ||
| 1101 | ownerKind: "openapi-source-binding", | ||
| 1102 | ownerId: b.sourceId, | ||
| 1103 | ownerName: | ||
| 1104 | sources.get(`${b.sourceScopeId}:${b.sourceId}`) ?? null, | ||
| 1105 | slot: `binding:${b.slot}`, | ||
| 1106 | }), | ||
| 1107 | ); | ||
| 1108 | }), | ||
| 1109 | |||
| 1033 | // Re-fetch the spec from its origin URL (captured at addSpec time) | 1110 | // Re-fetch the spec from its origin URL (captured at addSpec time) |
| 1034 | // and replay the same parse → extract → upsertSource → register | 1111 | // and replay the same parse → extract → upsertSource → register |
| 1035 | // path used by addSpec. Sources without a stored URL surface a | 1112 | // path used by addSpec. Sources without a stored URL surface a |
| @@ -1,8 +1,10 @@ | |||
| 1 | import { Effect, Schema } from "effect"; | 1 | import { Effect, Schema } from "effect"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | ConnectionId, | ||
| 4 | defineSchema, | 5 | defineSchema, |
| 5 | ScopeId, | 6 | ScopeId, |
| 7 | SecretId, | ||
| 6 | StorageError, | 8 | StorageError, |
| 7 | type StorageDeps, | 9 | type StorageDeps, |
| 8 | type StorageFailure, | 10 | type StorageFailure, |
| @@ -27,6 +29,15 @@ import { · 18 unmodified lines | |||
| 27 | // - openapi_source_binding: credential bindings for shared sources | 29 | // - openapi_source_binding: credential bindings for shared sources |
| 28 | // --------------------------------------------------------------------------- | 30 | // --------------------------------------------------------------------------- |
| 29 | 31 | ||
| 32 | // Each of the secret-backed child tables (`openapi_source_query_param`, | ||
| 33 | // `openapi_source_spec_fetch_header`, | ||
| 34 | // `openapi_source_spec_fetch_query_param`) shares the same column shape: | ||
| 35 | // id/scope_id/source_id/name plus a `kind` enum that discriminates a | ||
| 36 | // literal text value from a secret reference (with optional prefix). | ||
| 37 | // The fields are inlined per-table because `defineSchema`'s type | ||
| 38 | // narrowing relies on the literal types staying on the original | ||
| 39 | // declaration site. | ||
| 40 | |||
| 30 | export const openapiSchema = defineSchema({ | 41 | export const openapiSchema = defineSchema({ |
| 31 | openapi_source: { | 42 | openapi_source: { |
| 32 | fields: { | 43 | fields: { |
| @@ -40,10 +51,14 @@ export const openapiSchema = defineSchema({ · 7 unmodified lines | |||
| 40 | // is the address re-fetched on `refreshSource`. | 51 | // is the address re-fetched on `refreshSource`. |
| 41 | source_url: { type: "string", required: false }, | 52 | source_url: { type: "string", required: false }, |
| 42 | base_url: { type: "string", required: false }, | 53 | base_url: { type: "string", required: false }, |
| 54 | // `headers` and `oauth2` stay JSON: these carry slot names, not | ||
| 55 | // direct secret/connection ids. The secrets/connections that | ||
| 56 | // actually power them live one level of indirection deeper, in | ||
| 57 | // `openapi_source_binding` rows keyed by slot — and those ARE | ||
| 58 | // normalized below. Headers and oauth2 are plugin-private | ||
| 59 | // structural data, not cross-cutting refs. | ||
| 43 | headers: { type: "json", required: false }, | 60 | headers: { type: "json", required: false }, |
| 44 | query_params: { type: "json", required: false }, | ||
| 45 | oauth2: { type: "json", required: false }, | 61 | oauth2: { type: "json", required: false }, |
| 46 | invocation_config: { type: "json", required: true }, | ||
| 47 | }, | 62 | }, |
| 48 | }, | 63 | }, |
| 49 | openapi_operation: { | 64 | openapi_operation: { |
| @@ -66,11 +81,54 @@ export const openapiSchema = defineSchema({ · 16 unmodified lines | |||
| 66 | // bindings when a shared source is removed. | 81 | // bindings when a shared source is removed. |
| 67 | target_scope_id: { type: "string", required: true, index: true }, | 82 | target_scope_id: { type: "string", required: true, index: true }, |
| 68 | slot: { type: "string", required: true, index: true }, | 83 | slot: { type: "string", required: true, index: true }, |
| 69 | value: { type: "json", required: true }, | 84 | // Discriminated union, flattened. Exactly one of secret_id / |
| 85 | // connection_id / text_value is populated based on `kind`. | ||
| 86 | // `secret_id` and `connection_id` are indexed so usages queries | ||
| 87 | // are one-hop SELECTs. | ||
| 88 | kind: { type: ["secret", "connection", "text"], required: true }, | ||
| 89 | secret_id: { type: "string", required: false, index: true }, | ||
| 90 | connection_id: { type: "string", required: false, index: true }, | ||
| 91 | text_value: { type: "string", required: false }, | ||
| 70 | created_at: { type: "date", required: true }, | 92 | created_at: { type: "date", required: true }, |
| 71 | updated_at: { type: "date", required: true }, | 93 | updated_at: { type: "date", required: true }, |
| 72 | }, | 94 | }, |
| 73 | }, | 95 | }, |
| 96 | openapi_source_query_param: { | ||
| 97 | fields: { | ||
| 98 | id: { type: "string", required: true }, | ||
| 99 | scope_id: { type: "string", required: true, index: true }, | ||
| 100 | source_id: { type: "string", required: true, index: true }, | ||
| 101 | name: { type: "string", required: true }, | ||
| 102 | kind: { type: ["text", "secret"], required: true }, | ||
| 103 | text_value: { type: "string", required: false }, | ||
| 104 | secret_id: { type: "string", required: false, index: true }, | ||
| 105 | secret_prefix: { type: "string", required: false }, | ||
| 106 | }, | ||
| 107 | }, | ||
| 108 | openapi_source_spec_fetch_header: { | ||
| 109 | fields: { | ||
| 110 | id: { type: "string", required: true }, | ||
| 111 | scope_id: { type: "string", required: true, index: true }, | ||
| 112 | source_id: { type: "string", required: true, index: true }, | ||
| 113 | name: { type: "string", required: true }, | ||
| 114 | kind: { type: ["text", "secret"], required: true }, | ||
| 115 | text_value: { type: "string", required: false }, | ||
| 116 | secret_id: { type: "string", required: false, index: true }, | ||
| 117 | secret_prefix: { type: "string", required: false }, | ||
| 118 | }, | ||
| 119 | }, | ||
| 120 | openapi_source_spec_fetch_query_param: { | ||
| 121 | fields: { | ||
| 122 | id: { type: "string", required: true }, | ||
| 123 | scope_id: { type: "string", required: true, index: true }, | ||
| 124 | source_id: { type: "string", required: true, index: true }, | ||
| 125 | name: { type: "string", required: true }, | ||
| 126 | kind: { type: ["text", "secret"], required: true }, | ||
| 127 | text_value: { type: "string", required: false }, | ||
| 128 | secret_id: { type: "string", required: false, index: true }, | ||
| 129 | secret_prefix: { type: "string", required: false }, | ||
| 130 | }, | ||
| 131 | }, | ||
| 74 | }); | 132 | }); |
| 75 | 133 | ||
| 76 | export type OpenapiSchema = typeof openapiSchema; | 134 | export type OpenapiSchema = typeof openapiSchema; |
| @@ -166,16 +224,70 @@ const decodeBinding = Schema.decodeUnknownSync(OperationBinding); · 89 unmodified lines | |||
| 166 | 224 | ||
| 167 | const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth); | 225 | const decodeOAuth2 = Schema.decodeUnknownSync(OAuth2Auth); |
| 168 | const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig); | 226 | const encodeOAuth2SourceConfig = Schema.encodeSync(OAuth2SourceConfig); |
| 169 | const encodeSourceBindingValue = Schema.encodeSync(OpenApiSourceBindingValue); | ||
| 170 | const decodeSourceBindingValue = Schema.decodeUnknownSync( | ||
| 171 | OpenApiSourceBindingValue, | ||
| 172 | ); | ||
| 173 | 227 | ||
| 174 | const asJsonObject = (value: unknown): Record<string, unknown> => { | 228 | interface ChildRow { |
| 175 | if (value == null) return {}; | 229 | readonly id: string; |
| 176 | if (typeof value === "string") | 230 | readonly scope_id: string; |
| 177 | return JSON.parse(value) as Record<string, unknown>; | 231 | readonly source_id: string; |
| 178 | return value as Record<string, unknown>; | 232 | readonly name: string; |
| 233 | readonly kind: "text" | "secret"; | ||
| 234 | readonly text_value?: string; | ||
| 235 | readonly secret_id?: string; | ||
| 236 | readonly secret_prefix?: string; | ||
| 237 | // Index signature to satisfy adapter's `RowInput` shape (the typed | ||
| 238 | // adapter exposes its row shape with one). | ||
| 239 | readonly [k: string]: unknown; | ||
| 240 | } | ||
| 241 | |||
| 242 | // Collapse a SecretBackedValue map into the flat child-table column | ||
| 243 | // shape used by openapi_source_query_param and the two | ||
| 244 | // openapi_source_spec_fetch_* tables. Returns one record per entry. | ||
| 245 | const valueMapToChildRows = ( | ||
| 246 | sourceId: string, | ||
| 247 | scope: string, | ||
| 248 | values: Record<string, HeaderValue> | undefined, | ||
| 249 | ): readonly ChildRow[] => { | ||
| 250 | if (!values) return []; | ||
| 251 | return Object.entries(values).map(([name, value]) => { | ||
| 252 | const id = JSON.stringify([sourceId, name]); | ||
| 253 | if (typeof value === "string") { | ||
| 254 | return { | ||
| 255 | id, | ||
| 256 | scope_id: scope, | ||
| 257 | source_id: sourceId, | ||
| 258 | name, | ||
| 259 | kind: "text", | ||
| 260 | text_value: value, | ||
| 261 | }; | ||
| 262 | } | ||
| 263 | return { | ||
| 264 | id, | ||
| 265 | scope_id: scope, | ||
| 266 | source_id: sourceId, | ||
| 267 | name, | ||
| 268 | kind: "secret", | ||
| 269 | secret_id: value.secretId, | ||
| 270 | secret_prefix: value.prefix, | ||
| 271 | }; | ||
| 272 | }); | ||
| 273 | }; | ||
| 274 | |||
| 275 | const childRowsToValueMap = ( | ||
| 276 | rows: readonly Record<string, unknown>[], | ||
| 277 | ): Record<string, HeaderValue> => { | ||
| 278 | const out: Record<string, HeaderValue> = {}; | ||
| 279 | for (const row of rows) { | ||
| 280 | const name = row.name as string; | ||
| 281 | if (row.kind === "secret" && typeof row.secret_id === "string") { | ||
| 282 | const prefix = row.secret_prefix as string | undefined | null; | ||
| 283 | out[name] = prefix | ||
| 284 | ? { secretId: row.secret_id, prefix } | ||
| 285 | : { secretId: row.secret_id }; | ||
| 286 | } else if (row.kind === "text" && typeof row.text_value === "string") { | ||
| 287 | out[name] = row.text_value; | ||
| 288 | } | ||
| 289 | } | ||
| 290 | return out; | ||
| 179 | }; | 291 | }; |
| 180 | 292 | ||
| 181 | const toJsonRecord = (value: unknown): Record<string, unknown> => | 293 | const toJsonRecord = (value: unknown): Record<string, unknown> => |
| @@ -367,6 +479,44 @@ export interface OpenapiStore { · 185 unmodified lines | |||
| 367 | slot: string, | 479 | slot: string, |
| 368 | scope: string, | 480 | scope: string, |
| 369 | ) => Effect.Effect<void, StorageFailure>; | 481 | ) => Effect.Effect<void, StorageFailure>; |
| 482 | |||
| 483 | // --------------------------------------------------------------------- | ||
| 484 | // Usage lookups — back `usagesForSecret` / `usagesForConnection`. | ||
| 485 | // Each is one indexed SELECT against the new normalized columns. | ||
| 486 | // --------------------------------------------------------------------- | ||
| 487 | |||
| 488 | /** Source-binding rows that point at the given secret id. */ | ||
| 489 | readonly findBindingsBySecret: ( | ||
| 490 | secretId: string, | ||
| 491 | ) => Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure>; | ||
| 492 | |||
| 493 | /** Source-binding rows that point at the given connection id. */ | ||
| 494 | readonly findBindingsByConnection: ( | ||
| 495 | connectionId: string, | ||
| 496 | ) => Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure>; | ||
| 497 | |||
| 498 | /** Child rows from query_params / specFetch tables that reference the | ||
| 499 | * given secret id, tagged with the table they came from so the | ||
| 500 | * caller can produce a readable `slot` like | ||
| 501 | * `query_param:foo` or `spec_fetch_header:Authorization`. */ | ||
| 502 | readonly findChildRowsBySecret: (secretId: string) => Effect.Effect< | ||
| 503 | readonly { | ||
| 504 | readonly kind: | ||
| 505 | | "query_param" | ||
| 506 | | "spec_fetch_header" | ||
| 507 | | "spec_fetch_query_param"; | ||
| 508 | readonly source_id: string; | ||
| 509 | readonly scope_id: string; | ||
| 510 | readonly name: string; | ||
| 511 | }[], | ||
| 512 | StorageFailure | ||
| 513 | >; | ||
| 514 | |||
| 515 | /** Resolve display names for one or more `(scope_id, source_id)` pairs | ||
| 516 | * in a single round trip, keyed by `${scope_id}:${source_id}`. */ | ||
| 517 | readonly lookupSourceNames: ( | ||
| 518 | keys: readonly string[], | ||
| 519 | ) => Effect.Effect<ReadonlyMap<string, string>, StorageFailure>; | ||
| 370 | } | 520 | } |
| 371 | 521 | ||
| 372 | // --------------------------------------------------------------------------- | 522 | // --------------------------------------------------------------------------- |
| @@ -400,6 +550,25 @@ export const makeDefaultOpenapiStore = ({ · 27 unmodified lines | |||
| 400 | encodeSyntheticRowIdPart(scopeId), | 550 | encodeSyntheticRowIdPart(scopeId), |
| 401 | ].join("::"); | 551 | ].join("::"); |
| 402 | 552 | ||
| 553 | const rowToSourceBindingValue = ( | ||
| 554 | row: Record<string, unknown>, | ||
| 555 | ): OpenApiSourceBindingValue => { | ||
| 556 | const kind = row.kind as string; | ||
| 557 | if (kind === "secret" && typeof row.secret_id === "string") { | ||
| 558 | return { kind: "secret", secretId: SecretId.make(row.secret_id) }; | ||
| 559 | } | ||
| 560 | if (kind === "connection" && typeof row.connection_id === "string") { | ||
| 561 | return { | ||
| 562 | kind: "connection", | ||
| 563 | connectionId: ConnectionId.make(row.connection_id), | ||
| 564 | }; | ||
| 565 | } | ||
| 566 | // text fallback covers both well-formed text rows and any | ||
| 567 | // partial/null row that survived a malformed write — `text_value` | ||
| 568 | // defaults to "" so the type stays satisfied without a throw. | ||
| 569 | return { kind: "text", text: (row.text_value as string | null) ?? "" }; | ||
| 570 | }; | ||
| 571 | |||
| 403 | const rowToSourceBinding = ( | 572 | const rowToSourceBinding = ( |
| 404 | row: Record<string, unknown>, | 573 | row: Record<string, unknown>, |
| 405 | ): OpenApiSourceBindingRef => | 574 | ): OpenApiSourceBindingRef => |
| @@ -408,7 +577,7 @@ export const makeDefaultOpenapiStore = ({ · 2 unmodified lines | |||
| 408 | sourceScopeId: ScopeId.make(row.source_scope_id as string), | 577 | sourceScopeId: ScopeId.make(row.source_scope_id as string), |
| 409 | scopeId: ScopeId.make(row.target_scope_id as string), | 578 | scopeId: ScopeId.make(row.target_scope_id as string), |
| 410 | slot: row.slot as string, | 579 | slot: row.slot as string, |
| 411 | value: decodeSourceBindingValue(asJsonObject(row.value)), | 580 | value: rowToSourceBindingValue(row), |
| 412 | createdAt: | 581 | createdAt: |
| 413 | row.created_at instanceof Date | 582 | row.created_at instanceof Date |
| 414 | ? row.created_at | 583 | ? row.created_at |
| @@ -419,6 +588,18 @@ export const makeDefaultOpenapiStore = ({ · 4 unmodified lines | |||
| 419 | : new Date(row.updated_at as string), | 588 | : new Date(row.updated_at as string), |
| 420 | }); | 589 | }); |
| 421 | 590 | ||
| 591 | const sourceBindingValueColumns = ( | ||
| 592 | value: OpenApiSourceBindingValue, | ||
| 593 | ): { kind: string; secret_id?: string; connection_id?: string; text_value?: string } => { | ||
| 594 | if (value.kind === "secret") { | ||
| 595 | return { kind: "secret", secret_id: value.secretId }; | ||
| 596 | } | ||
| 597 | if (value.kind === "connection") { | ||
| 598 | return { kind: "connection", connection_id: value.connectionId }; | ||
| 599 | } | ||
| 600 | return { kind: "text", text_value: value.text }; | ||
| 601 | }; | ||
| 602 | |||
| 422 | const validateBindingScopes = (params: { | 603 | const validateBindingScopes = (params: { |
| 423 | readonly sourceScope: string; | 604 | readonly sourceScope: string; |
| 424 | readonly targetScope: string; | 605 | readonly targetScope: string; |
| @@ -485,39 +666,88 @@ export const makeDefaultOpenapiStore = ({ · 60 unmodified lines | |||
| 485 | return source; | 666 | return source; |
| 486 | }); | 667 | }); |
| 487 | 668 | ||
| 488 | const rowToSource = (row: Record<string, unknown>): StoredSource => { | 669 | const loadChildValueMap = ( |
| 489 | const normalizedHeaders = normalizeStoredHeaders(row.headers); | 670 | model: |
| 490 | const normalizedOAuth2 = normalizeStoredOAuth2(row.oauth2); | 671 | | "openapi_source_query_param" |
| 491 | const invocationConfig = asJsonObject(row.invocation_config); | 672 | | "openapi_source_spec_fetch_header" |
| 492 | return { | 673 | | "openapi_source_spec_fetch_query_param", |
| 493 | namespace: row.id as string, | 674 | sourceId: string, |
| 494 | scope: row.scope_id as string, | 675 | scope: string, |
| 495 | name: row.name as string, | 676 | ) => |
| 496 | config: { | 677 | adapter |
| 497 | spec: row.spec as string, | 678 | .findMany({ |
| 498 | sourceUrl: (row.source_url as string | null | undefined) ?? undefined, | 679 | model, |
| 499 | baseUrl: (row.base_url as string | null | undefined) ?? undefined, | 680 | where: [ |
| 500 | headers: normalizedHeaders.headers, | 681 | { field: "source_id", value: sourceId }, |
| 501 | queryParams: decodeHeaders(row.query_params), | 682 | { field: "scope_id", value: scope }, |
| 502 | specFetchCredentials: invocationConfig.specFetchCredentials as | 683 | ], |
| 503 | | OpenApiSpecFetchCredentials | 684 | }) |
| 504 | | undefined, | 685 | .pipe(Effect.map(childRowsToValueMap)); |
| 505 | oauth2: normalizedOAuth2.oauth2, | 686 | |
| 506 | }, | 687 | const rowToSource = ( |
| 507 | legacy: | 688 | row: Record<string, unknown>, |
| 508 | Object.keys(normalizedHeaders.legacy).length > 0 || | 689 | ): Effect.Effect<StoredSource, StorageFailure> => |
| 509 | normalizedOAuth2.legacy | 690 | Effect.gen(function* () { |
| 510 | ? { | 691 | const sourceId = row.id as string; |
| 511 | ...(Object.keys(normalizedHeaders.legacy).length > 0 | 692 | const scope = row.scope_id as string; |
| 512 | ? { headers: normalizedHeaders.legacy } | 693 | const normalizedHeaders = normalizeStoredHeaders(row.headers); |
| 694 | const normalizedOAuth2 = normalizeStoredOAuth2(row.oauth2); | ||
| 695 | |||
| 696 | const queryParams = yield* loadChildValueMap( | ||
| 697 | "openapi_source_query_param", | ||
| 698 | sourceId, | ||
| 699 | scope, | ||
| 700 | ); | ||
| 701 | const specFetchHeaders = yield* loadChildValueMap( | ||
| 702 | "openapi_source_spec_fetch_header", | ||
| 703 | sourceId, | ||
| 704 | scope, | ||
| 705 | ); | ||
| 706 | const specFetchQueryParams = yield* loadChildValueMap( | ||
| 707 | "openapi_source_spec_fetch_query_param", | ||
| 708 | sourceId, | ||
| 709 | scope, | ||
| 710 | ); | ||
| 711 | const specFetchCredentials: OpenApiSpecFetchCredentials | undefined = | ||
| 712 | Object.keys(specFetchHeaders).length === 0 && | ||
| 713 | Object.keys(specFetchQueryParams).length === 0 | ||
| 714 | ? undefined | ||
| 715 | : { | ||
| 716 | ...(Object.keys(specFetchHeaders).length > 0 | ||
| 717 | ? { headers: specFetchHeaders } | ||
| 513 | : {}), | 718 | : {}), |
| 514 | ...(normalizedOAuth2.legacy | 719 | ...(Object.keys(specFetchQueryParams).length > 0 |
| 515 | ? { oauth2: normalizedOAuth2.legacy } | 720 | ? { queryParams: specFetchQueryParams } |
| 516 | : {}), | 721 | : {}), |
| 517 | } | 722 | }; |
| 518 | : undefined, | 723 | |
| 519 | }; | 724 | return { |
| 520 | }; | 725 | namespace: sourceId, |
| 726 | scope, | ||
| 727 | name: row.name as string, | ||
| 728 | config: { | ||
| 729 | spec: row.spec as string, | ||
| 730 | sourceUrl: (row.source_url as string | null | undefined) ?? undefined, | ||
| 731 | baseUrl: (row.base_url as string | null | undefined) ?? undefined, | ||
| 732 | headers: normalizedHeaders.headers, | ||
| 733 | queryParams, | ||
| 734 | specFetchCredentials, | ||
| 735 | oauth2: normalizedOAuth2.oauth2, | ||
| 736 | }, | ||
| 737 | legacy: | ||
| 738 | Object.keys(normalizedHeaders.legacy).length > 0 || | ||
| 739 | normalizedOAuth2.legacy | ||
| 740 | ? { | ||
| 741 | ...(Object.keys(normalizedHeaders.legacy).length > 0 | ||
| 742 | ? { headers: normalizedHeaders.legacy } | ||
| 743 | : {}), | ||
| 744 | ...(normalizedOAuth2.legacy | ||
| 745 | ? { oauth2: normalizedOAuth2.legacy } | ||
| 746 | : {}), | ||
| 747 | } | ||
| 748 | : undefined, | ||
| 749 | }; | ||
| 750 | }); | ||
| 521 | 751 | ||
| 522 | const rowToOperation = (row: Record<string, unknown>): StoredOperation => ({ | 752 | const rowToOperation = (row: Record<string, unknown>): StoredOperation => ({ |
| 523 | toolId: row.id as string, | 753 | toolId: row.id as string, |
| @@ -527,6 +757,35 @@ export const makeDefaultOpenapiStore = ({ · 3 unmodified lines | |||
| 527 | ), | 757 | ), |
| 528 | }); | 758 | }); |
| 529 | 759 | ||
| 760 | // Replace the rows of one child table for a source: delete then bulk | ||
| 761 | // insert. Single helper so upsertSource and updateSourceMeta both | ||
| 762 | // funnel through the same write path. | ||
| 763 | const replaceChildRows = ( | ||
| 764 | model: | ||
| 765 | | "openapi_source_query_param" | ||
| 766 | | "openapi_source_spec_fetch_header" | ||
| 767 | | "openapi_source_spec_fetch_query_param", | ||
| 768 | sourceId: string, | ||
| 769 | scope: string, | ||
| 770 | values: Record<string, HeaderValue> | undefined, | ||
| 771 | ) => | ||
| 772 | Effect.gen(function* () { | ||
| 773 | yield* adapter.deleteMany({ | ||
| 774 | model, | ||
| 775 | where: [ | ||
| 776 | { field: "source_id", value: sourceId }, | ||
| 777 | { field: "scope_id", value: scope }, | ||
| 778 | ], | ||
| 779 | }); | ||
| 780 | const rows = valueMapToChildRows(sourceId, scope, values); | ||
| 781 | if (rows.length === 0) return; | ||
| 782 | yield* adapter.createMany({ | ||
| 783 | model, | ||
| 784 | data: rows, | ||
| 785 | forceAllowId: true, | ||
| 786 | }); | ||
| 787 | }); | ||
| 788 | |||
| 530 | const deleteSource = ( | 789 | const deleteSource = ( |
| 531 | namespace: string, | 790 | namespace: string, |
| 532 | scope: string, | 791 | scope: string, |
| @@ -540,6 +799,20 @@ export const makeDefaultOpenapiStore = ({ · 7 unmodified lines | |||
| 540 | { field: "scope_id", value: scope }, | 799 | { field: "scope_id", value: scope }, |
| 541 | ], | 800 | ], |
| 542 | }); | 801 | }); |
| 802 | // Drop every child table's rows for this source/scope. | ||
| 803 | for (const model of [ | ||
| 804 | "openapi_source_query_param", | ||
| 805 | "openapi_source_spec_fetch_header", | ||
| 806 | "openapi_source_spec_fetch_query_param", | ||
| 807 | ] as const) { | ||
| 808 | yield* adapter.deleteMany({ | ||
| 809 | model, | ||
| 810 | where: [ | ||
| 811 | { field: "source_id", value: namespace }, | ||
| 812 | { field: "scope_id", value: scope }, | ||
| 813 | ], | ||
| 814 | }); | ||
| 815 | } | ||
| 543 | yield* adapter.delete({ | 816 | yield* adapter.delete({ |
| 544 | model: "openapi_source", | 817 | model: "openapi_source", |
| 545 | where: [ | 818 | where: [ |
| @@ -587,18 +860,30 @@ export const makeDefaultOpenapiStore = ({ · 41 unmodified lines | |||
| 587 | ], | 860 | ], |
| 588 | ), | 861 | ), |
| 589 | ) as Record<string, unknown>, | 862 | ) as Record<string, unknown>, |
| 590 | query_params: input.config.queryParams, | ||
| 591 | oauth2: input.config.oauth2 | 863 | oauth2: input.config.oauth2 |
| 592 | ? toJsonRecord(encodeOAuth2SourceConfig(input.config.oauth2)) | 864 | ? toJsonRecord(encodeOAuth2SourceConfig(input.config.oauth2)) |
| 593 | : undefined, | 865 | : undefined, |
| 594 | invocation_config: { | ||
| 595 | ...(input.config.specFetchCredentials | ||
| 596 | ? { specFetchCredentials: input.config.specFetchCredentials } | ||
| 597 | : {}), | ||
| 598 | }, | ||
| 599 | }, | 866 | }, |
| 600 | forceAllowId: true, | 867 | forceAllowId: true, |
| 601 | }); | 868 | }); |
| 869 | yield* replaceChildRows( | ||
| 870 | "openapi_source_query_param", | ||
| 871 | input.namespace, | ||
| 872 | input.scope, | ||
| 873 | input.config.queryParams, | ||
| 874 | ); | ||
| 875 | yield* replaceChildRows( | ||
| 876 | "openapi_source_spec_fetch_header", | ||
| 877 | input.namespace, | ||
| 878 | input.scope, | ||
| 879 | input.config.specFetchCredentials?.headers, | ||
| 880 | ); | ||
| 881 | yield* replaceChildRows( | ||
| 882 | "openapi_source_spec_fetch_query_param", | ||
| 883 | input.namespace, | ||
| 884 | input.scope, | ||
| 885 | input.config.specFetchCredentials?.queryParams, | ||
| 886 | ); | ||
| 602 | if (operations.length > 0) { | 887 | if (operations.length > 0) { |
| 603 | yield* adapter.createMany({ | 888 | yield* adapter.createMany({ |
| 604 | model: "openapi_operation", | 889 | model: "openapi_operation", |
| @@ -623,7 +908,7 @@ export const makeDefaultOpenapiStore = ({ · 18 unmodified lines | |||
| 623 | ], | 908 | ], |
| 624 | }); | 909 | }); |
| 625 | if (!existingRow) return; | 910 | if (!existingRow) return; |
| 626 | const existing = rowToSource(existingRow); | 911 | const existing = yield* rowToSource(existingRow); |
| 627 | 912 | ||
| 628 | const nextName = patch.name?.trim() || existing.name; | 913 | const nextName = patch.name?.trim() || existing.name; |
| 629 | const nextBaseUrl = | 914 | const nextBaseUrl = |
| @@ -632,10 +917,6 @@ export const makeDefaultOpenapiStore = ({ · 2 unmodified lines | |||
| 632 | patch.headers !== undefined | 917 | patch.headers !== undefined |
| 633 | ? patch.headers | 918 | ? patch.headers |
| 634 | : (existing.config.headers ?? {}); | 919 | : (existing.config.headers ?? {}); |
| 635 | const nextQueryParams = | ||
| 636 | patch.queryParams !== undefined | ||
| 637 | ? patch.queryParams | ||
| 638 | : (existing.config.queryParams ?? {}); | ||
| 639 | const nextOAuth2 = | 920 | const nextOAuth2 = |
| 640 | patch.oauth2 !== undefined ? patch.oauth2 : existing.config.oauth2; | 921 | patch.oauth2 !== undefined ? patch.oauth2 : existing.config.oauth2; |
| 641 | 922 | ||
| @@ -660,30 +941,41 @@ export const makeDefaultOpenapiStore = ({ · 18 unmodified lines | |||
| 660 | }, | 941 | }, |
| 661 | ]), | 942 | ]), |
| 662 | ) as Record<string, unknown>, | 943 | ) as Record<string, unknown>, |
| 663 | query_params: nextQueryParams, | ||
| 664 | oauth2: nextOAuth2 | 944 | oauth2: nextOAuth2 |
| 665 | ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2)) | 945 | ? toJsonRecord(encodeOAuth2SourceConfig(nextOAuth2)) |
| 666 | : undefined, | 946 | : undefined, |
| 667 | invocation_config: asJsonObject(existingRow.invocation_config), | ||
| 668 | }, | 947 | }, |
| 669 | }); | 948 | }); |
| 949 | if (patch.queryParams !== undefined) { | ||
| 950 | yield* replaceChildRows( | ||
| 951 | "openapi_source_query_param", | ||
| 952 | namespace, | ||
| 953 | scope, | ||
| 954 | patch.queryParams, | ||
| 955 | ); | ||
| 956 | } | ||
| 670 | }), | 957 | }), |
| 671 | 958 | ||
| 672 | getSource: (namespace, scope) => | 959 | getSource: (namespace, scope) => |
| 673 | adapter | 960 | Effect.gen(function* () { |
| 674 | .findOne({ | 961 | const row = yield* adapter.findOne({ |
| 675 | model: "openapi_source", | 962 | model: "openapi_source", |
| 676 | where: [ | 963 | where: [ |
| 677 | { field: "id", value: namespace }, | 964 | { field: "id", value: namespace }, |
| 678 | { field: "scope_id", value: scope }, | 965 | { field: "scope_id", value: scope }, |
| 679 | ], | 966 | ], |
| 680 | }) | 967 | }); |
| 681 | .pipe(Effect.map((row) => (row ? rowToSource(row) : null))), | 968 | if (!row) return null; |
| 969 | return yield* rowToSource(row); | ||
| 970 | }), | ||
| 682 | 971 | ||
| 683 | listSources: () => | 972 | listSources: () => |
| 684 | adapter | 973 | Effect.gen(function* () { |
| 685 | .findMany({ model: "openapi_source" }) | 974 | const rows = yield* adapter.findMany({ model: "openapi_source" }); |
| 686 | .pipe(Effect.map((rows) => rows.map(rowToSource))), | 975 | return yield* Effect.forEach(rows, rowToSource, { |
| 976 | concurrency: "unbounded", | ||
| 977 | }); | ||
| 978 | }), | ||
| 687 | 979 | ||
| 688 | getOperationByToolId: (toolId, scope) => | 980 | getOperationByToolId: (toolId, scope) => |
| 689 | adapter | 981 | adapter |
| @@ -779,6 +1071,7 @@ export const makeDefaultOpenapiStore = ({ · 89 unmodified lines | |||
| 779 | input.scope as string, | 1071 | input.scope as string, |
| 780 | ); | 1072 | ); |
| 781 | const now = new Date(); | 1073 | const now = new Date(); |
| 1074 | const valueColumns = sourceBindingValueColumns(input.value); | ||
| 782 | yield* adapter.delete({ | 1075 | yield* adapter.delete({ |
| 783 | model: "openapi_source_binding", | 1076 | model: "openapi_source_binding", |
| 784 | where: [{ field: "id", value: id }], | 1077 | where: [{ field: "id", value: id }], |
| @@ -791,7 +1084,7 @@ export const makeDefaultOpenapiStore = ({ · 6 unmodified lines | |||
| 791 | source_scope_id: input.sourceScope as string, | 1084 | source_scope_id: input.sourceScope as string, |
| 792 | target_scope_id: input.scope as string, | 1085 | target_scope_id: input.scope as string, |
| 793 | slot: input.slot, | 1086 | slot: input.slot, |
| 794 | value: toJsonRecord(encodeSourceBindingValue(input.value)), | 1087 | ...valueColumns, |
| 795 | created_at: now, | 1088 | created_at: now, |
| 796 | updated_at: now, | 1089 | updated_at: now, |
| 797 | }, | 1090 | }, |
| @@ -825,5 +1118,64 @@ export const makeDefaultOpenapiStore = ({ · 27 unmodified lines | |||
| 825 | ], | 1118 | ], |
| 826 | }); | 1119 | }); |
| 827 | }), | 1120 | }), |
| 1121 | |||
| 1122 | findBindingsBySecret: (secretId) => | ||
| 1123 | adapter | ||
| 1124 | .findMany({ | ||
| 1125 | model: "openapi_source_binding", | ||
| 1126 | where: [{ field: "secret_id", value: secretId }], | ||
| 1127 | }) | ||
| 1128 | .pipe(Effect.map((rows) => rows.map(rowToSourceBinding))), | ||
| 1129 | |||
| 1130 | findBindingsByConnection: (connectionId) => | ||
| 1131 | adapter | ||
| 1132 | .findMany({ | ||
| 1133 | model: "openapi_source_binding", | ||
| 1134 | where: [{ field: "connection_id", value: connectionId }], | ||
| 1135 | }) | ||
| 1136 | .pipe(Effect.map((rows) => rows.map(rowToSourceBinding))), | ||
| 1137 | |||
| 1138 | findChildRowsBySecret: (secretId) => | ||
| 1139 | Effect.gen(function* () { | ||
| 1140 | const tables = [ | ||
| 1141 | { model: "openapi_source_query_param" as const, kind: "query_param" as const }, | ||
| 1142 | { model: "openapi_source_spec_fetch_header" as const, kind: "spec_fetch_header" as const }, | ||
| 1143 | { model: "openapi_source_spec_fetch_query_param" as const, kind: "spec_fetch_query_param" as const }, | ||
| 1144 | ]; | ||
| 1145 | const perTable = yield* Effect.forEach( | ||
| 1146 | tables, | ||
| 1147 | (t) => | ||
| 1148 | adapter | ||
| 1149 | .findMany({ | ||
| 1150 | model: t.model, | ||
| 1151 | where: [{ field: "secret_id", value: secretId }], | ||
| 1152 | }) | ||
| 1153 | .pipe( | ||
| 1154 | Effect.map((rows) => | ||
| 1155 | rows.map((r) => ({ | ||
| 1156 | kind: t.kind, | ||
| 1157 | source_id: r.source_id as string, | ||
| 1158 | scope_id: r.scope_id as string, | ||
| 1159 | name: r.name as string, | ||
| 1160 | })), | ||
| 1161 | ), | ||
| 1162 | ), | ||
| 1163 | { concurrency: "unbounded" }, | ||
| 1164 | ); | ||
| 1165 | return perTable.flat(); | ||
| 1166 | }), | ||
| 1167 | |||
| 1168 | lookupSourceNames: (keys) => | ||
| 1169 | Effect.gen(function* () { | ||
| 1170 | if (keys.length === 0) return new Map<string, string>(); | ||
| 1171 | const rows = yield* adapter.findMany({ model: "openapi_source" }); | ||
| 1172 | const requested = new Set(keys); | ||
| 1173 | const out = new Map<string, string>(); | ||
| 1174 | for (const r of rows) { | ||
| 1175 | const key = `${r.scope_id as string}:${r.id as string}`; | ||
| 1176 | if (requested.has(key)) out.set(key, r.name as string); | ||
| 1177 | } | ||
| 1178 | return out; | ||
| 1179 | }), | ||
| 828 | }; | 1180 | }; |
| 829 | }; | 1181 | }; |
| @@ -1,4 +1,10 @@ | |||
| 1 | import { PolicyId, type ScopeId, type ToolId, type SecretId } from "@executor-js/sdk"; | 1 | import { |
| 2 | PolicyId, | ||
| 3 | type ConnectionId, | ||
| 4 | type ScopeId, | ||
| 5 | type SecretId, | ||
| 6 | type ToolId, | ||
| 7 | } from "@executor-js/sdk"; | ||
| 2 | import * as Atom from "effect/unstable/reactivity/Atom"; | 8 | import * as Atom from "effect/unstable/reactivity/Atom"; |
| 3 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; | 9 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; |
| 4 | 10 | ||
| @@ -72,6 +78,34 @@ export const connectionsAtom = (scopeId: ScopeId) => · 67 unmodified lines | |||
| 72 | reactivityKeys: [ReactivityKey.connections], | 78 | reactivityKeys: [ReactivityKey.connections], |
| 73 | }); | 79 | }); |
| 74 | 80 | ||
| 81 | export const secretUsagesAtom = (scopeId: ScopeId, secretId: SecretId) => | ||
| 82 | ExecutorApiClient.query("secrets", "usages", { | ||
| 83 | params: { scopeId, secretId }, | ||
| 84 | timeToLive: "30 seconds", | ||
| 85 | // Refresh whenever any source / connection / secret changes — adding | ||
| 86 | // an oauth source pulls in a new connection-secret link and we want | ||
| 87 | // the usage list to reflect it. | ||
| 88 | reactivityKeys: [ | ||
| 89 | ReactivityKey.secrets, | ||
| 90 | ReactivityKey.sources, | ||
| 91 | ReactivityKey.connections, | ||
| 92 | ], | ||
| 93 | }); | ||
| 94 | |||
| 95 | export const connectionUsagesAtom = ( | ||
| 96 | scopeId: ScopeId, | ||
| 97 | connectionId: ConnectionId, | ||
| 98 | ) => | ||
| 99 | ExecutorApiClient.query("connections", "usages", { | ||
| 100 | params: { scopeId, connectionId }, | ||
| 101 | timeToLive: "30 seconds", | ||
| 102 | reactivityKeys: [ | ||
| 103 | ReactivityKey.connections, | ||
| 104 | ReactivityKey.sources, | ||
| 105 | ReactivityKey.secrets, | ||
| 106 | ], | ||
| 107 | }); | ||
| 108 | |||
| 75 | export const policiesAtom = (scopeId: ScopeId) => | 109 | export const policiesAtom = (scopeId: ScopeId) => |
| 76 | ExecutorApiClient.query("policies", "list", { | 110 | ExecutorApiClient.query("policies", "list", { |
| 77 | params: { scopeId }, | 111 | params: { scopeId }, |
| @@ -1,9 +1,10 @@ | |||
| 1 | import { useAtomSet } from "@effect/atom-react"; | 1 | import { Suspense } from "react"; |
| 2 | import { useAtomValue, useAtomSet } from "@effect/atom-react"; | ||
| 2 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; | 3 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; |
| 3 | import { ConnectionId } from "@executor-js/sdk"; | 4 | import { ConnectionId, type ScopeId } from "@executor-js/sdk"; |
| 4 | import { toast } from "sonner"; | 5 | import { toast } from "sonner"; |
| 5 | 6 | ||
| 6 | import { removeConnection } from "../api/atoms"; | 7 | import { connectionUsagesAtom, removeConnection } from "../api/atoms"; |
| 7 | import { useConnectionsWithPendingRemovals, usePendingConnectionRemovals } from "../api/optimistic"; | 8 | import { useConnectionsWithPendingRemovals, usePendingConnectionRemovals } from "../api/optimistic"; |
| 8 | import { connectionWriteKeys } from "../api/reactivity-keys"; | 9 | import { connectionWriteKeys } from "../api/reactivity-keys"; |
| 9 | import { useScope, useScopeStack } from "../hooks/use-scope"; | 10 | import { useScope, useScopeStack } from "../hooks/use-scope"; |
| @@ -50,11 +51,45 @@ const connectionScopeLabel = ( · 40 unmodified lines | |||
| 50 | return "Scoped"; | 51 | return "Scoped"; |
| 51 | }; | 52 | }; |
| 52 | 53 | ||
| 54 | // --------------------------------------------------------------------------- | ||
| 55 | // Used-by footer — same shape as the secrets page. Returns null when a | ||
| 56 | // connection isn't referenced anywhere so newly-created connections | ||
| 57 | // don't get a stray "Used by 0" line before any source binds to them. | ||
| 58 | // --------------------------------------------------------------------------- | ||
| 59 | |||
| 60 | function ConnectionUsageFooter(props: { | ||
| 61 | scopeId: ScopeId; | ||
| 62 | connectionId: ConnectionId; | ||
| 63 | }) { | ||
| 64 | const usages = useAtomValue( | ||
| 65 | connectionUsagesAtom(props.scopeId, props.connectionId), | ||
| 66 | ); | ||
| 67 | return AsyncResult.match(usages, { | ||
| 68 | onInitial: () => null, | ||
| 69 | onFailure: () => null, | ||
| 70 | onSuccess: ({ value }) => { | ||
| 71 | if (value.length === 0) return null; | ||
| 72 | const labels = value | ||
| 73 | .map((u) => u.ownerName ?? u.ownerId) | ||
| 74 | .filter((s, i, a) => a.indexOf(s) === i); | ||
| 75 | const visible = labels.slice(0, 3); | ||
| 76 | const hidden = labels.length - visible.length; | ||
| 77 | return ( | ||
| 78 | <CardStackEntryDescription className="mt-1 text-xs text-muted-foreground"> | ||
| 79 | Used by {visible.join(", ")} | ||
| 80 | {hidden > 0 ? ` +${hidden} more` : ""} | ||
| 81 | </CardStackEntryDescription> | ||
| 82 | ); | ||
| 83 | }, | ||
| 84 | }); | ||
| 85 | } | ||
| 86 | |||
| 53 | // --------------------------------------------------------------------------- | 87 | // --------------------------------------------------------------------------- |
| 54 | // Connection row | 88 | // Connection row |
| 55 | // --------------------------------------------------------------------------- | 89 | // --------------------------------------------------------------------------- |
| 56 | 90 | ||
| 57 | function ConnectionRow(props: { | 91 | function ConnectionRow(props: { |
| 92 | scopeId: ScopeId; | ||
| 58 | connection: { | 93 | connection: { |
| 59 | id: string; | 94 | id: string; |
| 60 | scopeId: string; | 95 | scopeId: string; |
| @@ -80,6 +115,12 @@ function ConnectionRow(props: { · 19 unmodified lines | |||
| 80 | <CardStackEntryDescription className="text-xs text-muted-foreground"> | 115 | <CardStackEntryDescription className="text-xs text-muted-foreground"> |
| 81 | {displayProvider(connection.provider)} | 116 | {displayProvider(connection.provider)} |
| 82 | </CardStackEntryDescription> | 117 | </CardStackEntryDescription> |
| 118 | <Suspense fallback={null}> | ||
| 119 | <ConnectionUsageFooter | ||
| 120 | scopeId={props.scopeId} | ||
| 121 | connectionId={ConnectionId.make(connection.id)} | ||
| 122 | /> | ||
| 123 | </Suspense> | ||
| 83 | </CardStackEntryContent> | 124 | </CardStackEntryContent> |
| 84 | <CardStackEntryActions> | 125 | <CardStackEntryActions> |
| 85 | <Badge variant="outline">{scopeLabel}</Badge> | 126 | <Badge variant="outline">{scopeLabel}</Badge> |
| @@ -185,6 +226,7 @@ export function ConnectionsPage() { · 99 unmodified lines | |||
| 185 | }) => ( | 226 | }) => ( |
| 186 | <ConnectionRow | 227 | <ConnectionRow |
| 187 | key={c.id} | 228 | key={c.id} |
| 229 | scopeId={scopeId} | ||
| 188 | connection={{ | 230 | connection={{ |
| 189 | id: c.id, | 231 | id: c.id, |
| 190 | scopeId: c.scopeId, | 232 | scopeId: c.scopeId, |
| @@ -1,10 +1,10 @@ | |||
| 1 | import { useMemo, useState, Suspense } from "react"; | 1 | import { useMemo, useState, Suspense } from "react"; |
| 2 | import { useAtomValue, useAtomSet } from "@effect/atom-react"; | 2 | import { useAtomValue, useAtomSet } from "@effect/atom-react"; |
| 3 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; | 3 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; |
| 4 | import { secretsAtom, removeSecret } from "../api/atoms"; | 4 | import { secretsAtom, secretUsagesAtom, removeSecret } from "../api/atoms"; |
| 5 | import { secretWriteKeys } from "../api/reactivity-keys"; | 5 | import { secretWriteKeys } from "../api/reactivity-keys"; |
| 6 | import { useSecretProviderPlugins } from "@executor-js/sdk/client"; | 6 | import { useSecretProviderPlugins } from "@executor-js/sdk/client"; |
| 7 | import { SecretId } from "@executor-js/sdk"; | 7 | import { SecretId, type ScopeId } from "@executor-js/sdk"; |
| 8 | import { SecretForm } from "../plugins/secret-form"; | 8 | import { SecretForm } from "../plugins/secret-form"; |
| 9 | import { useScope } from "../hooks/use-scope"; | 9 | import { useScope } from "../hooks/use-scope"; |
| 10 | import { | 10 | import { |
| @@ -122,11 +122,44 @@ function AddSecretDialogContent(props: { · 111 unmodified lines | |||
| 122 | ); | 122 | ); |
| 123 | } | 123 | } |
| 124 | 124 | ||
| 125 | // --------------------------------------------------------------------------- | ||
| 126 | // Used-by footer — fetched per-secret. Keeps the list compact: shows the | ||
| 127 | // count plus the first few owner names, with a "+N more" tail. Empty | ||
| 128 | // state collapses to nothing so secrets that aren't referenced anywhere | ||
| 129 | // don't get a noisy "Used by 0" line. | ||
| 130 | // --------------------------------------------------------------------------- | ||
| 131 | |||
| 132 | function SecretUsageFooter(props: { | ||
| 133 | scopeId: ScopeId; | ||
| 134 | secretId: SecretId; | ||
| 135 | }) { | ||
| 136 | const usages = useAtomValue(secretUsagesAtom(props.scopeId, props.secretId)); | ||
| 137 | return AsyncResult.match(usages, { | ||
| 138 | onInitial: () => null, | ||
| 139 | onFailure: () => null, | ||
| 140 | onSuccess: ({ value }) => { | ||
| 141 | if (value.length === 0) return null; | ||
| 142 | const labels = value | ||
| 143 | .map((u) => u.ownerName ?? u.ownerId) | ||
| 144 | .filter((s, i, a) => a.indexOf(s) === i); | ||
| 145 | const visible = labels.slice(0, 3); | ||
| 146 | const hidden = labels.length - visible.length; | ||
| 147 | return ( | ||
| 148 | <CardStackEntryDescription className="mt-1 text-xs text-muted-foreground"> | ||
| 149 | Used by {visible.join(", ")} | ||
| 150 | {hidden > 0 ? ` +${hidden} more` : ""} | ||
| 151 | </CardStackEntryDescription> | ||
| 152 | ); | ||
| 153 | }, | ||
| 154 | }); | ||
| 155 | } | ||
| 156 | |||
| 125 | // --------------------------------------------------------------------------- | 157 | // --------------------------------------------------------------------------- |
| 126 | // Secret row | 158 | // Secret row |
| 127 | // --------------------------------------------------------------------------- | 159 | // --------------------------------------------------------------------------- |
| 128 | 160 | ||
| 129 | function SecretRow(props: { | 161 | function SecretRow(props: { |
| 162 | scopeId: ScopeId; | ||
| 130 | showProvider: boolean; | 163 | showProvider: boolean; |
| 131 | secret: { id: string; name: string; provider?: string }; | 164 | secret: { id: string; name: string; provider?: string }; |
| 132 | onRemove: () => void; | 165 | onRemove: () => void; |
| @@ -147,6 +180,12 @@ function SecretRow(props: { · 14 unmodified lines | |||
| 147 | {secret.id} | 180 | {secret.id} |
| 148 | </span> | 181 | </span> |
| 149 | </CardStackEntryTitle> | 182 | </CardStackEntryTitle> |
| 183 | <Suspense fallback={null}> | ||
| 184 | <SecretUsageFooter | ||
| 185 | scopeId={props.scopeId} | ||
| 186 | secretId={SecretId.make(secret.id)} | ||
| 187 | /> | ||
| 188 | </Suspense> | ||
| 150 | </CardStackEntryContent> | 189 | </CardStackEntryContent> |
| 151 | <CardStackEntryActions> | 190 | <CardStackEntryActions> |
| 152 | {showProvider && secret.provider && <Badge variant="outline">{secret.provider}</Badge>} | 191 | {showProvider && secret.provider && <Badge variant="outline">{secret.provider}</Badge>} |
| @@ -306,6 +345,7 @@ export function SecretsPage(props: { · 153 unmodified lines | |||
| 306 | }) => ( | 345 | }) => ( |
| 307 | <SecretRow | 346 | <SecretRow |
| 308 | key={s.id} | 347 | key={s.id} |
| 348 | scopeId={scopeId} | ||
| 309 | showProvider={showProviderInfo} | 349 | showProvider={showProviderInfo} |
| 310 | secret={{ | 350 | secret={{ |
| 311 | id: s.id, | 351 | id: s.id, |