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
77 lines
2.1 KiB
TypeScript
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();
|