1193 lines
34 KiB
TypeScript
1193 lines
34 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;
|
|
attributes: Record<string, unknown>;
|
|
};
|
|
|
|
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;
|
|
const port = host.endsWith(":49230") ? "49280" : DEFAULT_API_PORT;
|
|
|
|
return `${protocol}://${apiHostname}:${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()}`);
|
|
}
|
|
|
|
function ownerQualifiedNameForForm(formQualifiedName: string) {
|
|
const parts = formQualifiedName.split(".");
|
|
return parts.length > 1 ? parts.slice(0, -1).join(".") : formQualifiedName;
|
|
}
|
|
|
|
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 selectedFormQualifiedName =
|
|
selectedTreeNode?.kind === "FORM"
|
|
? selectedTreeNode.qualified_name
|
|
: selectedRoutineName && selectedRoutineName.split(".").at(-1)?.toLocaleLowerCase("ru-RU").includes("форма")
|
|
? selectedRoutineName
|
|
: null;
|
|
const selectedObjectName = selectedFormQualifiedName
|
|
? ownerQualifiedNameForForm(selectedFormQualifiedName)
|
|
: 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: selectedTreeNode,
|
|
selectedObjectSchema,
|
|
selectedObjectUi,
|
|
selectedObjectImpact,
|
|
snapshot,
|
|
exportSnapshot,
|
|
report,
|
|
review,
|
|
coverage,
|
|
forms,
|
|
integrations,
|
|
jobs,
|
|
flowchart,
|
|
authoringPreview,
|
|
semanticDiffPreview,
|
|
authoringChanges,
|
|
projectVersions,
|
|
editorSourceText: authoringSourceText || fallbackSourceText,
|
|
editorProposedText: authoringProposedText,
|
|
editorSelectedObject,
|
|
editorSelectedRoutine,
|
|
editorSelectedForm: selectedFormQualifiedName,
|
|
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
|
|
);
|
|
}
|