This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// The enablement flow: one idempotent procedure pair an organization administrator runs, with the
|
||||
// platform's checks in front of any configuration change and every denial a typed reason code the
|
||||
// UI maps to an upgrade or policy state.
|
||||
//
|
||||
// enableDocumentAi: already enabled -> answers the current configuration, writes nothing.
|
||||
// Otherwise: eligibility (plan capability + rollout flags) -> quota -> the vector backing
|
||||
// (an operator-configured endpoint, validated before it is trusted, or a managed link through
|
||||
// the providers API) -> one transaction persists the configuration. A failure at any step
|
||||
// writes nothing and is retirable: the same call again resumes from the first failing check.
|
||||
//
|
||||
// disableDocumentAi: preserves everything — collections, documents, vectors and the recorded
|
||||
// configuration stay; only `enabled` flips, so re-enabling resumes without duplication.
|
||||
|
||||
import { AppError } from "@conusai-kit/core"
|
||||
import { config, type EnablementReason } from "../contract/index.js"
|
||||
import type { DocumentAiPlatform, KnowledgeOrgScope } from "./ports.js"
|
||||
|
||||
/** A typed enablement denial: the problem's code is the stable reason the UI maps to a state. */
|
||||
export function enablementDenied(reason: EnablementReason, detail?: string): AppError {
|
||||
const status = reason === "quota_exhausted" ? 429 : 403
|
||||
const title =
|
||||
reason === "plan_required"
|
||||
? "The organization's plan does not include document AI"
|
||||
: reason === "feature_disabled"
|
||||
? "Document AI is disabled by policy"
|
||||
: "The indexing quota is exhausted"
|
||||
return new AppError({
|
||||
type: `/problems/${reason}`,
|
||||
title,
|
||||
status,
|
||||
code: reason,
|
||||
...(detail === undefined ? {} : { detail }),
|
||||
})
|
||||
}
|
||||
|
||||
export interface SettingsView {
|
||||
enabled: boolean
|
||||
backend: "vectors" | "platform" | null
|
||||
source: "operator" | "managed" | null
|
||||
url: string | null
|
||||
}
|
||||
|
||||
async function currentConfig(deps: { data: import("@conusai-kit/data").DataSource }, scope: KnowledgeOrgScope) {
|
||||
const row = (await deps.data.forScope(scope).of(config).list({ limit: 1 })).items[0]
|
||||
return row ?? null
|
||||
}
|
||||
|
||||
export async function getSettings(
|
||||
deps: { data: import("@conusai-kit/data").DataSource },
|
||||
scope: KnowledgeOrgScope,
|
||||
): Promise<SettingsView> {
|
||||
const row = await currentConfig(deps, scope)
|
||||
return {
|
||||
enabled: row?.enabled ?? false,
|
||||
backend: row?.backend ?? null,
|
||||
source: row?.source ?? null,
|
||||
url: row?.url ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export interface SettingsDeps {
|
||||
data: import("@conusai-kit/data").DataSource
|
||||
platform: DocumentAiPlatform
|
||||
now?: () => number
|
||||
}
|
||||
|
||||
export async function enableDocumentAi(deps: SettingsDeps, scope: KnowledgeOrgScope): Promise<SettingsView> {
|
||||
const existing = await currentConfig(deps, scope)
|
||||
if (existing?.enabled) {
|
||||
return { enabled: true, backend: existing.backend, source: existing.source, url: existing.url }
|
||||
}
|
||||
|
||||
const eligibility = await deps.platform.checkEligibility(scope)
|
||||
if (!eligibility.ok) throw enablementDenied(eligibility.reason, eligibility.detail)
|
||||
|
||||
const remaining = await deps.platform.indexingQuota(scope)
|
||||
if (remaining <= 0) throw enablementDenied("quota_exhausted", "the indexing volume for today is spent")
|
||||
|
||||
// The vector backing: an operator-configured endpoint+key when the deployment carries one
|
||||
// (validated before it is trusted), otherwise a managed link through the providers API.
|
||||
const operator = deps.platform.operatorQdrant()
|
||||
let source: "operator" | "managed"
|
||||
let serviceId: string | null
|
||||
let url: string | null
|
||||
if (operator !== null) {
|
||||
const probe = await deps.platform.probeQdrant(operator.url, operator.apiKey)
|
||||
if (!probe.ok) {
|
||||
throw new AppError({
|
||||
type: "/problems/validation_failed",
|
||||
title: "The configured Qdrant endpoint did not answer",
|
||||
status: 422,
|
||||
code: "validation_failed",
|
||||
detail: probe.detail,
|
||||
})
|
||||
}
|
||||
source = "operator"
|
||||
serviceId = null
|
||||
url = operator.url
|
||||
} else {
|
||||
const linked = await deps.platform.linkManagedQdrant(scope)
|
||||
source = "managed"
|
||||
serviceId = linked.serviceId
|
||||
url = linked.url
|
||||
}
|
||||
|
||||
await deps.data.unitOfWork.run(scope, async (tx) => {
|
||||
const row = (await tx.of(config).list({ limit: 1 })).items[0]
|
||||
const patch = {
|
||||
enabled: true,
|
||||
backend: "vectors" as const,
|
||||
source,
|
||||
serviceId,
|
||||
url,
|
||||
updatedBy: scope.actor.userId,
|
||||
configuredAt: new Date((deps.now ?? Date.now)()).toISOString(),
|
||||
}
|
||||
if (row) await tx.of(config).update(row.id, patch, row.version)
|
||||
else await tx.of(config).insert(patch)
|
||||
})
|
||||
return { enabled: true, backend: "vectors", source, url }
|
||||
}
|
||||
|
||||
export async function disableDocumentAi(deps: SettingsDeps, scope: KnowledgeOrgScope): Promise<{ enabled: false }> {
|
||||
await deps.data.unitOfWork.run(scope, async (tx) => {
|
||||
const row = (await tx.of(config).list({ limit: 1 })).items[0]
|
||||
if (!row?.enabled) return
|
||||
// Everything else is preserved on purpose: backend, source, the service id, the endpoint, and
|
||||
// every collection, document and vector. Disabling fences new work; it deletes nothing.
|
||||
await tx.of(config).update(
|
||||
row.id,
|
||||
{
|
||||
enabled: false,
|
||||
updatedBy: scope.actor.userId,
|
||||
configuredAt: new Date((deps.now ?? Date.now)()).toISOString(),
|
||||
},
|
||||
row.version,
|
||||
)
|
||||
})
|
||||
return { enabled: false }
|
||||
}
|
||||
Reference in New Issue
Block a user