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:
1 parent
e37f69ebf0
commit
01a771407d
8 files changed
+102
-72
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user