refactor(repo): переход на pnpm monorepo с shadcn/ui и Fastify+Drizzle
Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import * as schema from './schema/index.js'
|
||||
|
||||
export type Db = BetterSQLite3Database<typeof schema>
|
||||
|
||||
let _db: Db | null = null
|
||||
let _sqlite: Database.Database | null = null
|
||||
|
||||
const DEFAULT_DB_PATH = resolve(process.cwd(), 'data', 'vps-tracker.db')
|
||||
|
||||
export function getDbPath(): string {
|
||||
return process.env.DB_PATH ?? DEFAULT_DB_PATH
|
||||
}
|
||||
|
||||
export function getDb(): Db {
|
||||
if (_db) return _db
|
||||
const dbPath = getDbPath()
|
||||
const dir = dirname(dbPath)
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
_sqlite = new Database(dbPath)
|
||||
_sqlite.pragma('journal_mode = WAL')
|
||||
_sqlite.pragma('foreign_keys = ON')
|
||||
_db = drizzle(_sqlite, { schema })
|
||||
return _db
|
||||
}
|
||||
|
||||
export function closeDb(): void {
|
||||
if (_sqlite) {
|
||||
_sqlite.close()
|
||||
_sqlite = null
|
||||
_db = null
|
||||
}
|
||||
}
|
||||
|
||||
export { schema }
|
||||
@@ -0,0 +1,51 @@
|
||||
import { desc, eq } from 'drizzle-orm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
import { generateId } from './utils.js'
|
||||
|
||||
type Row = typeof schema.balanceLedger.$inferSelect
|
||||
type Insert = Partial<typeof schema.balanceLedger.$inferInsert> & {
|
||||
type: string
|
||||
date: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
function normalize(input: Partial<Row>) {
|
||||
return {
|
||||
type: input.type ?? '',
|
||||
date: input.date ?? '',
|
||||
amount: Number(input.amount) || 0,
|
||||
currency: input.currency ?? '',
|
||||
direction: input.direction ?? '',
|
||||
providerAccountId: input.providerAccountId ?? '',
|
||||
vpsId: input.vpsId ?? '',
|
||||
note: input.note ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
export const balanceLedgerRepository = {
|
||||
list(): Row[] {
|
||||
return getDb().select().from(schema.balanceLedger).orderBy(desc(schema.balanceLedger.date)).all()
|
||||
},
|
||||
get(id: string): Row | undefined {
|
||||
return getDb().select().from(schema.balanceLedger).where(eq(schema.balanceLedger.id, id)).get()
|
||||
},
|
||||
create(input: Insert, id?: string): Row {
|
||||
const finalId = id ?? input.id ?? generateId('ledger')
|
||||
getDb().insert(schema.balanceLedger).values({ id: finalId, ...normalize(input) }).run()
|
||||
return this.get(finalId)!
|
||||
},
|
||||
update(id: string, input: Partial<Row>): Row | undefined {
|
||||
const existing = this.get(id)
|
||||
if (!existing) return undefined
|
||||
getDb()
|
||||
.update(schema.balanceLedger)
|
||||
.set(normalize({ ...existing, ...input }))
|
||||
.where(eq(schema.balanceLedger.id, id))
|
||||
.run()
|
||||
return this.get(id)
|
||||
},
|
||||
delete(id: string): boolean {
|
||||
const r = getDb().delete(schema.balanceLedger).where(eq(schema.balanceLedger.id, id)).run()
|
||||
return r.changes > 0
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { desc, eq } from 'drizzle-orm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
import { generateId } from './utils.js'
|
||||
|
||||
type Row = typeof schema.payments.$inferSelect
|
||||
type Insert = Partial<typeof schema.payments.$inferInsert> & { type: string; date: string; amount: number }
|
||||
|
||||
function normalize(input: Partial<Row>) {
|
||||
return {
|
||||
type: input.type ?? '',
|
||||
date: input.date ?? '',
|
||||
amount: Number(input.amount) || 0,
|
||||
currency: input.currency ?? '',
|
||||
providerAccountId: input.providerAccountId ?? '',
|
||||
vpsId: input.vpsId ?? '',
|
||||
note: input.note ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
export const paymentsRepository = {
|
||||
list(): Row[] {
|
||||
return getDb().select().from(schema.payments).orderBy(desc(schema.payments.date)).all()
|
||||
},
|
||||
get(id: string): Row | undefined {
|
||||
return getDb().select().from(schema.payments).where(eq(schema.payments.id, id)).get()
|
||||
},
|
||||
create(input: Insert, id?: string): Row {
|
||||
const finalId = id ?? input.id ?? generateId('pay')
|
||||
getDb().insert(schema.payments).values({ id: finalId, ...normalize(input) }).run()
|
||||
return this.get(finalId)!
|
||||
},
|
||||
update(id: string, input: Partial<Row>): Row | undefined {
|
||||
const existing = this.get(id)
|
||||
if (!existing) return undefined
|
||||
getDb()
|
||||
.update(schema.payments)
|
||||
.set(normalize({ ...existing, ...input }))
|
||||
.where(eq(schema.payments.id, id))
|
||||
.run()
|
||||
return this.get(id)
|
||||
},
|
||||
delete(id: string): boolean {
|
||||
const r = getDb().delete(schema.payments).where(eq(schema.payments.id, id)).run()
|
||||
return r.changes > 0
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { eq, like, asc, sql } from 'drizzle-orm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
|
||||
export function normalizeProjectNameInput(name: unknown): string {
|
||||
if (name == null) return ''
|
||||
return String(name).trim()
|
||||
}
|
||||
|
||||
export function findProjectByNameCaseInsensitive(
|
||||
name: string,
|
||||
): (typeof schema.serverProjects.$inferSelect) | undefined {
|
||||
const n = normalizeProjectNameInput(name)
|
||||
if (!n) return undefined
|
||||
return getDb()
|
||||
.select()
|
||||
.from(schema.serverProjects)
|
||||
.where(eq(sql`LOWER(${schema.serverProjects.name})`, n.toLowerCase()))
|
||||
.get()
|
||||
}
|
||||
|
||||
export function resolveOrCreateProject(
|
||||
name: unknown,
|
||||
): { id: string | null; name: string } {
|
||||
const n = normalizeProjectNameInput(name)
|
||||
if (!n) return { id: null, name: '' }
|
||||
const existing = findProjectByNameCaseInsensitive(n)
|
||||
if (existing) return { id: existing.id, name: existing.name }
|
||||
const id = `proj-${randomUUID()}`
|
||||
const now = new Date().toISOString()
|
||||
getDb()
|
||||
.insert(schema.serverProjects)
|
||||
.values({ id, name: n, color: null, sortOrder: 0, notes: null, createdAt: now })
|
||||
.run()
|
||||
return { id, name: n }
|
||||
}
|
||||
|
||||
export function projectSuggestions(
|
||||
q: string,
|
||||
limit = 20,
|
||||
): { id: string; name: string }[] {
|
||||
const term = normalizeProjectNameInput(q)
|
||||
const lim = Math.min(50, Math.max(1, Number(limit) || 20))
|
||||
const db = getDb()
|
||||
if (!term) {
|
||||
return db
|
||||
.select({ id: schema.serverProjects.id, name: schema.serverProjects.name })
|
||||
.from(schema.serverProjects)
|
||||
.orderBy(asc(schema.serverProjects.name))
|
||||
.limit(lim)
|
||||
.all()
|
||||
}
|
||||
const esc = term.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')
|
||||
const pattern = `%${esc.toLowerCase()}%`
|
||||
return db
|
||||
.select({ id: schema.serverProjects.id, name: schema.serverProjects.name })
|
||||
.from(schema.serverProjects)
|
||||
.where(like(sql`LOWER(${schema.serverProjects.name})`, pattern))
|
||||
.orderBy(asc(schema.serverProjects.name))
|
||||
.limit(lim)
|
||||
.all()
|
||||
}
|
||||
|
||||
export function getProjectNameById(id: string): string {
|
||||
const row = getDb()
|
||||
.select({ name: schema.serverProjects.name })
|
||||
.from(schema.serverProjects)
|
||||
.where(eq(schema.serverProjects.id, id))
|
||||
.get()
|
||||
return row?.name ?? ''
|
||||
}
|
||||
|
||||
export const projectsRepository = {
|
||||
list(): (typeof schema.serverProjects.$inferSelect)[] {
|
||||
return getDb()
|
||||
.select()
|
||||
.from(schema.serverProjects)
|
||||
.orderBy(asc(schema.serverProjects.name))
|
||||
.all()
|
||||
},
|
||||
create(input: { name: string; color?: string | null; notes?: string | null }) {
|
||||
const id = `proj-${randomUUID()}`
|
||||
const now = new Date().toISOString()
|
||||
getDb()
|
||||
.insert(schema.serverProjects)
|
||||
.values({
|
||||
id,
|
||||
name: input.name,
|
||||
color: input.color ?? null,
|
||||
sortOrder: 0,
|
||||
notes: input.notes ?? null,
|
||||
createdAt: now,
|
||||
})
|
||||
.run()
|
||||
return this.list().find((p) => p.id === id)!
|
||||
},
|
||||
update(
|
||||
id: string,
|
||||
input: Partial<{ name: string; color: string | null; notes: string | null }>,
|
||||
) {
|
||||
const existing = getDb()
|
||||
.select()
|
||||
.from(schema.serverProjects)
|
||||
.where(eq(schema.serverProjects.id, id))
|
||||
.get()
|
||||
if (!existing) return undefined
|
||||
getDb()
|
||||
.update(schema.serverProjects)
|
||||
.set({
|
||||
name: input.name ?? existing.name,
|
||||
color: input.color ?? existing.color,
|
||||
notes: input.notes ?? existing.notes,
|
||||
})
|
||||
.where(eq(schema.serverProjects.id, id))
|
||||
.run()
|
||||
return getDb().select().from(schema.serverProjects).where(eq(schema.serverProjects.id, id)).get()
|
||||
},
|
||||
delete(id: string): boolean {
|
||||
const r = getDb().delete(schema.serverProjects).where(eq(schema.serverProjects.id, id)).run()
|
||||
return r.changes > 0
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { asc, eq } from 'drizzle-orm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
import { generateId } from './utils.js'
|
||||
|
||||
type AccountRow = typeof schema.providerAccounts.$inferSelect
|
||||
type AccountInsert = Partial<typeof schema.providerAccounts.$inferInsert> & {
|
||||
providerId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface PublicAccountRow extends Omit<AccountRow, 'apiCredentials'> {
|
||||
apiCredentialsSet: boolean
|
||||
}
|
||||
|
||||
function sanitize(row: AccountRow | undefined): PublicAccountRow | undefined {
|
||||
if (!row) return undefined
|
||||
const { apiCredentials, ...rest } = row
|
||||
return { ...rest, apiCredentialsSet: Boolean(apiCredentials) }
|
||||
}
|
||||
|
||||
function normalize(input: Partial<AccountRow>) {
|
||||
const rawAlert = input.balanceAlertBelow
|
||||
const alertBelow = rawAlert != null && !Number.isNaN(Number(rawAlert)) ? Number(rawAlert) : null
|
||||
return {
|
||||
providerId: input.providerId ?? '',
|
||||
name: input.name ?? '',
|
||||
panelUrl: input.panelUrl ?? '',
|
||||
currency: input.currency ?? '',
|
||||
billingMode: input.billingMode ?? '',
|
||||
notes: input.notes ?? '',
|
||||
apiType: '',
|
||||
apiBaseUrl: '',
|
||||
apiCredentials: input.apiCredentials ?? '',
|
||||
balanceAlertBelow: Number.isFinite(alertBelow) ? alertBelow : null,
|
||||
}
|
||||
}
|
||||
|
||||
export const providerAccountsRepository = {
|
||||
list(): PublicAccountRow[] {
|
||||
const rows = getDb()
|
||||
.select()
|
||||
.from(schema.providerAccounts)
|
||||
.orderBy(asc(schema.providerAccounts.name))
|
||||
.all()
|
||||
return rows.map((r) => sanitize(r)!) as PublicAccountRow[]
|
||||
},
|
||||
|
||||
get(id: string): PublicAccountRow | undefined {
|
||||
const row = getDb()
|
||||
.select()
|
||||
.from(schema.providerAccounts)
|
||||
.where(eq(schema.providerAccounts.id, id))
|
||||
.get()
|
||||
return sanitize(row)
|
||||
},
|
||||
|
||||
getWithCredentials(id: string): AccountRow | undefined {
|
||||
return getDb()
|
||||
.select()
|
||||
.from(schema.providerAccounts)
|
||||
.where(eq(schema.providerAccounts.id, id))
|
||||
.get()
|
||||
},
|
||||
|
||||
create(input: AccountInsert, id?: string): PublicAccountRow {
|
||||
const db = getDb()
|
||||
const finalId = id ?? input.id ?? generateId('account')
|
||||
db.insert(schema.providerAccounts)
|
||||
.values({ id: finalId, ...normalize(input) })
|
||||
.run()
|
||||
return this.get(finalId)!
|
||||
},
|
||||
|
||||
update(id: string, input: Partial<AccountRow>): PublicAccountRow | undefined {
|
||||
const db = getDb()
|
||||
const existing = this.getWithCredentials(id)
|
||||
if (!existing) return undefined
|
||||
const apiCredentials =
|
||||
input.apiCredentials !== undefined
|
||||
? String(input.apiCredentials || '')
|
||||
: (existing.apiCredentials || '')
|
||||
|
||||
let balanceAlertBelow = existing.balanceAlertBelow
|
||||
if (input.balanceAlertBelow !== undefined) {
|
||||
const v = input.balanceAlertBelow
|
||||
balanceAlertBelow = v == null ? null : Number.isFinite(Number(v)) ? Number(v) : null
|
||||
}
|
||||
|
||||
db.update(schema.providerAccounts)
|
||||
.set({
|
||||
providerId: input.providerId ?? existing.providerId,
|
||||
name: input.name ?? existing.name,
|
||||
panelUrl: input.panelUrl ?? existing.panelUrl,
|
||||
currency: input.currency ?? existing.currency,
|
||||
billingMode: input.billingMode ?? existing.billingMode,
|
||||
notes: input.notes ?? existing.notes,
|
||||
apiType: '',
|
||||
apiBaseUrl: '',
|
||||
apiCredentials,
|
||||
balanceAlertBelow,
|
||||
})
|
||||
.where(eq(schema.providerAccounts.id, id))
|
||||
.run()
|
||||
return this.get(id)
|
||||
},
|
||||
|
||||
delete(id: string): boolean {
|
||||
const res = getDb()
|
||||
.delete(schema.providerAccounts)
|
||||
.where(eq(schema.providerAccounts.id, id))
|
||||
.run()
|
||||
return res.changes > 0
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { asc, eq } from 'drizzle-orm'
|
||||
import { getDb, schema, type Db } from '../index.js'
|
||||
import { generateId } from './utils.js'
|
||||
|
||||
export type ProviderInsert = Partial<typeof schema.providers.$inferInsert> & {
|
||||
name: string
|
||||
}
|
||||
|
||||
function normalize(input: Partial<typeof schema.providers.$inferInsert>) {
|
||||
return {
|
||||
name: input.name ?? '',
|
||||
website: input.website ?? '',
|
||||
contact: input.contact ?? '',
|
||||
baseCurrency: input.baseCurrency ?? '',
|
||||
usdRate: input.usdRate ?? '',
|
||||
eurRate: input.eurRate ?? '',
|
||||
notes: input.notes ?? '',
|
||||
apiType: input.apiType ?? '',
|
||||
apiBaseUrl: input.apiBaseUrl ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
export const providersRepository = {
|
||||
list(): (typeof schema.providers.$inferSelect)[] {
|
||||
return getDb().select().from(schema.providers).orderBy(asc(schema.providers.name)).all()
|
||||
},
|
||||
|
||||
get(id: string): (typeof schema.providers.$inferSelect) | undefined {
|
||||
return getDb().select().from(schema.providers).where(eq(schema.providers.id, id)).get()
|
||||
},
|
||||
|
||||
create(input: ProviderInsert, id?: string): (typeof schema.providers.$inferSelect) {
|
||||
const db: Db = getDb()
|
||||
const finalId = id ?? input.id ?? generateId('provider')
|
||||
db.insert(schema.providers)
|
||||
.values({ id: finalId, ...normalize(input) })
|
||||
.run()
|
||||
return this.get(finalId)!
|
||||
},
|
||||
|
||||
update(
|
||||
id: string,
|
||||
input: Partial<typeof schema.providers.$inferInsert>,
|
||||
): (typeof schema.providers.$inferSelect) | undefined {
|
||||
const db = getDb()
|
||||
const existing = this.get(id)
|
||||
if (!existing) return undefined
|
||||
const merged = {
|
||||
...existing,
|
||||
...normalize({ ...existing, ...input }),
|
||||
}
|
||||
db.update(schema.providers)
|
||||
.set(merged)
|
||||
.where(eq(schema.providers.id, id))
|
||||
.run()
|
||||
return this.get(id)
|
||||
},
|
||||
|
||||
delete(id: string): boolean {
|
||||
const res = getDb().delete(schema.providers).where(eq(schema.providers.id, id)).run()
|
||||
return res.changes > 0
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { asc, eq } from 'drizzle-orm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
|
||||
type Row = typeof schema.settings.$inferSelect
|
||||
|
||||
export type SettingsDto = Omit<Row, 'telegramBotToken' | 'autoConvert' | 'syncEnabled' | 'notifyPaymentExpiryEnabled' | 'notifyNewTariffsEnabled' | 'notifyLowBalanceEnabled' | 'notifySyncDigestEnabled' | 'customFields'> & {
|
||||
telegramBotTokenSet: boolean
|
||||
autoConvert: boolean
|
||||
syncEnabled: boolean
|
||||
notifyPaymentExpiryEnabled: boolean
|
||||
notifyNewTariffsEnabled: boolean
|
||||
notifyLowBalanceEnabled: boolean
|
||||
notifySyncDigestEnabled: boolean
|
||||
customFields: unknown[]
|
||||
}
|
||||
|
||||
function toDto(row: Row | undefined): SettingsDto | undefined {
|
||||
if (!row) return undefined
|
||||
let customFields: unknown[] = []
|
||||
if (row.customFields) {
|
||||
try {
|
||||
customFields = JSON.parse(row.customFields)
|
||||
} catch {
|
||||
customFields = []
|
||||
}
|
||||
}
|
||||
const { telegramBotToken, ...rest } = row
|
||||
return {
|
||||
...rest,
|
||||
telegramBotTokenSet: Boolean(telegramBotToken?.trim()),
|
||||
autoConvert: Boolean(row.autoConvert),
|
||||
syncEnabled: Boolean(row.syncEnabled),
|
||||
notifyPaymentExpiryEnabled: Boolean(row.notifyPaymentExpiryEnabled),
|
||||
notifyNewTariffsEnabled: Boolean(row.notifyNewTariffsEnabled),
|
||||
notifyLowBalanceEnabled: Boolean(row.notifyLowBalanceEnabled),
|
||||
notifySyncDigestEnabled: Boolean(row.notifySyncDigestEnabled),
|
||||
customFields: Array.isArray(customFields) ? customFields : [],
|
||||
}
|
||||
}
|
||||
|
||||
function serializeCustomFields(val: unknown): string | null {
|
||||
if (val == null) return null
|
||||
if (Array.isArray(val)) return JSON.stringify(val)
|
||||
if (typeof val === 'string') return val || null
|
||||
return null
|
||||
}
|
||||
|
||||
interface SettingsInput {
|
||||
baseCurrency?: string
|
||||
ratesUrl?: string
|
||||
autoConvert?: boolean
|
||||
ratesUpdatedAt?: string
|
||||
syncEnabled?: boolean
|
||||
syncIntervalMinutes?: number
|
||||
syncTariffsIntervalMinutes?: number
|
||||
telegramBotToken?: string
|
||||
telegramChatId?: string
|
||||
telegramMessageThreadId?: string
|
||||
notifyPaymentExpiryEnabled?: boolean
|
||||
notifyNewTariffsEnabled?: boolean
|
||||
notifyLowBalanceEnabled?: boolean
|
||||
notifySyncDigestEnabled?: boolean
|
||||
customFields?: unknown
|
||||
}
|
||||
|
||||
function buildValues(id: string, existing: Row | undefined, r: SettingsInput) {
|
||||
return {
|
||||
id,
|
||||
baseCurrency: r.baseCurrency ?? existing?.baseCurrency ?? 'RUB',
|
||||
ratesUrl: r.ratesUrl ?? existing?.ratesUrl ?? '',
|
||||
autoConvert:
|
||||
r.autoConvert !== undefined ? (r.autoConvert ? 1 : 0) : existing?.autoConvert ? 1 : 0,
|
||||
ratesUpdatedAt: r.ratesUpdatedAt ?? existing?.ratesUpdatedAt ?? '',
|
||||
syncEnabled:
|
||||
r.syncEnabled !== undefined ? (r.syncEnabled ? 1 : 0) : existing?.syncEnabled ? 1 : 0,
|
||||
syncIntervalMinutes:
|
||||
r.syncIntervalMinutes !== undefined
|
||||
? Math.max(15, Number(r.syncIntervalMinutes) || 60)
|
||||
: existing?.syncIntervalMinutes ?? 60,
|
||||
syncTariffsIntervalMinutes:
|
||||
r.syncTariffsIntervalMinutes !== undefined
|
||||
? Math.max(60, Number(r.syncTariffsIntervalMinutes) || 1440)
|
||||
: existing?.syncTariffsIntervalMinutes ?? 1440,
|
||||
telegramBotToken:
|
||||
r.telegramBotToken !== undefined ? r.telegramBotToken || '' : existing?.telegramBotToken ?? '',
|
||||
telegramChatId:
|
||||
r.telegramChatId !== undefined ? r.telegramChatId || '' : existing?.telegramChatId ?? '',
|
||||
telegramMessageThreadId:
|
||||
r.telegramMessageThreadId !== undefined
|
||||
? r.telegramMessageThreadId || ''
|
||||
: existing?.telegramMessageThreadId ?? '',
|
||||
notifyPaymentExpiryEnabled:
|
||||
r.notifyPaymentExpiryEnabled !== undefined
|
||||
? r.notifyPaymentExpiryEnabled
|
||||
? 1
|
||||
: 0
|
||||
: existing?.notifyPaymentExpiryEnabled
|
||||
? 1
|
||||
: 0,
|
||||
notifyNewTariffsEnabled:
|
||||
r.notifyNewTariffsEnabled !== undefined
|
||||
? r.notifyNewTariffsEnabled
|
||||
? 1
|
||||
: 0
|
||||
: existing?.notifyNewTariffsEnabled
|
||||
? 1
|
||||
: 0,
|
||||
notifyLowBalanceEnabled:
|
||||
r.notifyLowBalanceEnabled !== undefined
|
||||
? r.notifyLowBalanceEnabled
|
||||
? 1
|
||||
: 0
|
||||
: existing?.notifyLowBalanceEnabled
|
||||
? 1
|
||||
: 0,
|
||||
notifySyncDigestEnabled:
|
||||
r.notifySyncDigestEnabled !== undefined
|
||||
? r.notifySyncDigestEnabled
|
||||
? 1
|
||||
: 0
|
||||
: existing?.notifySyncDigestEnabled
|
||||
? 1
|
||||
: 0,
|
||||
customFields: serializeCustomFields(r.customFields ?? existing?.customFields),
|
||||
}
|
||||
}
|
||||
|
||||
export const settingsRepository = {
|
||||
list(): SettingsDto[] {
|
||||
const rows = getDb().select().from(schema.settings).orderBy(asc(schema.settings.id)).all()
|
||||
return rows.map((r) => toDto(r)!) as SettingsDto[]
|
||||
},
|
||||
get(id: string): SettingsDto | undefined {
|
||||
return toDto(getDb().select().from(schema.settings).where(eq(schema.settings.id, id)).get())
|
||||
},
|
||||
getRow(id: string): Row | undefined {
|
||||
return getDb().select().from(schema.settings).where(eq(schema.settings.id, id)).get()
|
||||
},
|
||||
upsert(id: string, input: SettingsInput): SettingsDto {
|
||||
const db = getDb()
|
||||
const existing = this.getRow(id)
|
||||
const values = buildValues(id, existing, input)
|
||||
if (existing) {
|
||||
db.update(schema.settings).set(values).where(eq(schema.settings.id, id)).run()
|
||||
} else {
|
||||
db.insert(schema.settings).values(values).run()
|
||||
}
|
||||
return this.get(id)!
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { vpsRepository } from './vps.js'
|
||||
import { providersRepository } from './providers.js'
|
||||
import { providerAccountsRepository } from './provider-accounts.js'
|
||||
import { paymentsRepository } from './payments.js'
|
||||
import { balanceLedgerRepository } from './balance-ledger.js'
|
||||
import { settingsRepository } from './settings.js'
|
||||
import { activeTariffsRepository, tariffSyncOptionsRepository } from './tariffs.js'
|
||||
import { projectsRepository } from './projects.js'
|
||||
|
||||
export interface Snapshot {
|
||||
vps: ReturnType<typeof vpsRepository.list>
|
||||
serverProjects: ReturnType<typeof projectsRepository.list>
|
||||
providers: ReturnType<typeof providersRepository.list>
|
||||
providerAccounts: ReturnType<typeof providerAccountsRepository.list>
|
||||
payments: ReturnType<typeof paymentsRepository.list>
|
||||
balanceLedger: ReturnType<typeof balanceLedgerRepository.list>
|
||||
settings: ReturnType<typeof settingsRepository.list>
|
||||
activeTariffs: ReturnType<typeof activeTariffsRepository.list>
|
||||
tariffSyncOptions: ReturnType<typeof tariffSyncOptionsRepository.list>
|
||||
}
|
||||
|
||||
export function getSnapshot(): Snapshot {
|
||||
return {
|
||||
vps: vpsRepository.list(),
|
||||
serverProjects: projectsRepository.list(),
|
||||
providers: providersRepository.list(),
|
||||
providerAccounts: providerAccountsRepository.list(),
|
||||
payments: paymentsRepository.list(),
|
||||
balanceLedger: balanceLedgerRepository.list(),
|
||||
settings: settingsRepository.list(),
|
||||
activeTariffs: activeTariffsRepository.list(),
|
||||
tariffSyncOptions: tariffSyncOptionsRepository.list(),
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
vpsRepository,
|
||||
providersRepository,
|
||||
providerAccountsRepository,
|
||||
paymentsRepository,
|
||||
balanceLedgerRepository,
|
||||
settingsRepository,
|
||||
activeTariffsRepository,
|
||||
tariffSyncOptionsRepository,
|
||||
projectsRepository,
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { asc, eq } from 'drizzle-orm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
|
||||
type Row = typeof schema.activeTariffs.$inferSelect
|
||||
|
||||
export type ActiveTariffDto = Omit<Row, 'orderAvailable' | 'ramGb'> & {
|
||||
orderAvailable: boolean
|
||||
ramGb: number
|
||||
}
|
||||
|
||||
function toDto(row: Row | undefined): ActiveTariffDto | undefined {
|
||||
if (!row) return undefined
|
||||
return {
|
||||
...row,
|
||||
orderAvailable: Boolean(row.orderAvailable),
|
||||
ramGb: row.ramGb != null ? Number(row.ramGb) : 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const activeTariffsRepository = {
|
||||
list(): ActiveTariffDto[] {
|
||||
const rows = getDb()
|
||||
.select()
|
||||
.from(schema.activeTariffs)
|
||||
.orderBy(asc(schema.activeTariffs.name))
|
||||
.all()
|
||||
return rows.map((r) => toDto(r)!) as ActiveTariffDto[]
|
||||
},
|
||||
byAccount(accountId: string): ActiveTariffDto[] {
|
||||
const rows = getDb()
|
||||
.select()
|
||||
.from(schema.activeTariffs)
|
||||
.where(eq(schema.activeTariffs.providerAccountId, accountId))
|
||||
.all()
|
||||
return rows.map((r) => toDto(r)!) as ActiveTariffDto[]
|
||||
},
|
||||
upsertMany(rows: (typeof schema.activeTariffs.$inferInsert)[]): void {
|
||||
const db = getDb()
|
||||
for (const r of rows) {
|
||||
const existing = db
|
||||
.select({ id: schema.activeTariffs.id })
|
||||
.from(schema.activeTariffs)
|
||||
.where(eq(schema.activeTariffs.id, r.id))
|
||||
.get()
|
||||
if (existing) {
|
||||
db.update(schema.activeTariffs).set(r).where(eq(schema.activeTariffs.id, r.id)).run()
|
||||
} else {
|
||||
db.insert(schema.activeTariffs).values(r).run()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export type TariffSyncOptionsRow = typeof schema.tariffSyncOptions.$inferSelect
|
||||
export interface TariffSyncOptionsDto {
|
||||
providerAccountId: string
|
||||
datacenters: unknown[]
|
||||
periods: unknown[]
|
||||
syncedAt: string
|
||||
}
|
||||
|
||||
export function toTariffSyncOptionsDto(
|
||||
row: TariffSyncOptionsRow | undefined,
|
||||
): TariffSyncOptionsDto | undefined {
|
||||
if (!row) return undefined
|
||||
let datacenters: unknown[] = []
|
||||
let periods: unknown[] = []
|
||||
try {
|
||||
datacenters = row.datacenters ? JSON.parse(row.datacenters) : []
|
||||
} catch {
|
||||
datacenters = []
|
||||
}
|
||||
try {
|
||||
periods = row.periods ? JSON.parse(row.periods) : []
|
||||
} catch {
|
||||
periods = []
|
||||
}
|
||||
return {
|
||||
providerAccountId: row.providerAccountId,
|
||||
datacenters: Array.isArray(datacenters) ? datacenters : [],
|
||||
periods: Array.isArray(periods) ? periods : [],
|
||||
syncedAt: row.syncedAt ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
export const tariffSyncOptionsRepository = {
|
||||
list(): TariffSyncOptionsDto[] {
|
||||
const rows = getDb().select().from(schema.tariffSyncOptions).all()
|
||||
return rows.map((r) => toTariffSyncOptionsDto(r)!) as TariffSyncOptionsDto[]
|
||||
},
|
||||
byAccount(accountId: string): TariffSyncOptionsDto | undefined {
|
||||
return toTariffSyncOptionsDto(
|
||||
getDb()
|
||||
.select()
|
||||
.from(schema.tariffSyncOptions)
|
||||
.where(eq(schema.tariffSyncOptions.providerAccountId, accountId))
|
||||
.get(),
|
||||
)
|
||||
},
|
||||
upsert(input: typeof schema.tariffSyncOptions.$inferInsert): void {
|
||||
const db = getDb()
|
||||
const existing = db
|
||||
.select({ providerAccountId: schema.tariffSyncOptions.providerAccountId })
|
||||
.from(schema.tariffSyncOptions)
|
||||
.where(eq(schema.tariffSyncOptions.providerAccountId, input.providerAccountId))
|
||||
.get()
|
||||
if (existing) {
|
||||
db.update(schema.tariffSyncOptions)
|
||||
.set(input)
|
||||
.where(eq(schema.tariffSyncOptions.providerAccountId, input.providerAccountId))
|
||||
.run()
|
||||
} else {
|
||||
db.insert(schema.tariffSyncOptions).values(input).run()
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function generateId(prefix: string): string {
|
||||
const ts = Date.now()
|
||||
const rand = Math.random().toString(36).slice(2, 9)
|
||||
return `${prefix}-${ts}-${rand}`
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import { desc, eq, inArray } from 'drizzle-orm'
|
||||
import { getDb, schema } from '../index.js'
|
||||
import { generateId } from './utils.js'
|
||||
import { resolveOrCreateProject, getProjectNameById } from './projects.js'
|
||||
|
||||
type VpsRow = typeof schema.vps.$inferSelect
|
||||
|
||||
export type VpsDto = Omit<VpsRow, 'additionalIps' | 'userOverrides' | 'projectId' | 'monitoringEnabled' | 'backupEnabled' | 'dailyRate' | 'monthlyRate'> & {
|
||||
additionalIps: string[]
|
||||
userOverrides: string[]
|
||||
projectId: string
|
||||
monitoringEnabled: boolean
|
||||
backupEnabled: boolean
|
||||
dailyRate: number | ''
|
||||
monthlyRate: number | ''
|
||||
}
|
||||
|
||||
const USER_OVERRIDABLE_FIELDS = [
|
||||
'country', 'city', 'datacenter', 'os', 'vcpu', 'ramGb', 'diskGb', 'diskType',
|
||||
'virtualization', 'purpose', 'environment', 'project', 'notes', 'sshPort',
|
||||
'rootUser', 'bandwidthTb', 'monitoringEnabled', 'backupEnabled',
|
||||
] as const
|
||||
|
||||
function toDto(row: VpsRow | undefined): VpsDto | undefined {
|
||||
if (!row) return undefined
|
||||
let additionalIps: string[] = []
|
||||
try {
|
||||
additionalIps = row.additionalIps ? JSON.parse(row.additionalIps) : []
|
||||
} catch {
|
||||
additionalIps = []
|
||||
}
|
||||
let userOverrides: string[] = []
|
||||
try {
|
||||
userOverrides = row.userOverrides ? JSON.parse(row.userOverrides) : []
|
||||
} catch {
|
||||
userOverrides = []
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
additionalIps,
|
||||
userOverrides,
|
||||
projectId: row.projectId ?? '',
|
||||
monitoringEnabled: Boolean(row.monitoringEnabled),
|
||||
backupEnabled: Boolean(row.backupEnabled),
|
||||
dailyRate: row.dailyRate != null ? row.dailyRate : '',
|
||||
monthlyRate: row.monthlyRate != null ? row.monthlyRate : '',
|
||||
}
|
||||
}
|
||||
|
||||
interface VpsInput {
|
||||
ip?: string
|
||||
ipv6?: string
|
||||
additionalIps?: string[]
|
||||
dns?: string
|
||||
providerId?: string
|
||||
providerAccountId?: string
|
||||
country?: string
|
||||
city?: string
|
||||
datacenter?: string
|
||||
os?: string
|
||||
vcpu?: number
|
||||
ramGb?: number
|
||||
diskGb?: number
|
||||
diskType?: string
|
||||
virtualization?: string
|
||||
bandwidthTb?: number
|
||||
sshPort?: number
|
||||
rootUser?: string
|
||||
purpose?: string
|
||||
environment?: string
|
||||
project?: string
|
||||
projectId?: string
|
||||
monitoringEnabled?: boolean
|
||||
backupEnabled?: boolean
|
||||
status?: string
|
||||
tariffType?: string
|
||||
currency?: string
|
||||
dailyRate?: number | ''
|
||||
monthlyRate?: number | ''
|
||||
createdAt?: string
|
||||
paidUntil?: string
|
||||
notes?: string
|
||||
userOverrides?: string[] | 'clear'
|
||||
}
|
||||
|
||||
function projectColumnsForSave(projectInput: unknown): { project: string; projectId: string } {
|
||||
const resolved = resolveOrCreateProject(projectInput)
|
||||
if (!resolved.id) return { project: '', projectId: '' }
|
||||
return { project: resolved.name, projectId: resolved.id }
|
||||
}
|
||||
|
||||
function numOrNull(v: unknown): number | null {
|
||||
if (v === '' || v == null) return null
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
function boolToInt(v: unknown): number {
|
||||
return v ? 1 : 0
|
||||
}
|
||||
|
||||
export const vpsRepository = {
|
||||
list(): VpsDto[] {
|
||||
const rows = getDb().select().from(schema.vps).orderBy(desc(schema.vps.createdAt)).all()
|
||||
return rows.map((r) => toDto(r)!) as VpsDto[]
|
||||
},
|
||||
|
||||
get(id: string): VpsDto | undefined {
|
||||
const row = getDb().select().from(schema.vps).where(eq(schema.vps.id, id)).get()
|
||||
return toDto(row)
|
||||
},
|
||||
|
||||
create(input: VpsInput, id?: string): VpsDto {
|
||||
const db = getDb()
|
||||
const finalId = id ?? generateId('vps')
|
||||
const additionalIps = Array.isArray(input.additionalIps) ? JSON.stringify(input.additionalIps) : '[]'
|
||||
const { project, projectId } = projectColumnsForSave(input.project)
|
||||
db.insert(schema.vps)
|
||||
.values({
|
||||
id: finalId,
|
||||
ip: input.ip ?? '',
|
||||
ipv6: input.ipv6 ?? '',
|
||||
additionalIps,
|
||||
dns: input.dns ?? '',
|
||||
providerId: input.providerId ?? '',
|
||||
providerAccountId: input.providerAccountId ?? '',
|
||||
country: input.country ?? '',
|
||||
city: input.city ?? '',
|
||||
datacenter: input.datacenter ?? '',
|
||||
os: input.os ?? '',
|
||||
vcpu: input.vcpu ?? 0,
|
||||
ramGb: input.ramGb ?? 0,
|
||||
diskGb: input.diskGb ?? 0,
|
||||
diskType: input.diskType ?? '',
|
||||
virtualization: input.virtualization ?? '',
|
||||
bandwidthTb: input.bandwidthTb ?? 0,
|
||||
sshPort: input.sshPort ?? 22,
|
||||
rootUser: input.rootUser ?? '',
|
||||
purpose: input.purpose ?? '',
|
||||
environment: input.environment ?? '',
|
||||
project,
|
||||
projectId: projectId || null,
|
||||
monitoringEnabled: boolToInt(input.monitoringEnabled),
|
||||
backupEnabled: boolToInt(input.backupEnabled),
|
||||
status: input.status ?? 'active',
|
||||
tariffType: input.tariffType ?? '',
|
||||
currency: input.currency ?? '',
|
||||
dailyRate: numOrNull(input.dailyRate),
|
||||
monthlyRate: numOrNull(input.monthlyRate),
|
||||
createdAt: input.createdAt ?? new Date().toISOString().slice(0, 10),
|
||||
paidUntil: input.paidUntil ?? '',
|
||||
notes: input.notes ?? '',
|
||||
userOverrides: input.userOverrides && Array.isArray(input.userOverrides)
|
||||
? JSON.stringify(input.userOverrides)
|
||||
: '[]',
|
||||
})
|
||||
.run()
|
||||
return this.get(finalId)!
|
||||
},
|
||||
|
||||
update(id: string, input: VpsInput): VpsDto | undefined {
|
||||
const db = getDb()
|
||||
const existing = getDb().select().from(schema.vps).where(eq(schema.vps.id, id)).get()
|
||||
if (!existing) return undefined
|
||||
|
||||
let userOverrides: string[] = []
|
||||
try {
|
||||
userOverrides = existing.userOverrides ? JSON.parse(existing.userOverrides) : []
|
||||
} catch {
|
||||
userOverrides = []
|
||||
}
|
||||
const clearOverrides =
|
||||
input.userOverrides === 'clear' ||
|
||||
(Array.isArray(input.userOverrides) && input.userOverrides.length === 0)
|
||||
if (clearOverrides) userOverrides = []
|
||||
|
||||
const additionalIps = Array.isArray(input.additionalIps) ? JSON.stringify(input.additionalIps) : '[]'
|
||||
|
||||
let projectOut = existing.project ?? ''
|
||||
let projectIdOut = existing.projectId ?? ''
|
||||
if (input.project !== undefined) {
|
||||
const r = projectColumnsForSave(input.project)
|
||||
projectOut = r.project
|
||||
projectIdOut = r.projectId
|
||||
} else if (input.projectId !== undefined) {
|
||||
if (!input.projectId) {
|
||||
projectOut = ''
|
||||
projectIdOut = ''
|
||||
} else {
|
||||
projectOut = getProjectNameById(input.projectId)
|
||||
projectIdOut = input.projectId
|
||||
}
|
||||
}
|
||||
|
||||
if (!clearOverrides) {
|
||||
for (const f of USER_OVERRIDABLE_FIELDS) {
|
||||
if (f === 'project') {
|
||||
const projectChanged =
|
||||
String(projectOut ?? '') !== String(existing.project ?? '') ||
|
||||
String(projectIdOut ?? '') !== String(existing.projectId ?? '')
|
||||
if (projectChanged && !userOverrides.includes('project')) {
|
||||
userOverrides.push('project')
|
||||
}
|
||||
continue
|
||||
}
|
||||
const newVal = (input as Record<string, unknown>)[f]
|
||||
const oldVal = (existing as Record<string, unknown>)[f]
|
||||
const changed = String(newVal ?? '') !== String(oldVal ?? '')
|
||||
if (changed && !userOverrides.includes(f)) {
|
||||
userOverrides.push(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
const userOverridesJson = JSON.stringify([...new Set(userOverrides)])
|
||||
|
||||
db.update(schema.vps)
|
||||
.set({
|
||||
ip: input.ip ?? '',
|
||||
ipv6: input.ipv6 ?? '',
|
||||
additionalIps,
|
||||
dns: input.dns ?? '',
|
||||
providerId: input.providerId ?? '',
|
||||
providerAccountId: input.providerAccountId ?? '',
|
||||
country: input.country ?? '',
|
||||
city: input.city ?? '',
|
||||
datacenter: input.datacenter ?? '',
|
||||
os: input.os ?? '',
|
||||
vcpu: input.vcpu ?? 0,
|
||||
ramGb: input.ramGb ?? 0,
|
||||
diskGb: input.diskGb ?? 0,
|
||||
diskType: input.diskType ?? '',
|
||||
virtualization: input.virtualization ?? '',
|
||||
bandwidthTb: input.bandwidthTb ?? 0,
|
||||
sshPort: input.sshPort ?? 22,
|
||||
rootUser: input.rootUser ?? '',
|
||||
purpose: input.purpose ?? '',
|
||||
environment: input.environment ?? '',
|
||||
project: projectOut,
|
||||
projectId: projectIdOut || null,
|
||||
monitoringEnabled: boolToInt(input.monitoringEnabled),
|
||||
backupEnabled: boolToInt(input.backupEnabled),
|
||||
status: input.status ?? 'active',
|
||||
tariffType: input.tariffType ?? '',
|
||||
currency: input.currency ?? '',
|
||||
dailyRate: numOrNull(input.dailyRate),
|
||||
monthlyRate: numOrNull(input.monthlyRate),
|
||||
createdAt: input.createdAt ?? '',
|
||||
paidUntil: input.paidUntil ?? '',
|
||||
notes: input.notes ?? '',
|
||||
userOverrides: userOverridesJson,
|
||||
})
|
||||
.where(eq(schema.vps.id, id))
|
||||
.run()
|
||||
return this.get(id)
|
||||
},
|
||||
|
||||
delete(id: string): boolean {
|
||||
const r = getDb().delete(schema.vps).where(eq(schema.vps.id, id)).run()
|
||||
return r.changes > 0
|
||||
},
|
||||
|
||||
bulkStatus(ids: string[], status: string): number {
|
||||
getDb().update(schema.vps).set({ status }).where(inArray(schema.vps.id, ids)).run()
|
||||
return ids.length
|
||||
},
|
||||
|
||||
bulkDelete(ids: string[]): number {
|
||||
const r = getDb().delete(schema.vps).where(inArray(schema.vps.id, ids)).run()
|
||||
return r.changes
|
||||
},
|
||||
|
||||
bulkProject(ids: string[], project: string | null): { updated: number; project: string; projectId: string } {
|
||||
const { project: projName, projectId: projId } = projectColumnsForSave(project ?? '')
|
||||
const rows = getDb()
|
||||
.select()
|
||||
.from(schema.vps)
|
||||
.where(inArray(schema.vps.id, ids))
|
||||
.all()
|
||||
let updated = 0
|
||||
for (const row of rows) {
|
||||
if (String(row.project ?? '') === projName && String(row.projectId ?? '') === String(projId ?? '')) {
|
||||
continue
|
||||
}
|
||||
let userOverrides: string[] = []
|
||||
try {
|
||||
userOverrides = row.userOverrides ? JSON.parse(row.userOverrides) : []
|
||||
} catch {
|
||||
userOverrides = []
|
||||
}
|
||||
if (!userOverrides.includes('project')) userOverrides.push('project')
|
||||
getDb()
|
||||
.update(schema.vps)
|
||||
.set({
|
||||
project: projName,
|
||||
projectId: projId || null,
|
||||
userOverrides: JSON.stringify([...new Set(userOverrides)]),
|
||||
})
|
||||
.where(eq(schema.vps.id, row.id))
|
||||
.run()
|
||||
updated++
|
||||
}
|
||||
return { updated, project: projName, projectId: projId }
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
export const providers = sqliteTable('providers', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
website: text('website'),
|
||||
contact: text('contact'),
|
||||
baseCurrency: text('baseCurrency'),
|
||||
usdRate: text('usdRate'),
|
||||
eurRate: text('eurRate'),
|
||||
notes: text('notes'),
|
||||
apiType: text('apiType'),
|
||||
apiBaseUrl: text('apiBaseUrl'),
|
||||
})
|
||||
|
||||
export const providerAccounts = sqliteTable('provider_accounts', {
|
||||
id: text('id').primaryKey(),
|
||||
providerId: text('providerId')
|
||||
.notNull()
|
||||
.references(() => providers.id),
|
||||
name: text('name').notNull(),
|
||||
panelUrl: text('panelUrl'),
|
||||
currency: text('currency'),
|
||||
billingMode: text('billingMode'),
|
||||
notes: text('notes'),
|
||||
apiType: text('apiType'),
|
||||
apiBaseUrl: text('apiBaseUrl'),
|
||||
apiCredentials: text('apiCredentials'),
|
||||
balanceApi: real('balance_api'),
|
||||
balanceCurrency: text('balance_currency'),
|
||||
balanceUpdatedAt: text('balance_updated_at'),
|
||||
enoughmoneyto: text('enoughmoneyto'),
|
||||
balanceAlertBelow: real('balance_alert_below'),
|
||||
})
|
||||
|
||||
export const serverProjects = sqliteTable('server_projects', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
color: text('color'),
|
||||
sortOrder: integer('sortOrder').default(0),
|
||||
notes: text('notes'),
|
||||
createdAt: text('createdAt'),
|
||||
})
|
||||
|
||||
export const vps = sqliteTable('vps', {
|
||||
id: text('id').primaryKey(),
|
||||
ip: text('ip'),
|
||||
ipv6: text('ipv6'),
|
||||
additionalIps: text('additionalIps'),
|
||||
dns: text('dns'),
|
||||
providerId: text('providerId').references(() => providers.id),
|
||||
providerAccountId: text('providerAccountId').references(() => providerAccounts.id),
|
||||
country: text('country'),
|
||||
city: text('city'),
|
||||
datacenter: text('datacenter'),
|
||||
os: text('os'),
|
||||
vcpu: integer('vcpu'),
|
||||
ramGb: integer('ramGb'),
|
||||
diskGb: integer('diskGb'),
|
||||
diskType: text('diskType'),
|
||||
virtualization: text('virtualization'),
|
||||
bandwidthTb: integer('bandwidthTb'),
|
||||
sshPort: integer('sshPort'),
|
||||
rootUser: text('rootUser'),
|
||||
purpose: text('purpose'),
|
||||
environment: text('environment'),
|
||||
project: text('project'),
|
||||
projectId: text('projectId').references(() => serverProjects.id),
|
||||
monitoringEnabled: integer('monitoringEnabled'),
|
||||
backupEnabled: integer('backupEnabled'),
|
||||
status: text('status'),
|
||||
tariffType: text('tariffType'),
|
||||
currency: text('currency'),
|
||||
dailyRate: real('dailyRate'),
|
||||
monthlyRate: real('monthlyRate'),
|
||||
createdAt: text('createdAt'),
|
||||
paidUntil: text('paidUntil'),
|
||||
notes: text('notes'),
|
||||
userOverrides: text('userOverrides'),
|
||||
})
|
||||
|
||||
export const payments = sqliteTable('payments', {
|
||||
id: text('id').primaryKey(),
|
||||
type: text('type').notNull(),
|
||||
date: text('date').notNull(),
|
||||
amount: real('amount').notNull(),
|
||||
currency: text('currency'),
|
||||
providerAccountId: text('providerAccountId').references(() => providerAccounts.id),
|
||||
vpsId: text('vpsId').references(() => vps.id),
|
||||
note: text('note'),
|
||||
})
|
||||
|
||||
export const balanceLedger = sqliteTable('balance_ledger', {
|
||||
id: text('id').primaryKey(),
|
||||
type: text('type').notNull(),
|
||||
date: text('date').notNull(),
|
||||
amount: real('amount').notNull(),
|
||||
currency: text('currency'),
|
||||
direction: text('direction'),
|
||||
providerAccountId: text('providerAccountId').references(() => providerAccounts.id),
|
||||
vpsId: text('vpsId').references(() => vps.id),
|
||||
note: text('note'),
|
||||
})
|
||||
|
||||
export const settings = sqliteTable('settings', {
|
||||
id: text('id').primaryKey(),
|
||||
baseCurrency: text('baseCurrency'),
|
||||
ratesUrl: text('ratesUrl'),
|
||||
autoConvert: integer('autoConvert'),
|
||||
ratesUpdatedAt: text('ratesUpdatedAt'),
|
||||
syncEnabled: integer('syncEnabled'),
|
||||
syncIntervalMinutes: integer('syncIntervalMinutes'),
|
||||
syncTariffsIntervalMinutes: integer('syncTariffsIntervalMinutes'),
|
||||
customFields: text('customFields'),
|
||||
telegramBotToken: text('telegramBotToken'),
|
||||
telegramChatId: text('telegramChatId'),
|
||||
notifyPaymentExpiryEnabled: integer('notifyPaymentExpiryEnabled'),
|
||||
notifyNewTariffsEnabled: integer('notifyNewTariffsEnabled'),
|
||||
telegramMessageThreadId: text('telegramMessageThreadId'),
|
||||
notifyLowBalanceEnabled: integer('notifyLowBalanceEnabled'),
|
||||
notifySyncDigestEnabled: integer('notifySyncDigestEnabled'),
|
||||
})
|
||||
|
||||
export const syncLog = sqliteTable('sync_log', {
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('accountId')
|
||||
.notNull()
|
||||
.references(() => providerAccounts.id),
|
||||
startedAt: text('startedAt').notNull(),
|
||||
finishedAt: text('finishedAt'),
|
||||
status: text('status'),
|
||||
vpsCount: integer('vpsCount'),
|
||||
paymentsCount: integer('paymentsCount'),
|
||||
error: text('error'),
|
||||
summary: text('summary'),
|
||||
})
|
||||
|
||||
export const activeTariffs = sqliteTable('active_tariffs', {
|
||||
id: text('id').primaryKey(),
|
||||
providerAccountId: text('providerAccountId')
|
||||
.notNull()
|
||||
.references(() => providerAccounts.id),
|
||||
providerId: text('providerId')
|
||||
.notNull()
|
||||
.references(() => providers.id),
|
||||
externalId: text('externalId').notNull(),
|
||||
datacenterKey: text('datacenterKey'),
|
||||
datacenterName: text('datacenterName'),
|
||||
name: text('name'),
|
||||
desc: text('desc'),
|
||||
vcpu: integer('vcpu'),
|
||||
ramGb: real('ramGb'),
|
||||
diskGb: integer('diskGb'),
|
||||
diskType: text('diskType'),
|
||||
virtualization: text('virtualization'),
|
||||
channel: text('channel'),
|
||||
location: text('location'),
|
||||
country: text('country'),
|
||||
cpuModel: text('cpuModel'),
|
||||
orderAvailable: integer('orderAvailable'),
|
||||
price: text('price'),
|
||||
syncedAt: text('syncedAt'),
|
||||
})
|
||||
|
||||
export const tariffSyncOptions = sqliteTable('tariff_sync_options', {
|
||||
providerAccountId: text('providerAccountId')
|
||||
.primaryKey()
|
||||
.references(() => providerAccounts.id),
|
||||
datacenters: text('datacenters'),
|
||||
periods: text('periods'),
|
||||
syncedAt: text('syncedAt'),
|
||||
})
|
||||
|
||||
export const now = sql`(datetime('now'))`
|
||||
Reference in New Issue
Block a user