591 lines
18 KiB
TypeScript
591 lines
18 KiB
TypeScript
import { apiFetch, assertSameOriginRequestUrl, BASE } from "./client";
|
|
import { joinBackendPath } from "./runtime-config";
|
|
|
|
export type DatabaseTransport = "postgres_direct" | "rest_api" | "ssh_tunnel";
|
|
export type ConnectionStatus = "untested" | "reachable" | "failed";
|
|
export type WorkspaceEvidenceState =
|
|
| "not_declared"
|
|
| "materialized_current_revision"
|
|
| "configuration_required"
|
|
| "configured_unverified"
|
|
| "workspace_unavailable";
|
|
export type CatalogSecretName =
|
|
| "password"
|
|
| "apiKey"
|
|
| "sshPrivateKey"
|
|
| "sshPrivateKeyPassphrase"
|
|
| "sshKnownHosts"
|
|
| "tlsCa";
|
|
|
|
export interface DatabaseBinding {
|
|
transport: DatabaseTransport;
|
|
host?: string;
|
|
port?: number;
|
|
username?: string;
|
|
baseUrl?: string;
|
|
restPath?: string;
|
|
restAuth?: "none" | "bearer" | "x-api-key";
|
|
tlsServername?: string;
|
|
sshHost?: string;
|
|
sshPort?: number;
|
|
sshUsername?: string;
|
|
sshTargetHost?: string;
|
|
sshTargetPort?: number;
|
|
}
|
|
|
|
export interface CatalogDatabase {
|
|
id?: string;
|
|
workspaceId: string;
|
|
workspaceName: string;
|
|
workspaceDescription?: string;
|
|
workspaceAvailable: boolean;
|
|
workspaceRevision: { commit: string; blob: string } | null;
|
|
workspaceEvidence: {
|
|
sourceType: "filesystem" | "http" | "s3" | null;
|
|
state: WorkspaceEvidenceState;
|
|
};
|
|
runtimeBinding: {
|
|
transport: DatabaseTransport;
|
|
configurationState: "ready" | "configuration_required";
|
|
sessionTransportSupported: boolean;
|
|
} | null;
|
|
configured: boolean;
|
|
engine: "postgres";
|
|
databaseName: string;
|
|
schema: string;
|
|
version: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
binding: DatabaseBinding;
|
|
connectionStatus: ConnectionStatus;
|
|
testedVersion?: number;
|
|
lastTestedAt?: string;
|
|
lastErrorCode?: string;
|
|
lastErrorMessage?: string;
|
|
schemaSyncedVersion?: number;
|
|
schemaSyncedAt?: string;
|
|
activeSyncRun?: CatalogSyncRun;
|
|
secrets: Record<CatalogSecretName, boolean>;
|
|
}
|
|
|
|
export interface CatalogMetrics {
|
|
scope: "global" | "database";
|
|
databaseId: string | null;
|
|
tables: number;
|
|
columns: number;
|
|
sensitiveColumns: number;
|
|
relationships: number;
|
|
descriptionTargets: number;
|
|
describedTargets: number;
|
|
descriptionCoverage: number;
|
|
updatedAt: string | null;
|
|
}
|
|
|
|
export interface DatabaseConfiguration {
|
|
workspaceId: string;
|
|
engine: "postgres";
|
|
databaseName: string;
|
|
schema: string;
|
|
binding: DatabaseBinding;
|
|
}
|
|
|
|
export interface CatalogTable {
|
|
id: string;
|
|
databaseId: string;
|
|
name: string;
|
|
sourceComment: string | null;
|
|
description: string | null;
|
|
generatedDescription: string | null;
|
|
lastSyncedDatabaseVersion?: number | null;
|
|
lastSyncedAt?: string | null;
|
|
version: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface CatalogColumn {
|
|
id: string;
|
|
tableId: string;
|
|
name: string;
|
|
ordinalPosition: number;
|
|
dataType: string;
|
|
isNullable: boolean;
|
|
defaultExpression: string | null;
|
|
primaryKeyPosition: number | null;
|
|
isPrimaryKey: boolean;
|
|
isForeignKey: boolean;
|
|
foreignKeyCount: number;
|
|
sourceComment: string | null;
|
|
description: string | null;
|
|
generatedDescription: string | null;
|
|
sensitive: boolean;
|
|
lastSyncedDatabaseVersion: number | null;
|
|
lastSyncedAt: string | null;
|
|
version: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface SensitivityReviewItem {
|
|
columnId: string;
|
|
tableId: string;
|
|
tableName: string;
|
|
columnName: string;
|
|
version: number;
|
|
currentSensitive: boolean;
|
|
sensitive: boolean;
|
|
assessment: "sensitive" | "non_sensitive";
|
|
evidence: Array<{
|
|
kind: "metadata" | "content" | "length" | "ner" | "coverage" | "type";
|
|
ruleId: string;
|
|
label?: string;
|
|
confidence?: number;
|
|
}>;
|
|
observedValues: number;
|
|
coverage: "metadata" | "complete" | "sampled" | "no_values";
|
|
}
|
|
|
|
export interface SensitivityAnalysisResult {
|
|
suggestions: SensitivityReviewItem[];
|
|
run: SensitivityAnalysisRun;
|
|
}
|
|
|
|
export type SensitivityAnalysisRequest =
|
|
| { scope: "all" }
|
|
| { scope: "selected_tables"; targetIds: string[] }
|
|
| { scope: "selected_columns"; targetIds: string[] };
|
|
|
|
export type SensitivityAnalysisStatus = "running" | "completed" | "failed" | "interrupted";
|
|
|
|
export interface SensitivityAnalysisRun {
|
|
id: string;
|
|
databaseId: string;
|
|
scope: SensitivityAnalysisRequest["scope"];
|
|
engine: "llm" | "local";
|
|
modelId: string | null;
|
|
policyVersion: string | null;
|
|
status: SensitivityAnalysisStatus;
|
|
total: number;
|
|
suggestedSensitive: number;
|
|
suggestedNonSensitive: number;
|
|
unknown: number;
|
|
inputTokens?: number;
|
|
cacheReadTokens?: number;
|
|
outputTokens?: number;
|
|
createdAt: string;
|
|
startedAt: string | null;
|
|
updatedAt: string;
|
|
finishedAt: string | null;
|
|
errorSummary: string | null;
|
|
}
|
|
|
|
export interface SensitivityAnalysisEvent {
|
|
runId: string;
|
|
sequence: number;
|
|
level: "info" | "warning" | "error";
|
|
message: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface CatalogRelationshipColumn {
|
|
position: number;
|
|
sourceColumnId: string;
|
|
sourceColumnName: string;
|
|
targetColumnId: string;
|
|
targetColumnName: string;
|
|
}
|
|
|
|
export type CatalogRelationshipOrigin = "physical" | "generated" | "manual";
|
|
export type CatalogRelationshipStatus = "active" | "excluded";
|
|
|
|
export interface CatalogRelationship {
|
|
id: string;
|
|
databaseId: string;
|
|
constraintName: string | null;
|
|
sourceTableId: string;
|
|
sourceTableName: string;
|
|
targetTableId: string;
|
|
targetTableName: string;
|
|
updateRule: string | null;
|
|
deleteRule: string | null;
|
|
deferrable: boolean;
|
|
initiallyDeferred: boolean;
|
|
columns: CatalogRelationshipColumn[];
|
|
origin: CatalogRelationshipOrigin;
|
|
status: CatalogRelationshipStatus;
|
|
lastSyncedDatabaseVersion: number | null;
|
|
lastSyncedAt: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface CatalogRelationshipRebuildResult {
|
|
added: number;
|
|
alreadyPresent: number;
|
|
excluded: number;
|
|
ambiguous: number;
|
|
}
|
|
|
|
export type CatalogDatabaseMetadataDeleteTarget = "tables" | "relationships";
|
|
export type CatalogTableMetadataDeleteTarget = "columns" | "relationships";
|
|
|
|
export interface CatalogMetadataDeleteCounts {
|
|
tables: number;
|
|
columns: number;
|
|
relationships: number;
|
|
}
|
|
|
|
export type CatalogDescriptionTarget = "tables" | "columns";
|
|
|
|
export interface CatalogDescriptionConsolidationCounts {
|
|
copied: number;
|
|
skipped: number;
|
|
}
|
|
|
|
export type CatalogSyncScope = "tables" | "columns" | "relationships" | "all";
|
|
export type CatalogSyncState = "queued" | "running" | "awaiting_confirmation" | "applying"
|
|
| "succeeded" | "failed" | "cancelled" | "interrupted";
|
|
export type CatalogSyncPhase = "queued" | "connecting" | "scanning_tables" | "scanning_columns"
|
|
| "scanning_relationships" | "planning" | "awaiting_confirmation" | "applying" | "completed";
|
|
|
|
export interface CatalogSchemaDiff {
|
|
deletedTables: string[];
|
|
deletedColumns: Array<{ tableName: string; columnName: string }>;
|
|
deletedRelationships: Array<{ sourceTableName: string; constraintName: string }>;
|
|
}
|
|
|
|
export interface CatalogSyncRun {
|
|
id: string;
|
|
databaseId: string;
|
|
scope: CatalogSyncScope;
|
|
tableIds: string[];
|
|
state: CatalogSyncState;
|
|
phase: CatalogSyncPhase;
|
|
requestedDatabaseVersion: number;
|
|
plannedDiff: CatalogSchemaDiff | null;
|
|
confirmationToken: string | null;
|
|
counts: { tables?: number; columns?: number; relationships?: number; created?: number; updated?: number; deleted?: number };
|
|
errorCode: string | null;
|
|
errorMessage: string | null;
|
|
cancelRequested: boolean;
|
|
createdAt: string;
|
|
startedAt: string | null;
|
|
updatedAt: string;
|
|
finishedAt: string | null;
|
|
heartbeatAt: string | null;
|
|
}
|
|
|
|
export interface CatalogSyncEvent {
|
|
id: number;
|
|
runId: string;
|
|
sequence: number;
|
|
level: "info" | "warning" | "error";
|
|
eventType: string;
|
|
message: string;
|
|
data: Record<string, unknown>;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface MetadataGenerationModel {
|
|
id: string;
|
|
label: string;
|
|
}
|
|
|
|
export interface MetadataGenerationModels {
|
|
models: MetadataGenerationModel[];
|
|
default: string | null;
|
|
}
|
|
|
|
export type DescriptionGenerationScope = "selected_columns" | "selected_tables" | "all" | "missing";
|
|
export type DescriptionGenerationStatus = "queued" | "running" | "completed"
|
|
| "completed_with_errors" | "cancelled" | "failed" | "interrupted";
|
|
|
|
export interface DescriptionGenerationRun {
|
|
id: string;
|
|
databaseId: string;
|
|
scope: DescriptionGenerationScope;
|
|
modelId: string;
|
|
language: "en" | "it";
|
|
status: DescriptionGenerationStatus;
|
|
total: number;
|
|
processed: number;
|
|
generated: number;
|
|
nonGeneratable: number;
|
|
failed: number;
|
|
inputTokens?: number;
|
|
cacheReadTokens?: number;
|
|
outputTokens?: number;
|
|
createdAt: string;
|
|
startedAt: string | null;
|
|
updatedAt: string;
|
|
finishedAt: string | null;
|
|
errorSummary: string | null;
|
|
}
|
|
|
|
export interface DescriptionGenerationEvent {
|
|
sequence: number;
|
|
level: "info" | "warning" | "error";
|
|
message: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export const listCatalogDatabases = () => apiFetch<CatalogDatabase[]>("/catalog/databases");
|
|
|
|
export const getCatalogMetrics = (databaseId?: string) => apiFetch<CatalogMetrics>(
|
|
databaseId
|
|
? `/catalog/metrics?databaseId=${encodeURIComponent(databaseId)}`
|
|
: "/catalog/metrics",
|
|
);
|
|
|
|
export const listMetadataGenerationModels = () =>
|
|
apiFetch<MetadataGenerationModels>("/catalog/metadata-generation/models");
|
|
|
|
export function startDescriptionGenerationRun(
|
|
databaseId: string,
|
|
modelId: string,
|
|
scope: Extract<DescriptionGenerationScope, "selected_columns" | "selected_tables">,
|
|
targetIds: string[],
|
|
): Promise<DescriptionGenerationRun>;
|
|
export function startDescriptionGenerationRun(
|
|
databaseId: string,
|
|
modelId: string,
|
|
scope: Extract<DescriptionGenerationScope, "all" | "missing">,
|
|
): Promise<DescriptionGenerationRun>;
|
|
export function startDescriptionGenerationRun(
|
|
databaseId: string,
|
|
modelId: string,
|
|
scope: DescriptionGenerationScope,
|
|
targetIds?: string[],
|
|
): Promise<DescriptionGenerationRun> {
|
|
const body = scope === "all" || scope === "missing"
|
|
? { modelId, scope }
|
|
: { modelId, scope, targetIds: targetIds ?? [] };
|
|
return apiFetch<DescriptionGenerationRun>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/description-generation-runs`,
|
|
{ method: "POST", body: JSON.stringify(body) },
|
|
);
|
|
}
|
|
|
|
export const getDescriptionGenerationRun = (runId: string) =>
|
|
apiFetch<DescriptionGenerationRun>(
|
|
`/catalog/description-generation-runs/${encodeURIComponent(runId)}`,
|
|
);
|
|
|
|
export const listDescriptionGenerationRuns = (limit = 50) =>
|
|
apiFetch<DescriptionGenerationRun[]>(
|
|
`/catalog/description-generation-runs?limit=${encodeURIComponent(String(limit))}`,
|
|
);
|
|
|
|
export const listDescriptionGenerationEvents = (runId: string, after = 0) =>
|
|
apiFetch<DescriptionGenerationEvent[]>(
|
|
`/catalog/description-generation-runs/${encodeURIComponent(runId)}/events-list?after=${after}`,
|
|
);
|
|
|
|
export const cancelDescriptionGenerationRun = (runId: string) =>
|
|
apiFetch<DescriptionGenerationRun>(
|
|
`/catalog/description-generation-runs/${encodeURIComponent(runId)}/cancel`,
|
|
{ method: "POST" },
|
|
);
|
|
|
|
export const unlockDescriptionGenerationRun = () =>
|
|
apiFetch<DescriptionGenerationRun>(
|
|
"/catalog/description-generation-runs/unlock",
|
|
{ method: "POST" },
|
|
);
|
|
|
|
export function descriptionGenerationEventsUrl(runId: string, after = 0): string {
|
|
const url = joinBackendPath(
|
|
BASE,
|
|
`/catalog/description-generation-runs/${encodeURIComponent(runId)}/events?after=${after}`,
|
|
);
|
|
assertSameOriginRequestUrl(url);
|
|
return url;
|
|
}
|
|
|
|
export const createCatalogDatabase = (input: DatabaseConfiguration) =>
|
|
apiFetch<CatalogDatabase>("/catalog/databases", { method: "POST", body: JSON.stringify(input) });
|
|
|
|
export const updateCatalogDatabase = (id: string, version: number, input: DatabaseConfiguration) =>
|
|
apiFetch<CatalogDatabase>(`/catalog/databases/${encodeURIComponent(id)}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ ...input, version }),
|
|
});
|
|
|
|
export const replaceCatalogDatabaseSecrets = (
|
|
id: string,
|
|
version: number,
|
|
values: Partial<Record<CatalogSecretName, string>>,
|
|
) => apiFetch<CatalogDatabase>(`/catalog/databases/${encodeURIComponent(id)}/secrets`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ version, values }),
|
|
});
|
|
|
|
export const testCatalogDatabase = (id: string, version: number) =>
|
|
apiFetch<CatalogDatabase>(`/catalog/databases/${encodeURIComponent(id)}/test`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ version }),
|
|
});
|
|
|
|
export const deleteCatalogDatabase = (id: string, version: number) =>
|
|
apiFetch<void>(`/catalog/databases/${encodeURIComponent(id)}?version=${version}`, { method: "DELETE" });
|
|
|
|
export const listCatalogTables = (databaseId: string) =>
|
|
apiFetch<CatalogTable[]>(`/catalog/databases/${encodeURIComponent(databaseId)}/tables`);
|
|
|
|
export const updateCatalogTableMetadata = (
|
|
databaseId: string,
|
|
tableId: string,
|
|
version: number,
|
|
description: string | null,
|
|
generatedDescription: string | null,
|
|
) => apiFetch<CatalogTable>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/tables/${encodeURIComponent(tableId)}`,
|
|
{ method: "PATCH", body: JSON.stringify({ version, description, generatedDescription }) },
|
|
);
|
|
|
|
export const listCatalogColumns = (databaseId: string, tableId: string) =>
|
|
apiFetch<CatalogColumn[]>(`/catalog/databases/${encodeURIComponent(databaseId)}/tables/${encodeURIComponent(tableId)}/columns`);
|
|
|
|
export const updateCatalogColumnMetadata = (
|
|
databaseId: string,
|
|
tableId: string,
|
|
columnId: string,
|
|
version: number,
|
|
description: string | null,
|
|
generatedDescription: string | null,
|
|
sensitive?: boolean,
|
|
) => apiFetch<CatalogColumn>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/tables/${encodeURIComponent(tableId)}/columns/${encodeURIComponent(columnId)}`,
|
|
{ method: "PATCH", body: JSON.stringify({ version, description, generatedDescription, sensitive }) },
|
|
);
|
|
|
|
export const updateCatalogColumnSensitive = (
|
|
databaseId: string,
|
|
tableId: string,
|
|
columnId: string,
|
|
version: number,
|
|
sensitive: boolean,
|
|
) => apiFetch<CatalogColumn>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/tables/${encodeURIComponent(tableId)}/columns/${encodeURIComponent(columnId)}`,
|
|
{ method: "PATCH", body: JSON.stringify({ version, sensitive }) },
|
|
);
|
|
|
|
export const runSensitivityAnalysis = (
|
|
databaseId: string,
|
|
selection: SensitivityAnalysisRequest,
|
|
) =>
|
|
apiFetch<SensitivityAnalysisResult>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/sensitive-data-suggestions`,
|
|
{ method: "POST", body: JSON.stringify(selection) },
|
|
);
|
|
|
|
export const getSensitivityAnalysisRun = (runId: string) =>
|
|
apiFetch<SensitivityAnalysisRun>(
|
|
`/catalog/sensitive-data-suggestion-runs/${encodeURIComponent(runId)}`,
|
|
);
|
|
|
|
export const listSensitivityAnalysisRuns = (limit = 50) =>
|
|
apiFetch<SensitivityAnalysisRun[]>(
|
|
`/catalog/sensitive-data-suggestion-runs?limit=${encodeURIComponent(String(limit))}`,
|
|
);
|
|
|
|
export const listSensitivityAnalysisEvents = (runId: string, after = 0) =>
|
|
apiFetch<SensitivityAnalysisEvent[]>(
|
|
`/catalog/sensitive-data-suggestion-runs/${encodeURIComponent(runId)}/events-list?after=${after}`,
|
|
);
|
|
|
|
export const listCatalogRelationships = (databaseId: string) =>
|
|
apiFetch<CatalogRelationship[]>(`/catalog/databases/${encodeURIComponent(databaseId)}/relationships`);
|
|
|
|
export const createCatalogRelationship = (
|
|
databaseId: string,
|
|
sourceColumnId: string,
|
|
targetColumnId: string,
|
|
) => apiFetch<CatalogRelationship>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/relationships`,
|
|
{ method: "POST", body: JSON.stringify({ sourceColumnId, targetColumnId }) },
|
|
);
|
|
|
|
export const rebuildGeneratedRelationships = (databaseId: string) =>
|
|
apiFetch<CatalogRelationshipRebuildResult>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/relationships/rebuild-generated`,
|
|
{ method: "POST" },
|
|
);
|
|
|
|
export const setCatalogRelationshipStatus = (
|
|
databaseId: string,
|
|
relationshipId: string,
|
|
status: CatalogRelationshipStatus,
|
|
) => apiFetch<CatalogRelationship>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/relationships/${encodeURIComponent(relationshipId)}`,
|
|
{ method: "PATCH", body: JSON.stringify({ status }) },
|
|
);
|
|
|
|
export const deleteCatalogRelationship = (databaseId: string, relationshipId: string) =>
|
|
apiFetch<void>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/relationships/${encodeURIComponent(relationshipId)}`,
|
|
{ method: "DELETE" },
|
|
);
|
|
|
|
export const deleteCatalogDatabaseMetadata = (
|
|
databaseIds: string[],
|
|
target: CatalogDatabaseMetadataDeleteTarget,
|
|
) => apiFetch<CatalogMetadataDeleteCounts>("/catalog/databases/metadata-cleanup", {
|
|
method: "POST",
|
|
body: JSON.stringify({ databaseIds, target }),
|
|
});
|
|
|
|
export const deleteCatalogTableMetadata = (
|
|
databaseId: string,
|
|
tableIds: string[],
|
|
target: CatalogTableMetadataDeleteTarget,
|
|
) => apiFetch<CatalogMetadataDeleteCounts>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/tables/metadata-cleanup`,
|
|
{ method: "POST", body: JSON.stringify({ tableIds, target }) },
|
|
);
|
|
|
|
export const consolidateCatalogDescriptions = (
|
|
databaseId: string,
|
|
target: CatalogDescriptionTarget,
|
|
targetIds: string[],
|
|
) => apiFetch<CatalogDescriptionConsolidationCounts>(
|
|
`/catalog/databases/${encodeURIComponent(databaseId)}/descriptions/consolidate`,
|
|
{ method: "POST", body: JSON.stringify({ target, targetIds }) },
|
|
);
|
|
|
|
export const startCatalogSync = (
|
|
databaseId: string,
|
|
version: number,
|
|
scope: CatalogSyncScope,
|
|
tableIds: string[] = [],
|
|
) => apiFetch<CatalogSyncRun>(`/catalog/databases/${encodeURIComponent(databaseId)}/sync-runs`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ version, scope, tableIds }),
|
|
});
|
|
|
|
export const listCatalogSyncRuns = (databaseId: string) =>
|
|
apiFetch<CatalogSyncRun[]>(`/catalog/databases/${encodeURIComponent(databaseId)}/sync-runs`);
|
|
|
|
export const getCatalogSyncRun = (runId: string) =>
|
|
apiFetch<CatalogSyncRun>(`/catalog/sync-runs/${encodeURIComponent(runId)}`);
|
|
|
|
export const listCatalogSyncEvents = (runId: string, after = 0) =>
|
|
apiFetch<CatalogSyncEvent[]>(`/catalog/sync-runs/${encodeURIComponent(runId)}/events-list?after=${after}`);
|
|
|
|
export const confirmCatalogSync = (runId: string, confirmationToken: string) =>
|
|
apiFetch<CatalogSyncRun>(`/catalog/sync-runs/${encodeURIComponent(runId)}/confirm`, {
|
|
method: "POST", body: JSON.stringify({ confirmationToken }),
|
|
});
|
|
|
|
export const cancelCatalogSync = (runId: string) =>
|
|
apiFetch<CatalogSyncRun>(`/catalog/sync-runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
|
|
|
|
export const retryCatalogSync = (runId: string) =>
|
|
apiFetch<CatalogSyncRun>(`/catalog/sync-runs/${encodeURIComponent(runId)}/retry`, { method: "POST" });
|
|
|
|
export function catalogSyncEventsUrl(runId: string, after = 0): string {
|
|
const url = joinBackendPath(BASE, `/catalog/sync-runs/${encodeURIComponent(runId)}/events?after=${after}`);
|
|
assertSameOriginRequestUrl(url);
|
|
return url;
|
|
}
|