fix(sync): сохранять данные синка в текущий space и не склеивать VPS

Вставки VPS, платежей, тарифов и uptime-проверок не задавали spaceId и
уходили в space-main. Поиск существующего VPS матчил bm-1 с bm-12 и
сервера с пустым IP между собой; теперь сначала по id, затем по точному
токену в заметке, затем по непустому IP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-06 22:22:38 +07:00
1 parent e37f69ebf0
commit 01a771407d
8 files changed
+102 -72

No files matched your search

+8 -12
View File
@@ -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,
+26 -1
View File
@@ -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'])
})
})
+6 -12
View File
@@ -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,
@@ -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
}
+7 -15
View File
@@ -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,
+2 -1
View File
@@ -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<UptimeCheckResult> {
db.insert(schema.vpsHealthChecks)
.values({
spaceId: getCurrentSpaceId(),
id: `hc-${randomUUID()}`,
vpsId: row.id,
checkedAt: now,
+7 -15
View File
@@ -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,
+7 -16
View File
@@ -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,