transceiver-db/scripts/migrate.ts
Rene Fichtmueller 7869f098b2 feat: linecard system support, Cisco 8000 accuracy, price anomaly detection
API/finder:
- Add modular chassis support: sibling linecards fetched when is_linecard=true
- Add chassis linecards when system_type=modular
- Extend switch response: system_type, is_linecard, chassis_model, slot_type,
  flexbox_compat_mode, flexbox_notes, description, switching_capacity_tbps,
  total_ports, category, lifecycle_status, features, use_cases, linecards[]

API/transceivers:
- Filter price_observations with COALESCE(is_anomalous, false) = false
  (direct prices + comparable market prices)

Scraper/db:
- Add PRICE_BOUNDS map (per form-factor min/max USD sanity bounds)
- Add isPriceAnomalous() — marks DB price_observations as is_anomalous=true
- Add competitor_verified flag: set true when valid competitor price stored
- upsertPriceObservation: skip prices outside sanity bounds, set competitor_verified

Scraper/hash:
- contentHash() now accepts Record<string,unknown> | string (union type)
  to support both structured objects and legacy string callers

Scrapers (skylane, tscom, wiitek):
- Fix contentHash() call signature: pass objects not JSON.stringify strings
- Fix wiitek: remove invalid 'name' param, fix t.id → transceiverId

Migrations:
- Add is_anomalous, competitor_verified, competitor_verified_at,
  image_primary columns
- Recreate sync_fully_verified trigger to include competitor_verified
- Add is_linecard, chassis_model, system_type, slot_type,
  flexbox_compat_mode, flexbox_notes to switches table
2026-04-09 09:06:22 +02:00

77 lines
2.1 KiB
TypeScript

import { readFileSync, readdirSync } from "fs";
import { join } from "path";
import { Pool } from "pg";
import { config } from "dotenv";
config();
const pool = new Pool({
host: process.env.POSTGRES_HOST || "localhost",
port: parseInt(process.env.POSTGRES_PORT || "5432"),
database: process.env.POSTGRES_DB || "transceiver_db",
user: process.env.POSTGRES_USER || "tip",
password: process.env.POSTGRES_PASSWORD || "tip_dev_2026",
});
async function migrate(): Promise<void> {
const sqlDir = join(__dirname, "..", "sql");
const files = readdirSync(sqlDir)
.filter((f) => f.endsWith(".sql"))
.sort();
const client = await pool.connect();
try {
// Create migration tracker if it doesn't exist
await client.query(`
CREATE TABLE IF NOT EXISTS _migrations (
filename TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
// Get already-applied migrations
const { rows } = await client.query(
"SELECT filename FROM _migrations ORDER BY filename"
);
const applied = new Set(rows.map((r: { filename: string }) => r.filename));
const pending = files.filter((f) => !applied.has(f));
if (pending.length === 0) {
console.log("All migrations already applied. Nothing to do.");
return;
}
console.log(
`${applied.size} already applied, running ${pending.length} pending migrations`
);
for (const file of pending) {
const sql = readFileSync(join(sqlDir, file), "utf-8");
console.log(`Running: ${file}...`);
await client.query("BEGIN");
try {
await client.query(sql);
await client.query(
"INSERT INTO _migrations (filename) VALUES ($1)",
[file]
);
await client.query("COMMIT");
console.log(` Done: ${file}`);
} catch (err) {
await client.query("ROLLBACK");
throw err;
}
}
console.log("\nAll migrations completed successfully.");
} catch (err) {
console.error("Migration failed:", err);
process.exit(1);
} finally {
client.release();
await pool.end();
}
}
migrate();