Files
sfera/frontend/sfera-web/src/lib/api.ts
T
m 3b37a217a8
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled
Use real IDE authoring session
2026-05-16 20:14:38 +03:00

1177 lines
33 KiB
TypeScript

const DEFAULT_API_PORT = process.env.SFERA_API_PORT ?? process.env.NEXT_PUBLIC_SFERA_API_PORT ?? "8000";
const FALLBACK_API_URL = "http://192.168.200.60:8000";
export type AdminSummary = {
indexed_projects: number;
stored_snapshots: number;
knowledge_records: number;
knowledge_packs: number;
users: number;
tasks: number;
comments: number;
ownership: number;
privacy_markers: number;
ai_usage_records: number;
jobs: number;
metrics: number;
marketplace_packages: number;
};
export type Neo4jStatus = {
status: string;
uri: string;
nodes: number;
edges: number;
};
export type StoredSnapshot = {
project_id: string;
snapshot_id: string;
snapshot_hash: string | null;
path: string;
};
export type ProjectSummary = {
project_id: string;
name: string;
status: string;
has_snapshot: boolean;
};
export type AiUsageSummary = {
request_count: number;
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
cost: number;
currency: string;
};
export type AiPolicy = {
policy: {
privacy_mode: string;
allow_code_context: boolean;
allow_external_calls: boolean;
token_limit_per_day: number | null;
};
used_tokens: number;
remaining_tokens: number | null;
};
export type Health = {
status: string;
};
export type SnapshotSummary = {
snapshot_id: string;
project_id: string;
snapshot_hash: string | null;
node_count: number;
edge_count: number;
diagnostics_count: number;
unresolved_references_count: number;
};
export type ReviewFinding = {
finding_id?: string;
title: string;
severity: "INFO" | "WARNING" | "ERROR" | string;
message: string;
source_path?: string | null;
line_start?: number | null;
};
export type WorkspaceModuleRoutine = {
name: string;
kind?: string;
line_start?: number | null;
line_end?: number | null;
export?: boolean;
};
export type WorkspaceModuleSource = {
name: string;
qualified_name: string;
module_role: string;
source_path: string;
source_text: string;
routines_count: number;
routines: WorkspaceModuleRoutine[];
};
export type BslCompletionItem = {
label: string;
kind: string;
detail?: string | null;
insert_text?: string | null;
};
export type ProjectReport = {
project_id: string;
snapshot_id: string;
node_count: number;
edge_count: number;
procedure_count: number;
query_count: number;
write_count: number;
role_count: number;
access_grant_count: number;
object_attribute_count: number;
tabular_section_count: number;
unsecured_object_count: number;
unsecured_objects: string[];
integration_count: number;
outbound_integration_count: number;
diagnostic_count: number;
ownership_count: number;
unowned_object_count: number;
unowned_objects: string[];
privacy_marker_count: number;
sensitive_candidate_count: number;
unclassified_sensitive_count: number;
ai_usage_request_count: number;
ai_usage_total_tokens: number;
ai_usage_cost: number;
};
export type NamedNode = {
lineage_id: string;
kind: string;
name: string;
qualified_name: string;
};
export type SourceLocation = {
source_path: string | null;
line_start: number | null;
line_end: number | null;
column_start: number | null;
column_end: number | null;
};
export type SymbolResult = {
node: NamedNode;
source: SourceLocation;
};
export type SymbolReference = {
edge_id: string;
kind: string;
direction: "incoming" | "outgoing" | string;
source: NamedNode | null;
target: NamedNode | null;
location: SourceLocation | null;
attributes: Record<string, unknown>;
};
export type SymbolReferences = {
symbol: SymbolResult;
references: SymbolReference[];
};
export type KnowledgeSchemaCoverage = {
covered_count: number;
uncovered_count: number;
uncovered: NamedNode[];
};
export type FormSemantics = {
form: NamedNode;
commands: NamedNode[];
elements: NamedNode[];
command_handlers: Record<string, NamedNode>;
};
export type ObjectUi = {
object: NamedNode;
forms: FormSemantics[];
};
export type TabularSectionColumns = {
tabular_section: NamedNode;
columns: NamedNode[];
};
export type ObjectSchema = {
object: NamedNode;
attributes: NamedNode[];
tabular_sections: TabularSectionColumns[];
};
export type ObjectImpact = {
object_name: string;
object: NamedNode;
modules: NamedNode[];
routines: NamedNode[];
forms: NamedNode[];
commands: NamedNode[];
attributes: NamedNode[];
tabular_sections: NamedNode[];
tabular_section_columns: Record<string, NamedNode[]>;
roles: NamedNode[];
role_access: Array<{ role: NamedNode; permissions: Record<string, unknown> }>;
jobs: NamedNode[];
callees: NamedNode[];
query_tables: NamedNode[];
writes: NamedNode[];
};
export type IntegrationEndpoint = {
endpoint_id: string;
name: string;
kind: string;
direction: string;
owner: string | null;
};
export type ScheduledJob = {
job_id: string;
name: string;
routine_name: string;
schedule: string | null;
};
export type MetadataTypeSpec = {
code: string;
russian_name: string;
tree_branch: string;
icon: string;
description?: string;
documentation_url?: string;
child_groups: string[];
module_kinds: string[];
properties?: string[];
context_actions?: string[];
};
export type MetadataChildObjectSpec = {
code: string;
russian_name: string;
parent_groups: string[];
description: string;
documentation_url: string;
};
export type MetadataCatalog = {
platform_family: string;
source: string;
common_branch_children: string[];
types: MetadataTypeSpec[];
child_object_types?: MetadataChildObjectSpec[];
};
export type MetadataTreeNode = {
id: string;
label: string;
kind: string;
icon: string;
qualified_name: string | null;
count: number;
loaded_count: number;
has_more: boolean;
children: MetadataTreeNode[];
};
export type ProjectMetadataTree = {
project_id: string;
root: MetadataTreeNode;
};
export type MetadataTreeChildren = {
project_id: string;
parent_id: string;
offset: number;
limit: number;
total: number;
has_more: boolean;
children: MetadataTreeNode[];
};
export type MetadataTreeSearch = {
project_id: string;
q: string;
total: number;
results: MetadataTreeNode[];
};
export type MetadataTreePathStep = {
parent_id: string;
child_id: string;
offset: number;
};
export type MetadataTreePath = {
project_id: string;
node_id: string;
path: string[];
steps: MetadataTreePathStep[];
};
export type SirNode = {
lineage_id: string;
semantic_id?: string;
kind: string;
name: string;
qualified_name: string;
source_path?: string | null;
line_start?: number | null;
line_end?: number | null;
attributes?: Record<string, unknown>;
};
export type SirExport = {
project_id: string;
snapshot_id: string;
snapshot_hash: string | null;
nodes: SirNode[];
edges: unknown[];
diagnostics: unknown[];
};
export type FlowchartNode = {
id: string;
label: string;
kind: string;
qualified_name?: string | null;
count: number;
level: number;
expandable: boolean;
};
export type FlowchartEdge = {
id: string;
source: string;
target: string;
kind: string;
label: string;
count: number;
};
export type ProjectFlowchart = {
project_id: string;
mode: "overview" | "focus" | string;
focus?: string | null;
total_nodes: number;
total_edges: number;
nodes: FlowchartNode[];
edges: FlowchartEdge[];
};
export type AuthoringContext = {
project_id: string;
object: NamedNode | null;
routine: NamedNode | null;
local_variables: string[];
parameters: string[];
object_attributes: NamedNode[];
tabular_sections: NamedNode[];
form_elements: NamedNode[];
commands: NamedNode[];
query_tables: NamedNode[];
writes: NamedNode[];
available_methods: string[];
review_findings: ReviewFinding[];
};
export type AuthoringCompletionPreview = {
allowed: boolean;
insert_text: string;
semantic_diff: Array<{ kind: string; text: string }>;
checks: Array<{ name: string; status: string; message: string }>;
context: AuthoringContext;
};
export type AuthoringSemanticDiffPreviewRequest = {
object_name: string;
routine_name?: string | null;
original_text: string;
proposed_text: string;
source_path?: string | null;
task_id?: string | null;
session_id?: string | null;
user_id?: string | null;
};
export type AuthoringSemanticDiffPreview = {
project_id: string;
target: NamedNode | null;
changed: boolean;
added_lines: number;
removed_lines: number;
semantic_diff: Array<{ kind: string; text: string }>;
affected_nodes: NamedNode[];
checks: Array<{ name: string; status: string; message: string }>;
version_preview: {
lineage_id: string;
semantic_id: string;
current_version_id: string | null;
next_version_id: string;
object_hash: string;
task_id: string | null;
session_id: string | null;
apply_available: boolean;
} | null;
};
export type AuthoringApplyChangeSetRequest = {
target_lineage_id?: string | null;
object_name?: string | null;
routine_name?: string | null;
source_path?: string | null;
original_text: string;
proposed_text: string;
task_id?: string | null;
session_id?: string | null;
user_id?: string | null;
estimated_tokens?: number;
expected_next_version_id: string;
approved_by: string;
approval_note?: string | null;
apply_to_production: false;
};
export type SemanticObjectVersion = {
version_id: string;
lineage_id: string;
semantic_id: string;
object_hash: string;
task_id: string | null;
session_id: string | null;
parent_version_id: string | null;
payload: Record<string, unknown>;
created_at: string;
};
export type ObjectVersionSummary = {
version_id: string;
lineage_id: string;
semantic_id: string;
object_hash: string;
parent_version_id: string | null;
task_id: string | null;
session_id: string | null;
};
export type SemanticObjectDiffEntry = {
path: string;
kind: string;
before?: unknown | null;
after?: unknown | null;
};
export type SemanticObjectDiff = {
from_version_id: string;
to_version_id: string;
lineage_id: string;
changed: boolean;
entries: SemanticObjectDiffEntry[];
};
export type AuthoringApplyChangeSetResponse = {
project_id: string;
status: string;
change_id: string;
version: SemanticObjectVersion;
preview: AuthoringSemanticDiffPreview;
persisted_path: string;
production_applied: boolean;
};
export type AuthoringRollbackPreview = {
project_id: string;
change_id: string;
original_version_id: string;
rollback_version_id: string;
target: NamedNode | null;
semantic_diff: Array<{ kind: string; text: string }>;
checks: Array<{ name: string; status: string; message: string }>;
apply_available: boolean;
};
export type AuthoringApplyRollbackRequest = {
expected_rollback_version_id: string;
approved_by: string;
approval_note?: string | null;
task_id?: string | null;
session_id?: string | null;
apply_to_production: false;
};
export type AuthoringApplyRollbackResponse = {
project_id: string;
status: string;
change_id: string;
rollback_change_id: string;
version: SemanticObjectVersion;
preview: AuthoringRollbackPreview;
persisted_path: string;
production_applied: boolean;
};
export type AuthoringMetadataAttributeDraft = {
name: string;
type: string;
synonym?: string | null;
required?: boolean;
};
export type AuthoringMetadataTabularSectionDraft = {
name: string;
synonym?: string | null;
attributes: AuthoringMetadataAttributeDraft[];
};
export type AuthoringMetadataCommandDraft = {
name: string;
handler?: string | null;
};
export type AuthoringMetadataObjectDraft = {
object_kind: string;
name: string;
synonym?: string | null;
attributes: AuthoringMetadataAttributeDraft[];
tabular_sections: AuthoringMetadataTabularSectionDraft[];
forms?: string[];
commands?: AuthoringMetadataCommandDraft[];
task_id?: string | null;
session_id?: string | null;
user_id?: string | null;
};
export type AuthoringMetadataObjectPreview = {
project_id: string;
target: NamedNode;
changed: boolean;
added_lines: number;
removed_lines: number;
semantic_diff: Array<{ kind: string; text: string }>;
checks: Array<{ name: string; status: string; message: string }>;
version_preview: NonNullable<AuthoringSemanticDiffPreview["version_preview"]>;
};
export type AuthoringApplyMetadataObjectRequest = AuthoringMetadataObjectDraft & {
expected_next_version_id: string;
approved_by: string;
approval_note?: string | null;
apply_to_production: false;
};
export type AuthoringApplyMetadataObjectResponse = {
project_id: string;
status: string;
change_id: string;
version: SemanticObjectVersion;
preview: AuthoringMetadataObjectPreview;
persisted_path: string;
production_applied: boolean;
};
export type AuthoringChangeSummary = {
change_id: string;
project_id: string;
status: string;
target: NamedNode | null;
version_id: string;
approved_by: string;
approval_note: string | null;
task_id: string | null;
session_id: string | null;
added_lines: number;
removed_lines: number;
production_applied: boolean;
};
export type AuthoringSessionContext = {
user_id: string;
task_id: string;
session_id: string;
};
export function resolveApiUrl(hostHeader?: string | null) {
const configuredUrl = process.env.SFERA_API_URL ?? process.env.NEXT_PUBLIC_SFERA_API_URL;
if (configuredUrl) {
return configuredUrl;
}
const host = hostHeader?.split(",")[0]?.trim();
if (!host) {
return FALLBACK_API_URL;
}
const protocol = host.startsWith("localhost") || host.startsWith("127.0.0.1") ? "http" : "http";
const hostname = host.startsWith("[")
? host.slice(0, host.indexOf("]") + 1)
: host.replace(/:\d+$/, "");
const apiHostname = hostname === "127.0.0.1" || hostname === "[::1]" ? "localhost" : hostname;
return `${protocol}://${apiHostname}:${DEFAULT_API_PORT}`;
}
async function getJson<T>(apiUrl: string, path: string): Promise<T> {
const response = await fetch(`${apiUrl}${path}`, {
cache: "no-store",
headers: { Accept: "application/json" }
});
if (!response.ok) {
throw new Error(await formatResponseError(response));
}
return response.json() as Promise<T>;
}
async function postJson<T>(apiUrl: string, path: string, body: unknown): Promise<T> {
const response = await fetch(`${apiUrl}${path}`, {
method: "POST",
cache: "no-store",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(await formatResponseError(response));
}
return response.json() as Promise<T>;
}
async function formatResponseError(response: Response) {
const status = `${response.status} ${response.statusText}`;
const detail = await readResponseErrorDetail(response);
return detail ? `${status}: ${detail}` : status;
}
async function readResponseErrorDetail(response: Response) {
const contentType = response.headers.get("content-type") ?? "";
if (contentType.includes("application/json")) {
try {
const payload = (await response.clone().json()) as unknown;
return stringifyErrorPayload(payload);
} catch {
return "";
}
}
try {
return (await response.text()).trim();
} catch {
return "";
}
}
function stringifyErrorPayload(payload: unknown): string {
if (typeof payload === "string") {
return payload;
}
if (payload && typeof payload === "object" && "detail" in payload) {
const detail = (payload as { detail: unknown }).detail;
if (typeof detail === "string") {
return detail;
}
if (detail !== null && detail !== undefined) {
return JSON.stringify(detail);
}
}
if (payload !== null && payload !== undefined) {
return JSON.stringify(payload);
}
return "";
}
async function getOptionalJson<T>(apiUrl: string, path: string, fallback: T): Promise<T> {
try {
return await getJson<T>(apiUrl, path);
} catch {
return fallback;
}
}
async function postOptionalJson<T>(apiUrl: string, path: string, body: unknown, fallback: T): Promise<T> {
try {
return await postJson<T>(apiUrl, path, body);
} catch {
return fallback;
}
}
function ideAuthoringSessionContext(projectId: string): AuthoringSessionContext {
const safeProjectId = projectId.replace(/[^A-Za-z0-9_.-]/g, "-");
return {
user_id: "ide.developer",
task_id: `task.ide.${safeProjectId}`,
session_id: `session.ide.${safeProjectId}`
};
}
export async function ensureAuthoringSession(projectId: string, apiUrl = resolveApiUrl()): Promise<AuthoringSessionContext> {
const context = ideAuthoringSessionContext(projectId);
await postJson(apiUrl, "/collaboration/users", {
user_id: context.user_id,
display_name: "SFERA IDE"
});
await postJson(apiUrl, `/security/users/${encodeURIComponent(context.user_id)}/roles/developer`, {});
await postJson(apiUrl, "/collaboration/tasks", {
task_id: context.task_id,
project_id: projectId,
title: "SFERA IDE authoring",
status: "IN_PROGRESS",
assignee_user_id: context.user_id
});
await postJson(apiUrl, "/collaboration/sessions", {
session: {
session_id: context.session_id,
task_id: context.task_id,
user_id: context.user_id
}
});
return context;
}
export async function getDashboardData(apiUrl = resolveApiUrl()) {
const [health, summary, snapshots, neo4j, aiUsage, aiPolicy] = await Promise.all([
getJson<Health>(apiUrl, "/health"),
getJson<AdminSummary>(apiUrl, "/admin/summary"),
getJson<StoredSnapshot[]>(apiUrl, "/storage/snapshots"),
getJson<Neo4jStatus>(apiUrl, "/graph/neo4j/status"),
getJson<AiUsageSummary>(apiUrl, "/ai/usage/summary"),
getJson<AiPolicy>(apiUrl, "/ai/policy")
]);
return {
health,
summary,
snapshots,
neo4j,
aiUsage,
aiPolicy,
apiUrl
};
}
export async function getApiHealth(apiUrl = resolveApiUrl()) {
return getJson<Health>(apiUrl, "/health");
}
export async function getStoredSnapshots(apiUrl = resolveApiUrl()) {
return getJson<StoredSnapshot[]>(apiUrl, "/storage/snapshots");
}
export async function getProjects(apiUrl = resolveApiUrl()) {
return getJson<ProjectSummary[]>(apiUrl, "/projects");
}
export async function searchProjectSymbols(
projectId: string,
query: string,
options: { kind?: string; limit?: number; apiUrl?: string } = {}
) {
const apiUrl = options.apiUrl ?? resolveApiUrl();
const params = new URLSearchParams({
q: query,
limit: String(options.limit ?? 50)
});
if (options.kind) {
params.set("kind", options.kind);
}
return getJson<SymbolResult[]>(apiUrl, `/projects/${projectId}/symbols?${params.toString()}`);
}
export async function getProjectSymbolDefinition(
projectId: string,
lineageId: string,
apiUrl = resolveApiUrl()
) {
const params = new URLSearchParams({ lineage_id: lineageId });
return getJson<SymbolResult>(apiUrl, `/projects/${projectId}/symbols/definition?${params.toString()}`);
}
export async function getProjectSymbolReferences(
projectId: string,
lineageId: string,
options: { direction?: "incoming" | "outgoing" | "both"; apiUrl?: string } = {}
) {
const apiUrl = options.apiUrl ?? resolveApiUrl();
const params = new URLSearchParams({
lineage_id: lineageId,
direction: options.direction ?? "incoming"
});
return getJson<SymbolReferences>(apiUrl, `/projects/${projectId}/symbols/references?${params.toString()}`);
}
export async function getBslCompletions(
projectId: string,
options: { apiUrl?: string; receiver?: string | null; q?: string | null; qualifiedName?: string | null; limit?: number } = {}
) {
const apiUrl = options.apiUrl ?? resolveApiUrl();
const params = new URLSearchParams({
q: options.q ?? "",
limit: String(options.limit ?? 80)
});
if (options.receiver) {
params.set("receiver", options.receiver);
}
if (options.qualifiedName) {
params.set("qualified_name", options.qualifiedName);
}
return getJson<BslCompletionItem[]>(apiUrl, `/projects/${projectId}/bsl/completions?${params.toString()}`);
}
export async function getProjectWorkspaceData(projectId: string, apiUrl = resolveApiUrl(), selectedRoutine?: string | null, activeMode?: string | null) {
const selectedRoutineName = selectedRoutine?.trim() ?? null;
const workspaceMode = activeMode?.trim() || "overview";
const isModuleMode = workspaceMode === "module";
const needsObjectPanels = !isModuleMode;
const needsFlowchart = workspaceMode === "flowchart";
const fallbackSourceText = [
"// Код модуля не загружен.",
"// Выберите реальный модуль в дереве проекта или выполните индексирование с исходниками BSL."
].join("\n");
const snapshot = await getOptionalJson<SnapshotSummary | null>(apiUrl, `/projects/${projectId}/snapshot`, null);
const isLargeProject = (snapshot?.node_count ?? 0) > 20000;
const [metadataCatalog, metadataTree, exportSnapshot] = await Promise.all([
isLargeProject ? Promise.resolve(null) : getOptionalJson<MetadataCatalog | null>(apiUrl, "/metadata/catalog", null),
getOptionalJson<ProjectMetadataTree | null>(apiUrl, `/projects/${projectId}/metadata/tree?object_limit_per_branch=${isLargeProject ? 0 : 200}`, null),
isLargeProject ? Promise.resolve(null) : getOptionalJson<SirExport | null>(apiUrl, `/projects/${projectId}/snapshot/export`, null)
]);
const flowchart = needsFlowchart ? await getOptionalJson<ProjectFlowchart | null>(apiUrl, `/projects/${projectId}/flowchart?limit=80`, null) : null;
const selectedMetadataSearch = selectedRoutineName
? await getOptionalJson<MetadataTreeSearch | null>(
apiUrl,
`/projects/${projectId}/metadata/tree/search?q=${encodeURIComponent(selectedRoutineName)}&limit=1`,
null
)
: null;
let selectedTreeNode = selectedMetadataSearch?.results[0] ?? findFirstModuleOwnerNode(metadataTree?.root) ?? null;
if (!selectedRoutineName && !selectedTreeNode) {
const firstCommonModulePage = await getOptionalJson<MetadataTreeChildren | null>(
apiUrl,
`/projects/${projectId}/metadata/tree/children?${new URLSearchParams({ node_id: "common.Общие модули", offset: "0", limit: "1" })}`,
null
);
selectedTreeNode = firstCommonModulePage?.children[0] ?? null;
}
const selectedObjectName = selectedRoutineName ?? selectedTreeNode?.qualified_name ?? null;
const selectedObjectModules = selectedObjectName
? getOptionalJson<WorkspaceModuleSource[]>(
apiUrl,
`/projects/${projectId}/normalized/object/modules?${new URLSearchParams({ qualified_name: selectedObjectName })}`,
[]
)
: Promise.resolve([]);
const [selectedObjectSchema, selectedObjectUi, selectedObjectImpact, report, review, coverage, forms, integrations, jobs, authoringChanges, projectVersions, objectModules] = await Promise.all([
needsObjectPanels && selectedObjectName
? getOptionalJson<ObjectSchema | null>(
apiUrl,
`/projects/${projectId}/objects/schema/${encodeURIComponent(selectedObjectName)}`,
null
)
: Promise.resolve(null),
needsObjectPanels && selectedObjectName
? getOptionalJson<ObjectUi | null>(
apiUrl,
`/projects/${projectId}/objects/ui/${encodeURIComponent(selectedObjectName)}`,
null
)
: Promise.resolve(null),
needsObjectPanels && selectedObjectName
? getOptionalJson<ObjectImpact | null>(
apiUrl,
`/projects/${projectId}/objects/impact/${encodeURIComponent(selectedObjectName)}`,
null
)
: Promise.resolve(null),
isLargeProject ? Promise.resolve(null) : getOptionalJson<ProjectReport | null>(apiUrl, `/projects/${projectId}/report`, null),
isLargeProject ? Promise.resolve([]) : getOptionalJson<ReviewFinding[]>(apiUrl, `/projects/${projectId}/review`, []),
isLargeProject ? Promise.resolve(null) : getOptionalJson<KnowledgeSchemaCoverage | null>(apiUrl, `/projects/${projectId}/knowledge/schema-coverage`, null),
isLargeProject ? Promise.resolve([]) : getOptionalJson<FormSemantics[]>(apiUrl, `/projects/${projectId}/ui/forms`, []),
isLargeProject ? Promise.resolve([]) : getOptionalJson<IntegrationEndpoint[]>(apiUrl, `/projects/${projectId}/integrations`, []),
isLargeProject ? Promise.resolve([]) : getOptionalJson<ScheduledJob[]>(apiUrl, `/projects/${projectId}/jobs/scheduled`, []),
getOptionalJson<AuthoringChangeSummary[]>(apiUrl, `/projects/${projectId}/authoring/changes`, []),
isLargeProject ? Promise.resolve([]) : getOptionalJson<ObjectVersionSummary[]>(apiUrl, `/projects/${projectId}/versions`, [])
,
selectedObjectModules
]);
const selectedObjectModule = selectedObjectName && objectModules.length > 0
? objectModules.find((module) =>
selectedRoutineName
? module.qualified_name === selectedRoutineName ||
module.source_path === selectedRoutineName ||
module.routines.some((routine) => routine.name === selectedRoutineName || `${module.qualified_name}.${routine.name}` === selectedRoutineName)
: false
) ?? objectModules[0]
: null;
const selectedModuleRoutine = selectedRoutineName
? selectedObjectModule?.routines.find((routine) =>
routine.name === selectedRoutineName ||
`${selectedObjectModule.qualified_name}.${routine.name}` === selectedRoutineName
)?.name ?? selectedObjectModule?.routines[0]?.name ?? null
: selectedObjectModule?.routines[0]?.name ?? null;
const snapshotModule = !selectedObjectModule ? firstSnapshotModule(exportSnapshot) : null;
const authoringSourceText = selectedObjectModule?.source_text?.trim()
? selectedObjectModule.source_text
: typeof snapshotModule?.attributes?.source_text === "string"
? snapshotModule.attributes.source_text
: "";
const editorSelectedObject = selectedObjectName ?? snapshotModule?.qualified_name ?? null;
const editorSelectedRoutine = selectedModuleRoutine ?? selectedRoutineName ?? null;
const authoringSession = selectedObjectName && authoringSourceText
? await ensureAuthoringSession(projectId, apiUrl).catch(() => null)
: null;
const authoringPreview = needsObjectPanels && selectedObjectName && authoringSourceText
? await postOptionalJson<AuthoringCompletionPreview | null>(
apiUrl,
`/projects/${projectId}/authoring/completion-preview`,
{
object_name: selectedObjectName,
routine_name: selectedModuleRoutine,
source_path: selectedObjectModule?.source_path ?? null,
source_text: authoringSourceText,
user_id: authoringSession?.user_id ?? null
},
null
)
: null;
const authoringProposedText = authoringPreview?.insert_text ? `${authoringSourceText}\n\n${authoringPreview.insert_text}` : null;
const semanticDiffPreview = selectedObjectName && authoringSourceText && authoringProposedText
? await postOptionalJson<AuthoringSemanticDiffPreview | null>(
apiUrl,
`/projects/${projectId}/authoring/semantic-diff-preview`,
{
object_name: selectedObjectName,
routine_name: selectedModuleRoutine,
original_text: authoringSourceText,
proposed_text: authoringProposedText,
source_path: selectedObjectModule?.source_path ?? null,
task_id: authoringSession?.task_id ?? null,
session_id: authoringSession?.session_id ?? null,
user_id: authoringSession?.user_id ?? null
},
null
)
: null;
return {
projectId,
apiUrl,
metadataCatalog,
metadataTree,
selectedMetadataNode: selectedMetadataSearch?.results[0] ?? null,
selectedObjectSchema,
selectedObjectUi,
selectedObjectImpact,
snapshot,
exportSnapshot,
report,
review,
coverage,
forms,
integrations,
jobs,
flowchart,
authoringPreview,
semanticDiffPreview,
authoringChanges,
projectVersions,
editorSourceText: authoringSourceText || fallbackSourceText,
editorProposedText: authoringProposedText,
editorSelectedObject,
editorSelectedRoutine,
editorModules: objectModules,
editorModuleName: selectedObjectModule?.name ?? snapshotModule?.name ?? null,
editorSourcePath: selectedObjectModule?.source_path ?? snapshotModule?.source_path ?? null
};
}
const moduleOwnerKinds = new Set([
"CATALOG",
"DOCUMENT",
"REGISTER",
"COMMON_MODULE",
"CONSTANT",
"DOCUMENT_JOURNAL",
"ENUM",
"REPORT",
"DATA_PROCESSOR",
"CHART_OF_CHARACTERISTIC_TYPES",
"CHART_OF_ACCOUNTS",
"CHART_OF_CALCULATION_TYPES",
"EXCHANGE_PLAN",
"EXTERNAL_DATA_SOURCE",
"SCHEDULED_JOB",
"BUSINESS_PROCESS",
"TASK",
"HTTP_SERVICE",
"WEB_SERVICE",
"XDTO_PACKAGE",
"EVENT_SUBSCRIPTION"
]);
function findFirstModuleOwnerNode(root?: MetadataTreeNode): MetadataTreeNode | null {
if (!root) {
return null;
}
const stack = [root];
while (stack.length > 0) {
const node = stack.shift()!;
if (node.qualified_name && moduleOwnerKinds.has(node.kind)) {
return node;
}
stack.push(...node.children);
}
return null;
}
function firstSnapshotModule(snapshot: SirExport | null): SirNode | null {
return snapshot?.nodes.find((node) => node.kind === "MODULE" && typeof node.attributes?.source_text === "string") ?? null;
}
export async function getMetadataTreeChildren(
projectId: string,
nodeId: string,
options: { offset?: number; limit?: number; apiUrl?: string } = {}
) {
const apiUrl = options.apiUrl ?? resolveApiUrl();
const params = new URLSearchParams({
node_id: nodeId,
offset: String(options.offset ?? 0),
limit: String(options.limit ?? 80)
});
return getJson<MetadataTreeChildren>(apiUrl, `/projects/${projectId}/metadata/tree/children?${params.toString()}`);
}
export async function searchMetadataTree(
projectId: string,
query: string,
options: { limit?: number; apiUrl?: string } = {}
) {
const apiUrl = options.apiUrl ?? resolveApiUrl();
const params = new URLSearchParams({
q: query,
limit: String(options.limit ?? 80)
});
return getJson<MetadataTreeSearch>(apiUrl, `/projects/${projectId}/metadata/tree/search?${params.toString()}`);
}
export async function getMetadataTreePath(
projectId: string,
nodeId: string,
options: { apiUrl?: string } = {}
) {
const apiUrl = options.apiUrl ?? resolveApiUrl();
const params = new URLSearchParams({ node_id: nodeId });
return getJson<MetadataTreePath>(apiUrl, `/projects/${projectId}/metadata/tree/path?${params.toString()}`);
}
export async function getProjectFlowchart(
projectId: string,
options: { apiUrl?: string; focus?: string | null; depth?: number; limit?: number } = {}
) {
const apiUrl = options.apiUrl ?? resolveApiUrl();
const params = new URLSearchParams();
if (options.focus) {
params.set("focus", options.focus);
}
if (options.depth) {
params.set("depth", String(options.depth));
}
if (options.limit) {
params.set("limit", String(options.limit));
}
const query = params.toString();
return getJson<ProjectFlowchart>(apiUrl, `/projects/${projectId}/flowchart${query ? `?${query}` : ""}`);
}
export async function getProjectVersions(projectId: string, apiUrl = resolveApiUrl()) {
return getJson<ObjectVersionSummary[]>(apiUrl, `/projects/${projectId}/versions`);
}
export async function getLineageVersions(lineageId: string, apiUrl = resolveApiUrl()) {
return getJson<SemanticObjectVersion[]>(apiUrl, `/versions/${encodeURIComponent(lineageId)}`);
}
export async function getVersionDiff(
lineageId: string,
fromVersionId: string,
toVersionId: string,
apiUrl = resolveApiUrl()
) {
const params = new URLSearchParams({
from_version_id: fromVersionId,
to_version_id: toVersionId
});
return getJson<SemanticObjectDiff>(apiUrl, `/versions/${encodeURIComponent(lineageId)}/diff?${params.toString()}`);
}
export async function applyAuthoringChangeSet(
projectId: string,
request: AuthoringApplyChangeSetRequest,
apiUrl = resolveApiUrl()
) {
return postJson<AuthoringApplyChangeSetResponse>(
apiUrl,
`/projects/${projectId}/authoring/apply-change-set`,
request
);
}
export async function getAuthoringSemanticDiffPreview(
projectId: string,
request: AuthoringSemanticDiffPreviewRequest,
apiUrl = resolveApiUrl()
) {
return postOptionalJson<AuthoringSemanticDiffPreview | null>(
apiUrl,
`/projects/${projectId}/authoring/semantic-diff-preview`,
request,
null
);
}
export async function getAuthoringRollbackPreview(
projectId: string,
changeId: string,
apiUrl = resolveApiUrl()
) {
return getJson<AuthoringRollbackPreview>(
apiUrl,
`/projects/${projectId}/authoring/changes/${encodeURIComponent(changeId)}/rollback-preview`
);
}
export async function applyAuthoringRollback(
projectId: string,
changeId: string,
request: AuthoringApplyRollbackRequest,
apiUrl = resolveApiUrl()
) {
return postJson<AuthoringApplyRollbackResponse>(
apiUrl,
`/projects/${projectId}/authoring/changes/${encodeURIComponent(changeId)}/apply-rollback`,
request
);
}
export async function getAuthoringMetadataObjectPreview(
projectId: string,
request: AuthoringMetadataObjectDraft,
apiUrl = resolveApiUrl()
) {
return postJson<AuthoringMetadataObjectPreview>(
apiUrl,
`/projects/${projectId}/authoring/metadata-object-preview`,
request
);
}
export async function applyAuthoringMetadataObject(
projectId: string,
request: AuthoringApplyMetadataObjectRequest,
apiUrl = resolveApiUrl()
) {
return postJson<AuthoringApplyMetadataObjectResponse>(
apiUrl,
`/projects/${projectId}/authoring/apply-metadata-object`,
request
);
}