refactor(search): persist provider in global config
This commit is contained in:
parent
48f8b5de98
commit
ccad83549d
38 changed files with 736 additions and 589 deletions
|
|
@ -1,4 +1,5 @@
|
|||
export * as Config from "./config"
|
||||
export * as ConfigGlobal from "./config/global"
|
||||
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import path from "path"
|
||||
|
|
|
|||
50
packages/core/src/config/global.ts
Normal file
50
packages/core/src/config/global.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export * as ConfigGlobal from "./global"
|
||||
|
||||
import { randomUUID } from "node:crypto"
|
||||
import path from "node:path"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { applyEdits, modify, type JSONPath } from "jsonc-parser"
|
||||
import { makeGlobalNode } from "../effect/app-node"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { Global } from "../global"
|
||||
import { EffectFlock } from "../util/effect-flock"
|
||||
|
||||
export interface Interface {
|
||||
readonly update: (path: JSONPath, value: unknown) => Effect.Effect<void, FSUtil.Error | EffectFlock.LockError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ConfigGlobal") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const global = yield* Global.Service
|
||||
const flock = yield* EffectFlock.Service
|
||||
|
||||
return Service.of({
|
||||
update: Effect.fn("ConfigGlobal.update")(function* (jsonPath, value) {
|
||||
yield* flock.withLock(
|
||||
Effect.gen(function* () {
|
||||
const existing = yield* Effect.filter(
|
||||
["opencode.jsonc", "opencode.json"].map((name) => path.join(global.config, name)),
|
||||
fs.existsSafe,
|
||||
)
|
||||
const filepath = existing[0] ?? path.join(global.config, "opencode.json")
|
||||
const text = (yield* fs.readFileStringSafe(filepath)) ?? "{}"
|
||||
const next = applyEdits(
|
||||
text,
|
||||
modify(text, jsonPath, value, { formattingOptions: { tabSize: 2, insertSpaces: true } }),
|
||||
)
|
||||
const temp = `${filepath}.${randomUUID()}.tmp`
|
||||
yield* fs.writeWithDirs(temp, next)
|
||||
yield* fs.rename(temp, filepath).pipe(Effect.ensuring(fs.remove(temp).pipe(Effect.ignore)))
|
||||
}),
|
||||
"global-config",
|
||||
)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer, deps: [EffectFlock.node, FSUtil.node, Global.node] })
|
||||
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
|
|
@ -46,7 +46,6 @@ export const migrations = (
|
|||
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
|
||||
import("./migration/20260703200000_reset_v2_session_events"),
|
||||
import("./migration/20260705180000_rename_instructions"),
|
||||
import("./migration/20260706133920_integration-search"),
|
||||
import("./migration/20260706223930_add-session-fork"),
|
||||
import("./migration/20260707010146_durable_session_inbox"),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260706133920_integration-search",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`integration_capability\` (
|
||||
\`capability\` text PRIMARY KEY,
|
||||
\`integration_id\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -87,14 +87,6 @@ export default {
|
|||
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`integration_capability\` (
|
||||
\`capability\` text PRIMARY KEY,
|
||||
\`integration_id\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`permission\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
export * as Integration from "./integration"
|
||||
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import { eq } from "drizzle-orm"
|
||||
import {
|
||||
Cause,
|
||||
Clock,
|
||||
|
|
@ -19,11 +18,9 @@ import {
|
|||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { Search } from "@opencode-ai/schema/search"
|
||||
import { Credential } from "./credential"
|
||||
import { Database } from "./database/database"
|
||||
import { State } from "./state"
|
||||
import { EventV2 } from "./event"
|
||||
import { IntegrationConnection } from "./integration/connection"
|
||||
import { IntegrationCapabilityTable } from "./integration/sql"
|
||||
|
||||
export const ID = Integration.ID
|
||||
export type ID = Integration.ID
|
||||
|
|
@ -64,12 +61,6 @@ export type Info = Integration.Info
|
|||
export const Inputs = Integration.Inputs
|
||||
export type Inputs = Integration.Inputs
|
||||
|
||||
export const SearchCapability = Integration.SearchCapability
|
||||
export type SearchCapability = Omit<Integration.SearchCapability, "selected">
|
||||
|
||||
export const Capability = Integration.Capability
|
||||
export type Capability = Integration.Capability
|
||||
|
||||
export type OAuthAuthorization = {
|
||||
readonly url: string
|
||||
readonly instructions: string
|
||||
|
|
@ -106,7 +97,7 @@ export type Implementation = OAuthImplementation | KeyImplementation | EnvImplem
|
|||
|
||||
export interface SearchImplementation {
|
||||
readonly integrationID: ID
|
||||
readonly capability: SearchCapability
|
||||
readonly connection: Integration.Search["connection"]
|
||||
readonly execute: (
|
||||
input: Search.Input,
|
||||
context: { readonly credential?: Credential.Value; readonly sessionID?: string },
|
||||
|
|
@ -155,12 +146,10 @@ export type Draft = {
|
|||
update: (implementation: Implementation) => void
|
||||
remove: (integrationID: ID, method: Method) => void
|
||||
}
|
||||
capability: {
|
||||
search: {
|
||||
list: () => readonly SearchImplementation[]
|
||||
update: (implementation: SearchImplementation) => void
|
||||
remove: (integrationID: ID) => void
|
||||
}
|
||||
search: {
|
||||
list: () => readonly SearchImplementation[]
|
||||
update: (implementation: SearchImplementation) => void
|
||||
remove: (integrationID: ID) => void
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,13 +207,9 @@ export interface Interface extends State.Transformable<Draft> {
|
|||
/** Cancels an attempt and releases its resources. */
|
||||
readonly cancel: (attemptID: AttemptID) => Effect.Effect<void>
|
||||
}
|
||||
readonly capability: {
|
||||
readonly search: {
|
||||
readonly list: () => Effect.Effect<readonly SearchImplementation[]>
|
||||
readonly get: (integrationID: ID) => Effect.Effect<SearchImplementation | undefined>
|
||||
readonly selected: () => Effect.Effect<ID | undefined>
|
||||
readonly select: (integrationID: ID) => Effect.Effect<void>
|
||||
}
|
||||
readonly search: {
|
||||
readonly list: () => Effect.Effect<readonly SearchImplementation[]>
|
||||
readonly get: (integrationID: ID) => Effect.Effect<SearchImplementation | undefined>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +242,6 @@ const layer = Layer.effect(
|
|||
Service,
|
||||
Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
const { db } = yield* Database.Service
|
||||
const events = yield* EventV2.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
|
||||
|
|
@ -317,30 +301,28 @@ const layer = Layer.effect(
|
|||
if (method.type === "oauth") current.implementations.delete(method.id)
|
||||
},
|
||||
},
|
||||
capability: {
|
||||
search: {
|
||||
list: () =>
|
||||
Array.from(draft.integrations.values()).flatMap((entry) =>
|
||||
entry.search ? [entry.search as SearchImplementation] : [],
|
||||
),
|
||||
update: (implementation) => {
|
||||
const current = draft.integrations.get(implementation.integrationID) ?? {
|
||||
ref: {
|
||||
id: implementation.integrationID,
|
||||
name: implementation.integrationID,
|
||||
},
|
||||
methods: [],
|
||||
implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
|
||||
}
|
||||
if (!draft.integrations.has(implementation.integrationID)) {
|
||||
draft.integrations.set(implementation.integrationID, current)
|
||||
}
|
||||
current.search = implementation as Types.DeepMutable<SearchImplementation>
|
||||
},
|
||||
remove: (integrationID) => {
|
||||
const current = draft.integrations.get(integrationID)
|
||||
if (current) delete current.search
|
||||
},
|
||||
search: {
|
||||
list: () =>
|
||||
Array.from(draft.integrations.values()).flatMap((entry) =>
|
||||
entry.search ? [entry.search as SearchImplementation] : [],
|
||||
),
|
||||
update: (implementation) => {
|
||||
const current = draft.integrations.get(implementation.integrationID) ?? {
|
||||
ref: {
|
||||
id: implementation.integrationID,
|
||||
name: implementation.integrationID,
|
||||
},
|
||||
methods: [],
|
||||
implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
|
||||
}
|
||||
if (!draft.integrations.has(implementation.integrationID)) {
|
||||
draft.integrations.set(implementation.integrationID, current)
|
||||
}
|
||||
current.search = implementation as Types.DeepMutable<SearchImplementation>
|
||||
},
|
||||
remove: (integrationID) => {
|
||||
const current = draft.integrations.get(integrationID)
|
||||
if (current) delete current.search
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
@ -362,24 +344,15 @@ const layer = Layer.effect(
|
|||
return [...credentials, ...env]
|
||||
}
|
||||
|
||||
const project = (entry: Entry, connections: IntegrationConnection.Info[], selectedSearch: ID | undefined) =>
|
||||
const project = (entry: Entry, connections: IntegrationConnection.Info[]) =>
|
||||
new Info({
|
||||
id: entry.ref.id,
|
||||
name: entry.ref.name,
|
||||
methods: entry.methods,
|
||||
capabilities: entry.search ? [{ ...entry.search.capability, selected: entry.ref.id === selectedSearch }] : [],
|
||||
search: entry.search ? { connection: entry.search.connection } : undefined,
|
||||
connections,
|
||||
})
|
||||
|
||||
const selectedSearch = Effect.fn("Integration.capability.search.selected")(function* () {
|
||||
return (yield* db
|
||||
.select({ integrationID: IntegrationCapabilityTable.integration_id })
|
||||
.from(IntegrationCapabilityTable)
|
||||
.where(eq(IntegrationCapabilityTable.capability, "search"))
|
||||
.get()
|
||||
.pipe(Effect.orDie))?.integrationID
|
||||
})
|
||||
|
||||
const authorize = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause })))
|
||||
|
||||
|
|
@ -441,13 +414,12 @@ const layer = Layer.effect(
|
|||
get: Effect.fn("Integration.get")(function* (id) {
|
||||
const entry = state.get().integrations.get(id)
|
||||
if (!entry) return undefined
|
||||
return project(entry, resolveConnections(entry, yield* credentials.list(id)), yield* selectedSearch())
|
||||
return project(entry, resolveConnections(entry, yield* credentials.list(id)))
|
||||
}),
|
||||
list: Effect.fn("Integration.list")(function* () {
|
||||
const saved = Map.groupBy(yield* credentials.all(), (credential) => credential.integrationID)
|
||||
const selected = yield* selectedSearch()
|
||||
return Array.from(state.get().integrations.values(), (entry) =>
|
||||
project(entry, resolveConnections(entry, saved.get(entry.ref.id) ?? []), selected),
|
||||
project(entry, resolveConnections(entry, saved.get(entry.ref.id) ?? [])),
|
||||
).toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
}),
|
||||
connection: {
|
||||
|
|
@ -587,36 +559,18 @@ const layer = Layer.effect(
|
|||
if (attempt) yield* Scope.close(attempt.scope, Exit.void)
|
||||
}),
|
||||
},
|
||||
capability: {
|
||||
search: {
|
||||
list: Effect.fn("Integration.capability.search.list")(function* () {
|
||||
return Array.from(state.get().integrations.values()).flatMap((entry) =>
|
||||
entry.search ? [entry.search as SearchImplementation] : [],
|
||||
)
|
||||
}),
|
||||
get: Effect.fn("Integration.capability.search.get")(function* (integrationID) {
|
||||
return state.get().integrations.get(integrationID)?.search as SearchImplementation | undefined
|
||||
}),
|
||||
selected: selectedSearch,
|
||||
select: Effect.fn("Integration.capability.search.select")(function* (integrationID) {
|
||||
if (!state.get().integrations.get(integrationID)?.search) {
|
||||
return yield* Effect.die(new Error(`Search capability not found: ${integrationID}`))
|
||||
}
|
||||
yield* db
|
||||
.insert(IntegrationCapabilityTable)
|
||||
.values({ capability: "search", integration_id: integrationID })
|
||||
.onConflictDoUpdate({
|
||||
target: IntegrationCapabilityTable.capability,
|
||||
set: { integration_id: integrationID },
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* events.publish(Event.Updated, {})
|
||||
}),
|
||||
},
|
||||
search: {
|
||||
list: Effect.fn("Integration.search.list")(function* () {
|
||||
return Array.from(state.get().integrations.values()).flatMap((entry) =>
|
||||
entry.search ? [entry.search as SearchImplementation] : [],
|
||||
)
|
||||
}),
|
||||
get: Effect.fn("Integration.search.get")(function* (integrationID) {
|
||||
return state.get().integrations.get(integrationID)?.search as SearchImplementation | undefined
|
||||
}),
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Credential.node, Database.node, EventV2.node] })
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Credential.node, EventV2.node] })
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import type { Integration } from "../integration"
|
||||
|
||||
export const IntegrationCapabilityTable = sqliteTable("integration_capability", {
|
||||
capability: text().$type<Integration.Capability["type"]>().primaryKey(),
|
||||
integration_id: text().$type<Integration.ID>().notNull(),
|
||||
...Timestamps,
|
||||
})
|
||||
|
|
@ -165,7 +165,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||
integration: {
|
||||
list: () => response(integration.list()),
|
||||
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
|
||||
selectCapability: (input) => integration.capability.search.select(Integration.ID.make(input.integrationID)),
|
||||
connectKey: (input) =>
|
||||
integration.connection.key({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
|
|
@ -343,9 +342,9 @@ function registerIntegration(draft: Integration.Draft, definition: IntegrationDe
|
|||
)
|
||||
}
|
||||
if (!definition.search) return
|
||||
draft.capability.search.update({
|
||||
draft.search.update({
|
||||
integrationID,
|
||||
capability: { type: "search", connection: definition.search.connection },
|
||||
connection: definition.search.connection,
|
||||
execute: definition.search.execute,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ export function fromPromise(plugin: Plugin) {
|
|||
integration: {
|
||||
list: (input) => run(host.integration.list(input)),
|
||||
get: (input) => run(host.integration.get(input)),
|
||||
selectCapability: (input) => run(host.integration.selectCapability(input)),
|
||||
connectKey: (input) => run(host.integration.connectKey(input)),
|
||||
connectOauth: (input) => run(host.integration.connectOauth(input)),
|
||||
attemptStatus: (input) => run(host.integration.attemptStatus(input)),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
export * as Search from "./search"
|
||||
|
||||
import { Search } from "@opencode-ai/schema/search"
|
||||
import { Context, Effect, Layer, Schema, Semaphore } from "effect"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { Context, Effect, Layer, Schema, Semaphore, Stream } from "effect"
|
||||
import path from "node:path"
|
||||
import { Config } from "./config"
|
||||
import { ConfigGlobal } from "./config/global"
|
||||
import { ConfigSearch } from "./config/search"
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import { EventV2 } from "./event"
|
||||
import { Form } from "./form"
|
||||
import { Global } from "./global"
|
||||
import { Integration } from "./integration"
|
||||
import { truthy } from "./flag/flag"
|
||||
|
||||
|
|
@ -50,6 +56,8 @@ export interface QueryInput extends Input {
|
|||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly selected: () => Effect.Effect<Integration.ID | undefined>
|
||||
readonly select: (providerID: Integration.ID) => Effect.Effect<void, ProviderNotFoundError>
|
||||
readonly query: (input: QueryInput) => Effect.Effect<Result, Error>
|
||||
}
|
||||
|
||||
|
|
@ -59,10 +67,14 @@ const layer = Layer.effect(
|
|||
Service,
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const configGlobal = yield* ConfigGlobal.Service
|
||||
const events = yield* EventV2.Service
|
||||
const forms = yield* Form.Service
|
||||
const global = yield* Global.Service
|
||||
const integrations = yield* Integration.Service
|
||||
const onboarding = Semaphore.makeUnsafe(1)
|
||||
const decodeOutput = Schema.decodeUnknownEffect(ProviderOutput)
|
||||
let pending: Integration.ID | undefined
|
||||
|
||||
const requireProvider = (
|
||||
providers: Map<Integration.ID, Integration.SearchImplementation>,
|
||||
|
|
@ -86,6 +98,39 @@ const layer = Layer.effect(
|
|||
}
|
||||
})
|
||||
|
||||
const globalProvider = Effect.fn("Search.globalProvider")(function* () {
|
||||
const entries = (yield* config.entries()).filter(
|
||||
(entry) => entry.type === "document" && entry.path && path.dirname(entry.path) === path.resolve(global.config),
|
||||
)
|
||||
return Config.latest(entries, "search")?.provider
|
||||
})
|
||||
|
||||
const selected = Effect.fn("Search.selected")(function* () {
|
||||
return pending ?? (yield* globalProvider())
|
||||
})
|
||||
|
||||
const save = Effect.fn("Search.save")(function* (providerID: Integration.ID) {
|
||||
pending = providerID
|
||||
yield* configGlobal.update(["search"], new ConfigSearch.Info({ provider: providerID })).pipe(
|
||||
Effect.tapError(() => Effect.sync(() => (pending = undefined))),
|
||||
Effect.orDie,
|
||||
)
|
||||
})
|
||||
|
||||
yield* events.subscribe(ConfigSchema.Event.Updated).pipe(
|
||||
Stream.runForEach(() =>
|
||||
globalProvider().pipe(
|
||||
Effect.tap((providerID) =>
|
||||
Effect.sync(() => {
|
||||
if (providerID === pending) pending = undefined
|
||||
}),
|
||||
),
|
||||
Effect.ignore,
|
||||
),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
const ask = Effect.fn("Search.ask")(function* (
|
||||
providers: Map<Integration.ID, Integration.SearchImplementation>,
|
||||
sessionID: string,
|
||||
|
|
@ -110,8 +155,7 @@ const layer = Layer.effect(
|
|||
.flatMap((provider) => {
|
||||
const info = infos.get(provider.integrationID)
|
||||
if (!info) return []
|
||||
const disconnected =
|
||||
provider.capability.connection === "optional" ? "Keyless available" : "Connection required"
|
||||
const disconnected = provider.connection === "optional" ? "Keyless available" : "Connection required"
|
||||
return [{ info, description: info.connections.length ? "Connected" : disconnected }]
|
||||
})
|
||||
.toSorted((a, b) => a.info.name.localeCompare(b.info.name))
|
||||
|
|
@ -135,7 +179,7 @@ const layer = Layer.effect(
|
|||
sessionID?: string,
|
||||
) {
|
||||
const active = yield* integrations.connection.active(provider.integrationID)
|
||||
if (active || provider.capability.connection === "optional") return active
|
||||
if (active || provider.connection === "optional") return active
|
||||
if (!sessionID) return yield* new ConnectionRequiredError({ providerID: provider.integrationID })
|
||||
const state = yield* forms
|
||||
.ask({
|
||||
|
|
@ -152,34 +196,40 @@ const layer = Layer.effect(
|
|||
return connected
|
||||
})
|
||||
|
||||
const select = Effect.fn("Search.select")(function* (input: QueryInput) {
|
||||
const resolve = Effect.fn("Search.resolve")(function* (input: QueryInput) {
|
||||
const providers = new Map(
|
||||
(yield* integrations.capability.search.list()).map((provider) => [provider.integrationID, provider]),
|
||||
(yield* integrations.search.list()).map((provider) => [provider.integrationID, provider]),
|
||||
)
|
||||
if (input.providerID) return yield* requireProvider(providers, input.providerID)
|
||||
const override = yield* configuredProvider()
|
||||
if (override) return yield* requireProvider(providers, override)
|
||||
const selected = yield* integrations.capability.search.selected()
|
||||
const provider = selected ? providers.get(selected) : undefined
|
||||
const providerID = yield* selected()
|
||||
const provider = providerID ? providers.get(providerID) : undefined
|
||||
if (provider) return provider
|
||||
const sessionID = input.sessionID
|
||||
if (!sessionID) return yield* new ProviderRequiredError()
|
||||
return yield* onboarding.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const current = yield* integrations.capability.search.selected()
|
||||
const selected = current ? providers.get(current) : undefined
|
||||
if (selected) return selected
|
||||
const current = yield* selected()
|
||||
const selectedProvider = current ? providers.get(current) : undefined
|
||||
if (selectedProvider) return selectedProvider
|
||||
const provider = yield* ask(providers, sessionID)
|
||||
yield* connect(provider, sessionID)
|
||||
yield* integrations.capability.search.select(provider.integrationID)
|
||||
yield* save(provider.integrationID)
|
||||
return provider
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
selected,
|
||||
select: Effect.fn("Search.select")(function* (providerID) {
|
||||
const provider = yield* integrations.search.get(providerID)
|
||||
if (!provider) return yield* new ProviderNotFoundError({ providerID })
|
||||
yield* save(providerID)
|
||||
}),
|
||||
query: Effect.fn("Search.query")(function* (input) {
|
||||
const provider = yield* select(input)
|
||||
const provider = yield* resolve(input)
|
||||
const connection = yield* connect(provider, input.sessionID)
|
||||
const credential = connection
|
||||
? yield* integrations.connection
|
||||
|
|
@ -196,4 +246,8 @@ const layer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Config.node, Form.node, Integration.node] })
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [Config.node, ConfigGlobal.node, EventV2.node, Form.node, Global.node, Integration.node],
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue