544 lines
16 KiB
TypeScript
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;
|
|
}
|