import { Effect, Option, Predicate, Schema } from "effect"; import { type Owner, type PluginStorageEntry, type StorageDeps, type StorageFailure, } from "@executor-js/sdk/core"; import { OperationBinding } from "./types"; // --------------------------------------------------------------------------- // Per-integration operation bindings. // // In v2 the integration's GraphQL operations are derived from introspection and // are identical for every connection, so the plugin store keeps one set of // operation bindings per integration slug — `invokeTool` reads them back to // rebuild the request. Operations are catalog-level metadata, so they live under // the shared `owner: "org"` partition. // --------------------------------------------------------------------------- const CATALOG_OWNER: Owner = "org"; const OPERATION_COLLECTION = "operation"; export interface StoredOperation { /** The tool's leaf name, e.g. `query.hello`. */ readonly toolName: string; /** The owning integration slug. */ readonly integration: string; readonly binding: OperationBinding; } const OperationBindingFromJsonString = Schema.fromJsonString(OperationBinding); const decodeOperationBindingFromJsonString = Schema.decodeUnknownSync( OperationBindingFromJsonString, ); const decodeOperationBinding = Schema.decodeUnknownSync(OperationBinding); const encodeBinding = Schema.encodeSync(OperationBinding); const decodeBinding = (value: unknown): OperationBinding => { if (typeof value === "string") return decodeOperationBindingFromJsonString(value); return decodeOperationBinding(value); }; const toJsonRecord = (value: unknown): Record => value as Record; const OperationStorage = Schema.Struct({ toolName: Schema.String, integration: Schema.String, binding: Schema.Unknown, }); const decodeOperationStorage = Schema.decodeUnknownOption(OperationStorage); const operationKey = (integration: string, toolName: string): string => `${integration}.${toolName}`; const operationData = (operation: StoredOperation) => ({ toolName: operation.toolName, integration: operation.integration, binding: toJsonRecord(encodeBinding(operation.binding)), }); const rowToOperation = (row: PluginStorageEntry): StoredOperation | null => { const decoded = decodeOperationStorage(row.data); if (Option.isNone(decoded)) return null; const operation = decoded.value; return { toolName: operation.toolName, integration: operation.integration, binding: decodeBinding(operation.binding), }; }; /** Blob key for an introspection snapshot's content hash. Content-addressed * so re-puts are idempotent and identical schemas share one blob per * partition. */ export const introspectionBlobKey = (introspectionHash: string): string => `introspection/${introspectionHash}`; export interface GraphqlStore { /** Replace the stored operation bindings for an integration. */ readonly replaceOperations: ( integration: string, operations: readonly StoredOperation[], ) => Effect.Effect; readonly getOperation: ( integration: string, toolName: string, ) => Effect.Effect; readonly listOperations: ( integration: string, ) => Effect.Effect; readonly removeOperations: (integration: string) => Effect.Effect; /** Persist an introspection JSON snapshot under its content hash. Org-owned * and content-addressed; never removed on integration removal because * another integration in the tenant may share the hash. */ readonly putIntrospection: ( introspectionHash: string, introspectionJson: string, ) => Effect.Effect; /** Load an introspection snapshot by content hash; null when absent. */ readonly getIntrospection: ( introspectionHash: string, ) => Effect.Effect; } export const makeDefaultGraphqlStore = ({ pluginStorage, blobs }: StorageDeps): GraphqlStore => { const listOperationRows = (integration: string) => pluginStorage .list({ collection: OPERATION_COLLECTION, keyPrefix: `${integration}.`, }) .pipe( Effect.map((rows) => rows.filter((row) => rowToOperation(row)?.integration === integration), ), ); const removeOperations = (integration: string) => Effect.gen(function* () { const rows = yield* listOperationRows(integration); for (const row of rows) { yield* pluginStorage.remove({ owner: CATALOG_OWNER, collection: OPERATION_COLLECTION, key: row.key, }); } }); return { replaceOperations: (integration, operations) => Effect.gen(function* () { yield* removeOperations(integration); for (const operation of operations) { yield* pluginStorage.put({ owner: CATALOG_OWNER, collection: OPERATION_COLLECTION, key: operationKey(integration, operation.toolName), data: operationData(operation), }); } }), getOperation: (integration, toolName) => pluginStorage .get({ collection: OPERATION_COLLECTION, key: operationKey(integration, toolName) }) .pipe(Effect.map((row) => (row ? rowToOperation(row) : null))), listOperations: (integration) => listOperationRows(integration).pipe( Effect.map((rows) => rows.map(rowToOperation).filter(Predicate.isNotNull)), ), removeOperations, putIntrospection: (introspectionHash, introspectionJson) => blobs.put(introspectionBlobKey(introspectionHash), introspectionJson, { owner: CATALOG_OWNER, }), getIntrospection: (introspectionHash) => blobs.get(introspectionBlobKey(introspectionHash)), }; };