From 01a771407d8b4ad5b0e55cd7b37fb63a0af62947 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 22:22:38 +0700 Subject: [PATCH] =?UTF-8?q?fix(sync):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5?= =?UTF-8?q?=20=D1=81=D0=B8=D0=BD=D0=BA=D0=B0=20=D0=B2=20=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=83=D1=89=D0=B8=D0=B9=20space=20=D0=B8=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D1=81=D0=BA=D0=BB=D0=B5=D0=B8=D0=B2=D0=B0=D1=82=D1=8C=20VPS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вставки VPS, платежей, тарифов и uptime-проверок не задавали spaceId и уходили в space-main. Поиск существующего VPS матчил bm-1 с bm-12 и сервера с пустым IP между собой; теперь сначала по id, затем по точному токену в заметке, затем по непустому IP. Co-Authored-By: Claude Opus 5.5 --- apps/api/src/services/billmanager/sync.ts | 20 ++++------ apps/api/src/services/fourvps/sync.test.ts | 27 ++++++++++++- apps/api/src/services/fourvps/sync.ts | 18 +++------ .../src/services/providers/find-synced-vps.ts | 39 +++++++++++++++++++ apps/api/src/services/ruvds/sync.ts | 22 ++++------- apps/api/src/services/uptime-check.ts | 3 +- apps/api/src/services/userapi/sync.ts | 22 ++++------- apps/api/src/services/veesp/sync.ts | 23 ++++------- 8 files changed, 102 insertions(+), 72 deletions(-) create mode 100644 apps/api/src/services/providers/find-synced-vps.ts diff --git a/apps/api/src/services/billmanager/sync.ts b/apps/api/src/services/billmanager/sync.ts index fffce26..57186c4 100644 --- a/apps/api/src/services/billmanager/sync.ts +++ b/apps/api/src/services/billmanager/sync.ts @@ -2,8 +2,9 @@ * Sync BILLmanager data into vps-tracker DB (Drizzle / @cfdm/db) */ -import { and, eq, like, or } from 'drizzle-orm' -import { getDb, schema } from '@cfdm/db' +import { eq } from 'drizzle-orm' +import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { findSyncedVps } from '../providers/find-synced-vps.js' import type { BillmanagerSyncAccount } from './context.js' import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance' @@ -165,16 +166,7 @@ export async function syncFromBillmanager( const paidUntil = vps.paidUntil || '' const notes = vps.notes ? `${vps.notes} [bm-${vps.externalId}]` : `bm-${vps.externalId}` - const existing = db - .select() - .from(schema.vps) - .where( - and( - eq(schema.vps.providerAccountId, accountId), - or(eq(schema.vps.ip, vps.ip), like(schema.vps.notes, `%bm-${vps.externalId}%`)), - ), - ) - .get() + const existing = findSyncedVps(accountId, id, [`bm-${vps.externalId}`], vps.ip) if (existing) { let userOverrides: string[] = [] @@ -271,6 +263,7 @@ export async function syncFromBillmanager( syncSummary.added.push({ id, label }) db.insert(schema.vps) .values({ + spaceId: getCurrentSpaceId(), id, ip: vps.ip, ipv6: vps.ipv6, @@ -331,6 +324,7 @@ export async function syncFromBillmanager( const payId = `pay-bm-${accountId}-${payment.externalId}` db.insert(schema.payments) .values({ + spaceId: getCurrentSpaceId(), id: payId, type: payment.type, date: payment.date, @@ -386,6 +380,7 @@ export async function syncFromBillmanager( } db.insert(schema.activeTariffs) .values({ + spaceId: getCurrentSpaceId(), id: tariffId, providerAccountId: accountId, providerId, @@ -416,6 +411,7 @@ export async function syncFromBillmanager( const periods = Array.isArray(slist.period) ? JSON.stringify(slist.period) : '[]' db.insert(schema.tariffSyncOptions) .values({ + spaceId: getCurrentSpaceId(), providerAccountId: accountId, datacenters, periods, diff --git a/apps/api/src/services/fourvps/sync.test.ts b/apps/api/src/services/fourvps/sync.test.ts index d3653dc..85a2763 100644 --- a/apps/api/src/services/fourvps/sync.test.ts +++ b/apps/api/src/services/fourvps/sync.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { closeDb, getSqlite } from '@cfdm/db' +import { closeDb, getSqlite, runWithSpaceAsync } from '@cfdm/db' import { resetTestDb } from '@cfdm/db/test-setup' import { providerAccountsRepository } from '@cfdm/db/repositories/provider-accounts' @@ -137,4 +137,29 @@ describe('syncFromFourvps', () => { .get('acc-4vps') as { balance_api: number } expect(bal.balance_api).toBe(77825) }) + it('writes synced rows into the current space', async () => { + getSqlite() + .prepare(`INSERT INTO spaces (id, name, slug, kind, createdAt) VALUES ('space-x', 'X', 'x', 'personal', '2026-01-01')`) + .run() + await runWithSpaceAsync('space-x', () => syncFromFourvps(account)) + const rows = getSqlite() + .prepare(`SELECT spaceId FROM vps UNION ALL SELECT spaceId FROM active_tariffs`) + .all() as { spaceId: string }[] + expect(rows.length).toBe(2) + expect(rows.every((r) => r.spaceId === 'space-x')).toBe(true) + }) + + it('does not merge a server into another by note prefix or empty IP', async () => { + getSqlite() + .prepare( + `INSERT INTO vps (id, spaceId, ip, providerId, providerAccountId, notes) VALUES ('manual-12', 'space-main', '', 'prov-4vps', 'acc-4vps', '4vps-1000')`, + ) + .run() + vi.mocked(fetchMyServers).mockResolvedValue([ + { ...(await fetchMyServers('', ''))[0]!, id: 100, ipv4: '' }, + ]) + await syncFromFourvps(account) + const ids = (getSqlite().prepare(`SELECT id FROM vps ORDER BY id`).all() as { id: string }[]).map((r) => r.id) + expect(ids).toEqual(['manual-12', 'vps-4vps-acc-4vps-100']) + }) }) diff --git a/apps/api/src/services/fourvps/sync.ts b/apps/api/src/services/fourvps/sync.ts index a4c1545..7c60112 100644 --- a/apps/api/src/services/fourvps/sync.ts +++ b/apps/api/src/services/fourvps/sync.ts @@ -2,8 +2,9 @@ * Sync 4VPS data into vps-tracker DB (Drizzle / @cfdm/db) */ -import { and, eq, like, or } from 'drizzle-orm' -import { getDb, schema } from '@cfdm/db' +import { eq } from 'drizzle-orm' +import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { findSyncedVps } from '../providers/find-synced-vps.js' import type { FourvpsSyncAccount } from './context.js' import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance' @@ -102,16 +103,7 @@ export async function syncFromFourvps( const paidUntil = vps.paidUntil || '' const notes = vps.notes - const existing = db - .select() - .from(schema.vps) - .where( - and( - eq(schema.vps.providerAccountId, accountId), - or(eq(schema.vps.ip, vps.ip), like(schema.vps.notes, `%4vps-${vps.externalId}%`)), - ), - ) - .get() + const existing = findSyncedVps(accountId, id, [`4vps-${vps.externalId}`], vps.ip) if (existing) { let userOverrides: string[] = [] @@ -175,6 +167,7 @@ export async function syncFromFourvps( syncSummary.added.push({ id, label }) db.insert(schema.vps) .values({ + spaceId: getCurrentSpaceId(), id, ip: vps.ip, ipv6: vps.ipv6, @@ -255,6 +248,7 @@ export async function syncFromFourvps( } db.insert(schema.activeTariffs) .values({ + spaceId: getCurrentSpaceId(), id: tariffId, providerAccountId: accountId, providerId, diff --git a/apps/api/src/services/providers/find-synced-vps.ts b/apps/api/src/services/providers/find-synced-vps.ts new file mode 100644 index 0000000..a449197 --- /dev/null +++ b/apps/api/src/services/providers/find-synced-vps.ts @@ -0,0 +1,39 @@ +import { and, eq, like, or } from 'drizzle-orm' +import { getDb, schema } from '@cfdm/db' + +type VpsRow = typeof schema.vps.$inferSelect + +const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +/** + * Existing VPS of a synced account: deterministic id → exact note token (bm-1 ≠ bm-12) → non-empty IP. + */ +export function findSyncedVps( + accountId: string, + id: string, + tokens: string[], + ip?: string | null, +): VpsRow | undefined { + const rows = getDb() + .select() + .from(schema.vps) + .where( + and( + eq(schema.vps.providerAccountId, accountId), + or( + eq(schema.vps.id, id), + ...(ip ? [eq(schema.vps.ip, ip)] : []), + ...tokens.map((t) => like(schema.vps.notes, `%${t}%`)), + ), + ), + ) + .all() + const byId = rows.find((r) => r.id === id) + if (byId) return byId + for (const t of tokens) { + const re = new RegExp(`(^|[^\\w-])${escapeRe(t)}(?![\\w-])`) + const hit = rows.find((r) => re.test(r.notes ?? '')) + if (hit) return hit + } + return ip ? rows.find((r) => r.ip === ip) : undefined +} diff --git a/apps/api/src/services/ruvds/sync.ts b/apps/api/src/services/ruvds/sync.ts index 6654217..27e9e44 100644 --- a/apps/api/src/services/ruvds/sync.ts +++ b/apps/api/src/services/ruvds/sync.ts @@ -2,8 +2,9 @@ * Sync RuVDS API v2 data into vps-tracker DB */ -import { and, eq, like, or } from 'drizzle-orm' -import { getDb, schema } from '@cfdm/db' +import { eq } from 'drizzle-orm' +import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { findSyncedVps } from '../providers/find-synced-vps.js' import type { RuvdsSyncAccount } from './context.js' import { ruvdsCredentialsString } from './context.js' @@ -112,19 +113,7 @@ export async function syncFromRuvds( const paidUntil = vps.paidUntil || '' const notes = vps.notes - const existing = db - .select() - .from(schema.vps) - .where( - and( - eq(schema.vps.providerAccountId, accountId), - or( - ...(vps.ip ? [eq(schema.vps.ip, vps.ip)] : []), - like(schema.vps.notes, `%ruvds-${vps.externalId}%`), - ), - ), - ) - .get() + const existing = findSyncedVps(accountId, id, [`ruvds-${vps.externalId}`], vps.ip) if (existing) { let userOverrides: string[] = [] @@ -205,6 +194,7 @@ export async function syncFromRuvds( syncSummary.added.push({ id, label }) db.insert(schema.vps) .values({ + spaceId: getCurrentSpaceId(), id, ip: vps.ip, ipv6: vps.ipv6, @@ -265,6 +255,7 @@ export async function syncFromRuvds( const payId = `pay-ruvds-${accountId}-${payment.externalId}` db.insert(schema.payments) .values({ + spaceId: getCurrentSpaceId(), id: payId, type: payment.type, date: payment.date, @@ -320,6 +311,7 @@ export async function syncFromRuvds( } db.insert(schema.activeTariffs) .values({ + spaceId: getCurrentSpaceId(), id: tariffId, providerAccountId: accountId, providerId, diff --git a/apps/api/src/services/uptime-check.ts b/apps/api/src/services/uptime-check.ts index 120d150..a64f065 100644 --- a/apps/api/src/services/uptime-check.ts +++ b/apps/api/src/services/uptime-check.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto' import { createConnection } from 'node:net' import { desc, eq } from 'drizzle-orm' -import { getDb, schema } from '@cfdm/db' +import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' const CHECK_TIMEOUT_MS = 5000 @@ -67,6 +67,7 @@ export async function runVpsUptimeChecks(): Promise { db.insert(schema.vpsHealthChecks) .values({ + spaceId: getCurrentSpaceId(), id: `hc-${randomUUID()}`, vpsId: row.id, checkedAt: now, diff --git a/apps/api/src/services/userapi/sync.ts b/apps/api/src/services/userapi/sync.ts index e3b567f..6017dc6 100644 --- a/apps/api/src/services/userapi/sync.ts +++ b/apps/api/src/services/userapi/sync.ts @@ -2,8 +2,9 @@ * Sync Macloud / VDSina UserAPI data into vps-tracker DB */ -import { and, eq, like, or } from 'drizzle-orm' -import { getDb, schema } from '@cfdm/db' +import { eq } from 'drizzle-orm' +import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { findSyncedVps } from '../providers/find-synced-vps.js' import type { UserApiSyncAccount } from './context.js' import { syncFallbackCurrency } from '@cfdm/shared/utils/account-balance' @@ -117,19 +118,7 @@ export async function syncFromUserApi( const paidUntil = vps.paidUntil || '' const notes = vps.notes - const existing = db - .select() - .from(schema.vps) - .where( - and( - eq(schema.vps.providerAccountId, accountId), - or( - ...(vps.ip ? [eq(schema.vps.ip, vps.ip)] : []), - like(schema.vps.notes, `%${idPrefix}-${vps.externalId}%`), - ), - ), - ) - .get() + const existing = findSyncedVps(accountId, id, [`${idPrefix}-${vps.externalId}`], vps.ip) if (existing) { let userOverrides: string[] = [] @@ -193,6 +182,7 @@ export async function syncFromUserApi( syncSummary.added.push({ id, label }) db.insert(schema.vps) .values({ + spaceId: getCurrentSpaceId(), id, ip: vps.ip, ipv6: vps.ipv6, @@ -253,6 +243,7 @@ export async function syncFromUserApi( const payId = `pay-${idPrefix}-${accountId}-${payment.externalId}` db.insert(schema.payments) .values({ + spaceId: getCurrentSpaceId(), id: payId, type: payment.type, date: payment.date, @@ -308,6 +299,7 @@ export async function syncFromUserApi( } db.insert(schema.activeTariffs) .values({ + spaceId: getCurrentSpaceId(), id: tariffId, providerAccountId: accountId, providerId, diff --git a/apps/api/src/services/veesp/sync.ts b/apps/api/src/services/veesp/sync.ts index 4599efc..64d11ec 100644 --- a/apps/api/src/services/veesp/sync.ts +++ b/apps/api/src/services/veesp/sync.ts @@ -2,8 +2,9 @@ * Sync Veesp client area data into vps-tracker DB */ -import { and, eq, like, or } from 'drizzle-orm' -import { getDb, schema } from '@cfdm/db' +import { eq } from 'drizzle-orm' +import { getCurrentSpaceId, getDb, schema } from '@cfdm/db' +import { findSyncedVps } from '../providers/find-synced-vps.js' import type { VeespSyncAccount } from './context.js' import { veespCredentialsString } from './context.js' @@ -101,20 +102,7 @@ export async function syncFromVeesp( const paidUntil = vps.paidUntil || '' const notes = vps.notes - const existing = db - .select() - .from(schema.vps) - .where( - and( - eq(schema.vps.providerAccountId, accountId), - or( - ...(vps.ip ? [eq(schema.vps.ip, vps.ip)] : []), - like(schema.vps.notes, `%veesp-${vps.externalId}%`), - like(schema.vps.notes, `%veesp-${record.serviceId}%`), - ), - ), - ) - .get() + const existing = findSyncedVps(accountId, id, [`veesp-${vps.externalId}`, `veesp-${record.serviceId}`], vps.ip) if (existing) { let userOverrides: string[] = [] @@ -195,6 +183,7 @@ export async function syncFromVeesp( syncSummary.added.push({ id, label }) db.insert(schema.vps) .values({ + spaceId: getCurrentSpaceId(), id, ip: vps.ip, ipv6: vps.ipv6, @@ -255,6 +244,7 @@ export async function syncFromVeesp( const payId = `pay-veesp-${accountId}-${payment.externalId}` db.insert(schema.payments) .values({ + spaceId: getCurrentSpaceId(), id: payId, type: payment.type, date: payment.date, @@ -310,6 +300,7 @@ export async function syncFromVeesp( } db.insert(schema.activeTariffs) .values({ + spaceId: getCurrentSpaceId(), id: tariffId, providerAccountId: accountId, providerId,