Files
govoplan-reporting/webui/src/api/reporting.ts
T

544 lines
16 KiB
TypeScript

import {
apiFetch,
apiPath,
apiUrl,
authHeaders,
type ApiSettings
} from "@govoplan/core-webui";
export type ReportingDefinitionKind = "dataset" | "semantic_model" | "report" | "quality_plan";
export type ReportingQueryMode = "summary" | "detail" | "pivot";
export type ReportingQuery = {
mode: ReportingQueryMode;
dimensions: string[];
measures: string[];
filters: Array<Record<string, unknown>>;
sort: Array<{ key: string; direction: "asc" | "desc" }>;
pivot?: {
rows: string[];
columns: string[];
measures: string[];
include_totals: boolean;
} | null;
offset: number;
limit: number;
};
export type ReportingDefinition = {
tenant_id: string;
definition_kind: ReportingDefinitionKind;
definition_id: string;
definition_key: string;
revision: number;
name: string;
description?: string | null;
status: "draft" | "active" | "retired";
visibility: "tenant" | "restricted";
content_hash: string;
parent_kind?: ReportingDefinitionKind | null;
parent_id?: string | null;
parent_revision?: number | null;
recorded_at: string;
change_reason: string;
payload: Record<string, unknown>;
};
export type SemanticModelPayload = {
dataset_id: string;
dataset_revision: number;
dimensions: Array<{ key: string; field: string; label: string }>;
measures: Array<{ key: string; label: string; aggregation: string }>;
default_dimensions: string[];
default_measures: string[];
};
export type ReportPayload = {
semantic_model_id: string;
semantic_model_revision: number;
parameters: Array<{
key: string;
label: string;
type: string;
required: boolean;
default?: unknown;
allowed_values: unknown[];
}>;
default_query: ReportingQuery;
visualization: {
kind: string;
category_dimension?: string | null;
series_dimension?: string | null;
measures: string[];
tabular_fallback: boolean;
};
institutional_references: Array<Record<string, unknown>>;
};
export type ReportExecution = {
execution_id: string;
report_id: string;
report_revision: number;
semantic_model_id: string;
semantic_model_revision: number;
dataset_id: string;
dataset_revision: number;
status: "running" | "succeeded" | "failed";
parameters: Record<string, unknown>;
query: ReportingQuery;
definition_hashes: Record<string, string>;
source_fingerprints: Array<Record<string, unknown>>;
output_hash?: string | null;
executor_version?: string | null;
schema: Array<{ name: string; type: string }>;
rows: Array<Record<string, unknown>>;
total_rows: number;
truncated: boolean;
diagnostics: Array<{ severity?: string; code?: string; message?: string }>;
provenance: Record<string, unknown>;
started_at: string;
finished_at?: string | null;
visualization?: {
kind: string;
requested_kind?: string;
category?: string | null;
series?: string | null;
measures?: string[];
options?: Record<string, unknown>;
fallback_reason?: string | null;
};
delivery_authorization?: Record<string, unknown>;
};
export type ReportingDrillContext = {
token: string;
drill_context_id: string;
execution_id: string;
dimension_path: Array<{ dimension: string; label: string; value: unknown }>;
expires_at: string;
};
export type ReportingDrillResult = Omit<ReportingDrillContext, "token"> & {
rows: Array<Record<string, unknown>>;
schema: Array<{ name: string; type: string }>;
total_rows: number;
truncated: boolean;
source_fingerprints: Array<Record<string, unknown>>;
policy_provenance: Record<string, unknown>;
};
export type ReportingSchedule = {
schedule_id: string;
report_id: string;
report_revision: number;
name: string;
revision: number;
trigger_kind: "scheduled" | "interval";
trigger_config: Record<string, unknown>;
parameters: Record<string, unknown>;
query: ReportingQuery;
publication_target: Record<string, unknown>;
enabled: boolean;
next_run_at?: string | null;
last_run_at?: string | null;
last_execution_id?: string | null;
};
export type ReportingPublicationTarget = {
capability: string;
label: string;
available: boolean;
reason?: string | null;
formats: string[];
target_label: string;
target_required: boolean;
required_options: string[];
};
export type ReportingPublication = {
publication_id: string;
execution_id: string;
target_capability: string;
target_ref?: string | null;
format: string;
status: string;
evidence: Record<string, unknown>;
error?: string | null;
completed_at?: string | null;
};
export type ReportingSavedView = {
view_id: string;
report_id: string;
report_revision: number;
name: string;
revision: number;
state: { query?: ReportingQuery };
shared: boolean;
updated_at: string;
};
export type ProviderReportParameter = {
key: string;
label: string;
type: string;
required: boolean;
description?: string | null;
options_from_provider: boolean;
};
export type ProviderReportField = {
path: string;
label: string;
type: string;
group: string;
nullable: boolean;
sensitive: boolean;
};
export type ProviderReportDescriptor = {
contract_version: string;
provider_id: string;
report_id: string;
revision: string;
title: string;
summary: string;
parameters: ProviderReportParameter[];
result_schema: ProviderReportField[];
privacy_transforms: Array<{ id: string; label: string; required: boolean }>;
purpose_required: boolean;
audience_scope_required: boolean;
retention_class: string;
export_formats: Array<"json" | "csv">;
reidentification_risk: "low" | "moderate" | "high";
presentation: { kind?: string };
available: boolean;
unavailable_reason?: string | null;
governance: {
retention_days?: number | null;
export_formats: string[];
required_privacy_transforms: string[];
provenance: Record<string, unknown>;
};
};
export type ProviderReportExecution = {
execution_id: string;
provider_id: string;
report_id: string;
report_revision: string;
contract_version: string;
purpose: string;
audience_scope: Record<string, unknown>;
parameters: Record<string, unknown>;
result_schema: ProviderReportField[];
result: Record<string, unknown>;
source_revisions: Array<Record<string, unknown>>;
effective_scope: Record<string, unknown>;
privacy_transforms: string[];
provenance: Record<string, unknown>;
governance_provenance: Record<string, unknown>;
retention_class: string;
retention_days?: number | null;
expires_at?: string | null;
output_hash: string;
generated_at: string;
actor_id?: string | null;
};
export function listDefinitions(
settings: ApiSettings,
options: { kinds?: ReportingDefinitionKind[]; status?: string[]; query?: string; limit?: number },
signal?: AbortSignal
): Promise<{ definitions: ReportingDefinition[]; total: number }> {
return apiFetch(settings, apiPath("/api/v1/reporting/definitions", {
definition_kind: options.kinds,
status: options.status,
query: options.query,
limit: options.limit ?? 200
}), { signal });
}
export function listProviderReports(
settings: ApiSettings,
signal?: AbortSignal
): Promise<{ reports: ProviderReportDescriptor[]; diagnostics: Array<Record<string, string>> }> {
return apiFetch(settings, "/api/v1/reporting/provider-reports", { signal });
}
export function listProviderParameterOptions(
settings: ApiSettings,
report: ProviderReportDescriptor,
parameterKey: string,
query = "",
signal?: AbortSignal
): Promise<{ options: Array<{ value: string; label: string; description?: string | null }> }> {
return apiFetch(settings, apiPath(
`/api/v1/reporting/provider-reports/${encodeURIComponent(report.provider_id)}/${encodeURIComponent(report.report_id)}/parameters/${encodeURIComponent(parameterKey)}/options`,
{ query, limit: 200 }
), { signal });
}
export function runProviderReport(
settings: ApiSettings,
report: ProviderReportDescriptor,
parameters: Record<string, unknown>,
purpose: string,
audienceScope: Record<string, unknown>
): Promise<ProviderReportExecution> {
return apiFetch(
settings,
`/api/v1/reporting/provider-reports/${encodeURIComponent(report.provider_id)}/${encodeURIComponent(report.report_id)}/executions`,
{
method: "POST",
body: JSON.stringify({
parameters,
purpose,
audience_scope: audienceScope,
idempotency_key: crypto.randomUUID()
})
}
);
}
export async function downloadProviderExecution(
settings: ApiSettings,
execution: ProviderReportExecution,
format: "json" | "csv",
purpose: string,
audienceScope: Record<string, unknown>
): Promise<void> {
const response = await fetch(
apiUrl(settings, `/api/v1/reporting/provider-executions/${encodeURIComponent(execution.execution_id)}/exports`),
{
method: "POST",
headers: { ...authHeaders(settings), "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ format, purpose, audience_scope: audienceScope })
}
);
if (!response.ok) throw new Error(await response.text());
const blob = await response.blob();
const disposition = response.headers.get("Content-Disposition") ?? "";
const filename = disposition.match(/filename="?([^";]+)"?/)?.[1] ?? `provider-report.${format}`;
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
export function getDefinition(
settings: ApiSettings,
kind: ReportingDefinitionKind,
id: string,
revision?: number,
signal?: AbortSignal
): Promise<ReportingDefinition> {
return apiFetch(settings, apiPath(
`/api/v1/reporting/definitions/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`,
{ revision }
), { signal });
}
export function runReport(
settings: ApiSettings,
report: ReportingDefinition,
query: ReportingQuery,
parameters: Record<string, unknown>
): Promise<ReportExecution> {
return apiFetch(settings, `/api/v1/reporting/reports/${encodeURIComponent(report.definition_id)}/executions`, {
method: "POST",
body: JSON.stringify({
report_revision: report.revision,
parameters,
query,
idempotency_key: crypto.randomUUID()
})
});
}
export function listExecutions(
settings: ApiSettings,
reportId: string,
signal?: AbortSignal
): Promise<{ executions: ReportExecution[] }> {
return apiFetch(settings, `/api/v1/reporting/reports/${encodeURIComponent(reportId)}/executions?limit=30`, { signal });
}
export function createDrillContext(
settings: ApiSettings,
executionId: string,
aggregateRow: Record<string, unknown>,
limit = 200
): Promise<ReportingDrillContext> {
return apiFetch(settings, `/api/v1/reporting/executions/${encodeURIComponent(executionId)}/drill-contexts`, {
method: "POST",
body: JSON.stringify({ aggregate_row: aggregateRow, limit })
});
}
export function resolveDrillContext(
settings: ApiSettings,
token: string,
signal?: AbortSignal
): Promise<ReportingDrillResult> {
return apiFetch(settings, `/api/v1/reporting/drill-contexts/${encodeURIComponent(token)}`, { signal });
}
export function listSavedViews(
settings: ApiSettings,
reportId: string,
signal?: AbortSignal
): Promise<{ views: ReportingSavedView[] }> {
return apiFetch(settings, `/api/v1/reporting/reports/${encodeURIComponent(reportId)}/saved-views`, { signal });
}
export function saveView(
settings: ApiSettings,
report: ReportingDefinition,
name: string,
query: ReportingQuery
): Promise<ReportingSavedView> {
const viewId = crypto.randomUUID();
return apiFetch(settings, `/api/v1/reporting/reports/${encodeURIComponent(report.definition_id)}/saved-views/${viewId}`, {
method: "PUT",
body: JSON.stringify({
view_id: viewId,
report_revision: report.revision,
name,
state: { query },
shared: false,
access: {},
expected_revision: null
})
});
}
export function createIntervalSchedule(
settings: ApiSettings,
report: ReportingDefinition,
name: string,
seconds: number,
query: ReportingQuery,
parameters: Record<string, unknown>
): Promise<Record<string, unknown>> {
const scheduleId = crypto.randomUUID();
return apiFetch(settings, `/api/v1/reporting/schedules/${scheduleId}`, {
method: "PUT",
body: JSON.stringify({
schedule_id: scheduleId,
report_id: report.definition_id,
report_revision: report.revision,
name,
trigger_kind: "interval",
trigger_config: { seconds },
parameters,
query,
publication_target: {},
enabled: true,
next_run_at: null,
expected_revision: null
})
});
}
export function listSchedules(
settings: ApiSettings,
reportId: string,
signal?: AbortSignal
): Promise<{ schedules: ReportingSchedule[] }> {
return apiFetch(settings, apiPath("/api/v1/reporting/schedules", { report_id: reportId }), { signal });
}
export function updateSchedule(
settings: ApiSettings,
schedule: ReportingSchedule,
changes: Partial<Pick<ReportingSchedule, "enabled" | "name">>
): Promise<ReportingSchedule> {
return apiFetch(settings, `/api/v1/reporting/schedules/${encodeURIComponent(schedule.schedule_id)}`, {
method: "PUT",
body: JSON.stringify({
schedule_id: schedule.schedule_id,
report_id: schedule.report_id,
report_revision: schedule.report_revision,
name: changes.name ?? schedule.name,
trigger_kind: schedule.trigger_kind,
trigger_config: schedule.trigger_config,
parameters: schedule.parameters,
query: schedule.query,
publication_target: schedule.publication_target,
enabled: changes.enabled ?? schedule.enabled,
next_run_at: schedule.next_run_at ?? null,
expected_revision: schedule.revision
})
});
}
export function listPublicationTargets(
settings: ApiSettings,
signal?: AbortSignal
): Promise<{ targets: ReportingPublicationTarget[] }> {
return apiFetch(settings, "/api/v1/reporting/publication-targets", { signal });
}
export function listPublications(
settings: ApiSettings,
executionId: string,
signal?: AbortSignal
): Promise<{ publications: ReportingPublication[] }> {
return apiFetch(settings, apiPath("/api/v1/reporting/publications", { execution_id: executionId }), { signal });
}
export function publishExecution(
settings: ApiSettings,
executionId: string,
request: {
target_capability: string;
target_ref?: string | null;
format: string;
options: Record<string, unknown>;
}
): Promise<ReportingPublication> {
return apiFetch(settings, `/api/v1/reporting/executions/${encodeURIComponent(executionId)}/publications`, {
method: "POST",
body: JSON.stringify({
...request,
idempotency_key: crypto.randomUUID()
})
});
}
export async function downloadExecution(
settings: ApiSettings,
executionId: string,
format: "csv" | "json"
): Promise<void> {
const response = await fetch(apiUrl(settings, apiPath(
`/api/v1/reporting/executions/${encodeURIComponent(executionId)}/export`,
{ format }
)), {
headers: authHeaders(settings),
credentials: "include"
});
if (!response.ok) throw new Error(await response.text() || "The report export failed.");
const blob = await response.blob();
const disposition = response.headers.get("content-disposition") ?? "";
const filename = disposition.match(/filename="?([^";]+)"?/i)?.[1] ?? `report.${format}`;
const href = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = href;
link.download = filename;
link.click();
URL.revokeObjectURL(href);
}
export function reportPayload(definition: ReportingDefinition): ReportPayload {
return definition.payload as unknown as ReportPayload;
}
export function semanticPayload(definition: ReportingDefinition): SemanticModelPayload {
return definition.payload as unknown as SemanticModelPayload;
}