feat: ship loose-offline dbsync (validate, agent push, LWW audit)
Add UUID/FK channel checks, agent whitelist/push APIs, bindings, super-admin LWW audit with rollback, reconcile rate limits, and sync docs. Default customers stay opt-in; company conflict UI is removed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -937,16 +937,18 @@ export async function stopSyncChannel(session: Session, id: string) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** @deprecated 公司侧已 403;请用 listPlatformLwwOverrides */
|
||||
export async function listSyncConflicts(session: Session, unresolved = true) {
|
||||
const q = unresolved ? "?unresolved=1" : "?unresolved=0";
|
||||
const res = await apiFetch(`/api/v1/admin/sync/conflicts${q}`, {
|
||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||
});
|
||||
const data = await readJson(res);
|
||||
throwIfBad(res, data, "list conflicts failed");
|
||||
throwIfBad(res, data, "list conflicts failed (公司侧已禁用,请用超管 LWW 审计)");
|
||||
return data as { items: SyncConflict[] };
|
||||
}
|
||||
|
||||
/** @deprecated 公司侧已 403 */
|
||||
export async function resolveSyncConflict(session: Session, id: string, resolution: string) {
|
||||
const res = await apiFetch(`/api/v1/admin/sync/conflicts/${encodeURIComponent(id)}/resolve`, {
|
||||
method: "POST",
|
||||
@@ -957,7 +959,7 @@ export async function resolveSyncConflict(session: Session, id: string, resoluti
|
||||
body: JSON.stringify({ resolution }),
|
||||
});
|
||||
const data = await readJson(res);
|
||||
throwIfBad(res, data, "resolve conflict failed");
|
||||
throwIfBad(res, data, "resolve conflict failed (公司侧已禁用)");
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1000,6 +1002,89 @@ export async function ingestSyncRows(
|
||||
return data as { ok: boolean; ingested: number; hint?: string };
|
||||
}
|
||||
|
||||
export async function ensureSyncBinding(
|
||||
session: Session,
|
||||
body: {
|
||||
local_database_id: string;
|
||||
online_db_id: string;
|
||||
channel_id?: string;
|
||||
note?: string;
|
||||
user_id?: number;
|
||||
}
|
||||
) {
|
||||
const res = await apiFetch(`/api/v1/admin/sync/bindings`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await readJson(res);
|
||||
throwIfBad(res, data, "ensure binding failed");
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function listSyncBindings(session: Session, localDatabaseId?: string) {
|
||||
const q = localDatabaseId
|
||||
? `?local_database_id=${encodeURIComponent(localDatabaseId)}`
|
||||
: "";
|
||||
const res = await apiFetch(`/api/v1/admin/sync/bindings${q}`, {
|
||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||
});
|
||||
const data = await readJson(res);
|
||||
throwIfBad(res, data, "list bindings failed");
|
||||
return data as { items: Array<Record<string, unknown>> };
|
||||
}
|
||||
|
||||
export async function listPlatformLwwOverrides(
|
||||
session: Session,
|
||||
opts?: { tenant_id?: number; channel_id?: string; limit?: number }
|
||||
) {
|
||||
const q = new URLSearchParams();
|
||||
if (opts?.tenant_id) q.set("tenant_id", String(opts.tenant_id));
|
||||
if (opts?.channel_id) q.set("channel_id", opts.channel_id);
|
||||
if (opts?.limit) q.set("limit", String(opts.limit));
|
||||
const qs = q.toString();
|
||||
const res = await apiFetch(`/api/v1/platform/dbsync/lww-overrides${qs ? `?${qs}` : ""}`, {
|
||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||
});
|
||||
const data = await readJson(res);
|
||||
throwIfBad(res, data, "list lww overrides failed");
|
||||
return data as {
|
||||
items: Array<{
|
||||
id: string;
|
||||
tenant_id: number;
|
||||
channel_id: string;
|
||||
table: string;
|
||||
row_pk: string;
|
||||
op: string;
|
||||
entry: string;
|
||||
policy: string;
|
||||
outcome: string;
|
||||
loser_payload?: string;
|
||||
winner_payload?: string;
|
||||
target_ver: number;
|
||||
source_ver: number;
|
||||
created_at: string;
|
||||
}>;
|
||||
hint?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function rollbackPlatformLwwOverride(session: Session, id: string) {
|
||||
const res = await apiFetch(
|
||||
`/api/v1/platform/dbsync/lww-overrides/${encodeURIComponent(id)}/rollback`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||
}
|
||||
);
|
||||
const data = await readJson(res);
|
||||
throwIfBad(res, data, "lww rollback failed");
|
||||
return data as { ok: boolean; record?: Record<string, unknown>; hint?: string };
|
||||
}
|
||||
|
||||
export async function listPlatformTenants(session: Session) {
|
||||
const res = await apiFetch(`/api/v1/platform/tenants`, {
|
||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||
|
||||
Reference in New Issue
Block a user