424 lines
14 KiB
TypeScript
424 lines
14 KiB
TypeScript
import { pool } from "./client";
|
|
|
|
export interface SearchParams {
|
|
q?: string;
|
|
form_factor?: string;
|
|
speed?: string;
|
|
speed_gbps?: number;
|
|
category?: string;
|
|
fiber_type?: string;
|
|
reach_min?: number;
|
|
reach_max?: number;
|
|
wdm_type?: string;
|
|
coherent?: boolean;
|
|
market_status?: string;
|
|
vendor?: string;
|
|
verified?: "price" | "image" | "details" | "full";
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export async function searchTransceivers(params: SearchParams) {
|
|
const conditions: string[] = [
|
|
`COALESCE(t.data_confidence, 'unknown') != 'garbage'`,
|
|
`COALESCE(t.product_page_url, '') NOT LIKE '%/category/%'`,
|
|
`COALESCE(t.category, '') NOT IN ('NonTransceiver', 'Accessory', 'Adapter / Converter', 'Switch / Media Converter', 'Switch / Network Infrastructure', 'NIC / Adapter', 'Mux / Passive Optical', 'Product Family', 'Loopback / Test Module')`,
|
|
];
|
|
const values: any[] = [];
|
|
let idx = 1;
|
|
|
|
if (params.q) {
|
|
conditions.push(`(search_vector @@ plainto_tsquery('english', $${idx}) OR t.part_number ILIKE '%' || $${idx} || '%' OR t.standard_name ILIKE '%' || $${idx} || '%')`);
|
|
values.push(params.q);
|
|
idx++;
|
|
}
|
|
if (params.form_factor) {
|
|
conditions.push(`form_factor = $${idx}`);
|
|
values.push(params.form_factor);
|
|
idx++;
|
|
}
|
|
if (params.speed) {
|
|
conditions.push(`speed = $${idx}`);
|
|
values.push(params.speed);
|
|
idx++;
|
|
}
|
|
if (params.speed_gbps) {
|
|
conditions.push(`speed_gbps = $${idx}`);
|
|
values.push(params.speed_gbps);
|
|
idx++;
|
|
}
|
|
if (params.category) {
|
|
conditions.push(`category = $${idx}`);
|
|
values.push(params.category);
|
|
idx++;
|
|
}
|
|
if (params.fiber_type) {
|
|
conditions.push(`fiber_type = $${idx}`);
|
|
values.push(params.fiber_type);
|
|
idx++;
|
|
}
|
|
if (params.reach_min) {
|
|
conditions.push(`reach_meters >= $${idx}`);
|
|
values.push(params.reach_min);
|
|
idx++;
|
|
}
|
|
if (params.reach_max) {
|
|
conditions.push(`reach_meters <= $${idx}`);
|
|
values.push(params.reach_max);
|
|
idx++;
|
|
}
|
|
if (params.wdm_type) {
|
|
conditions.push(`wdm_type = $${idx}`);
|
|
values.push(params.wdm_type);
|
|
idx++;
|
|
}
|
|
if (params.coherent !== undefined) {
|
|
conditions.push(`coherent = $${idx}`);
|
|
values.push(params.coherent);
|
|
idx++;
|
|
}
|
|
if (params.market_status) {
|
|
conditions.push(`market_status = $${idx}`);
|
|
values.push(params.market_status);
|
|
idx++;
|
|
}
|
|
if (params.vendor) {
|
|
conditions.push(`v.name ILIKE $${idx}`);
|
|
values.push(`%${params.vendor}%`);
|
|
idx++;
|
|
}
|
|
if (params.verified) {
|
|
const col = params.verified === "full" ? "fully_verified" : params.verified + "_verified";
|
|
conditions.push(`t.${col} = true`);
|
|
}
|
|
|
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
const limit = params.limit || 50;
|
|
const offset = params.offset || 0;
|
|
|
|
// Add relevance ranking when full-text search is used
|
|
const orderBy = params.q
|
|
? `ORDER BY (t.part_number ILIKE $1) DESC, ts_rank(search_vector, plainto_tsquery('english', $1)) DESC, fully_verified DESC NULLS LAST, has_image DESC NULLS LAST`
|
|
: `ORDER BY fully_verified DESC NULLS LAST, has_image DESC NULLS LAST, speed_gbps DESC NULLS LAST, reach_meters ASC NULLS LAST`;
|
|
|
|
const query = `
|
|
SELECT t.*, v.name as vendor_name
|
|
FROM transceivers t
|
|
LEFT JOIN vendors v ON t.vendor_id = v.id
|
|
${where}
|
|
${orderBy}
|
|
LIMIT ${limit} OFFSET ${offset}
|
|
`;
|
|
|
|
const countQuery = `SELECT COUNT(*) FROM transceivers t LEFT JOIN vendors v ON t.vendor_id = v.id ${where}`;
|
|
|
|
const [dataResult, countResult] = await Promise.all([
|
|
pool.query(query, values),
|
|
pool.query(countQuery, values),
|
|
]);
|
|
|
|
return {
|
|
data: dataResult.rows,
|
|
total: parseInt(countResult.rows[0].count),
|
|
limit,
|
|
offset,
|
|
};
|
|
}
|
|
|
|
export async function getTransceiverById(id: string) {
|
|
const result = await pool.query(
|
|
`SELECT t.*, v.name as vendor_name, s.name as standard_full_name
|
|
FROM transceivers t
|
|
LEFT JOIN vendors v ON t.vendor_id = v.id
|
|
LEFT JOIN standards s ON t.standard_id = s.id
|
|
WHERE t.id::text = $1::text OR t.slug = $1::text`,
|
|
[id]
|
|
);
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
export interface SwitchSearchParams extends SearchParams {
|
|
whitebox?: boolean;
|
|
sonic_compatible?: boolean;
|
|
asic_vendor?: string;
|
|
nos?: string;
|
|
ocp?: boolean;
|
|
max_speed_gbps?: number;
|
|
}
|
|
|
|
export async function searchSwitches(params: SwitchSearchParams) {
|
|
const conditions: string[] = [];
|
|
const values: any[] = [];
|
|
let idx = 1;
|
|
|
|
if (params.q) {
|
|
conditions.push(`sw.search_vector @@ plainto_tsquery('english', $${idx})`);
|
|
values.push(params.q);
|
|
idx++;
|
|
}
|
|
if (params.category) {
|
|
conditions.push(`sw.category = $${idx}`);
|
|
values.push(params.category);
|
|
idx++;
|
|
}
|
|
if (params.whitebox !== undefined) {
|
|
conditions.push(`sw.is_whitebox = $${idx}`);
|
|
values.push(params.whitebox);
|
|
idx++;
|
|
}
|
|
if (params.sonic_compatible !== undefined) {
|
|
conditions.push(`sw.sonic_compatible = $${idx}`);
|
|
values.push(params.sonic_compatible);
|
|
idx++;
|
|
}
|
|
if (params.asic_vendor) {
|
|
conditions.push(`sw.asic_vendor ILIKE $${idx}`);
|
|
values.push(`%${params.asic_vendor}%`);
|
|
idx++;
|
|
}
|
|
if (params.nos) {
|
|
conditions.push(`$${idx} = ANY(sw.supported_nos)`);
|
|
values.push(params.nos);
|
|
idx++;
|
|
}
|
|
if (params.ocp !== undefined) {
|
|
conditions.push(`sw.is_ocp_accepted = $${idx}`);
|
|
values.push(params.ocp);
|
|
idx++;
|
|
}
|
|
if (params.max_speed_gbps) {
|
|
conditions.push(`sw.max_speed_gbps = $${idx}`);
|
|
values.push(params.max_speed_gbps);
|
|
idx++;
|
|
}
|
|
|
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
const limit = params.limit || 50;
|
|
const offset = params.offset || 0;
|
|
|
|
const orderBy = params.q
|
|
? `ORDER BY ts_rank(sw.search_vector, plainto_tsquery('english', $1)) DESC`
|
|
: `ORDER BY sw.max_speed_gbps DESC NULLS LAST`;
|
|
|
|
const query = `
|
|
SELECT sw.*, v.name as vendor_name
|
|
FROM switches sw
|
|
LEFT JOIN vendors v ON sw.vendor_id = v.id
|
|
${where}
|
|
${orderBy}
|
|
LIMIT ${limit} OFFSET ${offset}
|
|
`;
|
|
|
|
const countQuery = `SELECT COUNT(*) FROM switches sw ${where}`;
|
|
|
|
const [dataResult, countResult] = await Promise.all([
|
|
pool.query(query, values),
|
|
pool.query(countQuery, values),
|
|
]);
|
|
|
|
return {
|
|
data: dataResult.rows,
|
|
total: parseInt(countResult.rows[0].count),
|
|
limit,
|
|
offset,
|
|
};
|
|
}
|
|
|
|
export async function getSwitchById(id: string) {
|
|
const result = await pool.query(
|
|
`SELECT sw.*, v.name as vendor_name, v.website as vendor_website
|
|
FROM switches sw
|
|
LEFT JOIN vendors v ON sw.vendor_id = v.id
|
|
WHERE sw.id = $1`,
|
|
[id]
|
|
);
|
|
return result.rows[0] || null;
|
|
}
|
|
|
|
export async function getSwitchDocuments(switchId: string) {
|
|
const result = await pool.query(
|
|
`SELECT id, doc_type, title, source_url, download_url, language, version, is_official, vendor_doc_id, page_count, file_size_bytes, r2_key, created_at
|
|
FROM product_documents
|
|
WHERE switch_id = $1
|
|
ORDER BY is_official DESC, doc_type, title`,
|
|
[switchId]
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
export async function getSwitchIssues(switchId: string) {
|
|
const result = await pool.query(
|
|
`SELECT id, source_type, source_name, source_url, title, summary, severity,
|
|
issue_tags, affected_firmware, fix_firmware, date_reported, is_resolved, confidence
|
|
FROM product_issues
|
|
WHERE switch_id = $1
|
|
ORDER BY
|
|
CASE severity WHEN 'critical' THEN 0 WHEN 'warning' THEN 1 ELSE 2 END,
|
|
date_reported DESC NULLS LAST`,
|
|
[switchId]
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
export async function getTransceiverIssues(transceiverI: string) {
|
|
const result = await pool.query(
|
|
`SELECT id, source_type, source_name, source_url, title, summary, severity,
|
|
issue_tags, affected_firmware, fix_firmware, date_reported, is_resolved, confidence
|
|
FROM product_issues
|
|
WHERE transceiver_id = $1
|
|
ORDER BY
|
|
CASE severity WHEN 'critical' THEN 0 WHEN 'warning' THEN 1 ELSE 2 END,
|
|
date_reported DESC NULLS LAST`,
|
|
[transceiverI]
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
export async function getTransceiverDocuments(transceiverI: string) {
|
|
const result = await pool.query(
|
|
`SELECT id, doc_type, title, source_url, download_url, language, version, is_official, vendor_doc_id, page_count, file_size_bytes, r2_key, created_at
|
|
FROM product_documents
|
|
WHERE transceiver_id = $1
|
|
ORDER BY is_official DESC, doc_type, title`,
|
|
[transceiverI]
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
export async function getCompatibleTransceivers(switchId: string) {
|
|
const result = await pool.query(
|
|
`SELECT t.*, c.status, c.verified_by, c.verification_method, c.notes as compat_notes,
|
|
v.name AS vendor_name, v.type AS vendor_type, v.website AS vendor_website,
|
|
-- Latest verified price
|
|
COALESCE(t.price_verified_eur,
|
|
(SELECT po.price FROM price_observations po WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1)
|
|
) AS latest_price,
|
|
CASE WHEN t.price_verified_eur IS NOT NULL THEN 'EUR'
|
|
ELSE (SELECT po.currency FROM price_observations po WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1)
|
|
END AS latest_currency
|
|
FROM compatibility c
|
|
JOIN transceivers t ON c.transceiver_id = t.id
|
|
LEFT JOIN vendors v ON t.vendor_id = v.id
|
|
WHERE c.switch_id = $1 AND c.status = 'compatible'
|
|
ORDER BY
|
|
CASE WHEN LOWER(v.name) = 'flexoptix' THEN 0 ELSE 1 END,
|
|
CASE WHEN c.verification_method = 'vendor_compat' THEN 0
|
|
WHEN c.verification_method = 'vendor_matrix' THEN 1
|
|
ELSE 2 END,
|
|
t.speed_gbps DESC`,
|
|
[switchId]
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
/**
|
|
* getFlexoptixSuggestions — returns Flexoptix transceivers that physically fit
|
|
* the switch's port slots, derived from ports_config JSONB keys.
|
|
* Works even before the compat scraper has processed the switch.
|
|
*/
|
|
export async function getFlexoptixSuggestions(switchId: string) {
|
|
const result = await pool.query(
|
|
`WITH switch_form_factors AS (
|
|
SELECT DISTINCT
|
|
CASE
|
|
WHEN k ILIKE '%QSFP-DD800%' THEN 'QSFP-DD800'
|
|
WHEN k ILIKE '%QSFP-DD%' THEN 'QSFP-DD'
|
|
WHEN k ILIKE '%OSFP224%' THEN 'OSFP224'
|
|
WHEN k ILIKE '%OSFP%' THEN 'OSFP'
|
|
WHEN k ILIKE '%QSFP28%' THEN 'QSFP28'
|
|
WHEN k ILIKE '%QSFP+%' THEN 'QSFP+'
|
|
WHEN k ILIKE '%QSFP%' THEN 'QSFP+'
|
|
WHEN k ILIKE '%SFP28%' THEN 'SFP28'
|
|
WHEN k ILIKE '%SFP+%' THEN 'SFP+'
|
|
WHEN k ILIKE '%SFP%' THEN 'SFP+'
|
|
WHEN k ILIKE '%CFP2%' THEN 'CFP2'
|
|
WHEN k ILIKE '%CFP4%' THEN 'CFP4'
|
|
WHEN k ILIKE '%CFP%' THEN 'CFP'
|
|
END AS form_factor
|
|
FROM switches sw,
|
|
jsonb_object_keys(sw.ports_config) AS k
|
|
WHERE sw.id = $1 AND sw.ports_config IS NOT NULL
|
|
)
|
|
SELECT t.id, t.slug, t.part_number, t.standard_name, t.form_factor,
|
|
t.speed, t.speed_gbps, t.reach_meters, t.reach_label,
|
|
t.fiber_type, t.wavelength_tx_nm AS wavelength_nm, t.market_status,
|
|
t.product_page_url, t.image_url,
|
|
t.price_verified_eur, t.price_verified_at, t.street_price_usd AS price_verified_usd,
|
|
v.name AS vendor_name, v.website AS vendor_website,
|
|
COALESCE(t.price_verified_eur,
|
|
(SELECT po.price FROM price_observations po
|
|
WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1)
|
|
) AS latest_price,
|
|
CASE WHEN t.price_verified_eur IS NOT NULL THEN 'EUR'
|
|
ELSE (SELECT po.currency FROM price_observations po
|
|
WHERE po.transceiver_id = t.id ORDER BY po.time DESC LIMIT 1)
|
|
END AS latest_currency,
|
|
so.warehouse_de_qty,
|
|
so.warehouse_global_qty,
|
|
so.backorder_qty,
|
|
so.backorder_estimated_date
|
|
FROM transceivers t
|
|
JOIN vendors v ON t.vendor_id = v.id
|
|
LEFT JOIN LATERAL (
|
|
SELECT warehouse_de_qty, warehouse_global_qty, backorder_qty, backorder_estimated_date
|
|
FROM stock_observations
|
|
WHERE transceiver_id = t.id
|
|
ORDER BY time DESC
|
|
LIMIT 1
|
|
) so ON true
|
|
WHERE LOWER(v.name) = 'flexoptix'
|
|
AND t.form_factor IN (
|
|
SELECT form_factor FROM switch_form_factors WHERE form_factor IS NOT NULL
|
|
)
|
|
ORDER BY t.speed_gbps DESC NULLS LAST, t.reach_meters ASC NULLS LAST`,
|
|
[switchId]
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
export async function listVendors(type?: string) {
|
|
const query = type
|
|
? `SELECT v.*,
|
|
(SELECT COUNT(*) FROM transceivers t WHERE t.vendor_id = v.id) as transceiver_count,
|
|
(SELECT COUNT(*) FROM switches s WHERE s.vendor_id = v.id) as switch_count
|
|
FROM vendors v WHERE v.type = $1 ORDER BY v.name`
|
|
: `SELECT v.*,
|
|
(SELECT COUNT(*) FROM transceivers t WHERE t.vendor_id = v.id) as transceiver_count,
|
|
(SELECT COUNT(*) FROM switches s WHERE s.vendor_id = v.id) as switch_count
|
|
FROM vendors v ORDER BY v.name`;
|
|
const result = await pool.query(query, type ? [type] : []);
|
|
return result.rows;
|
|
}
|
|
|
|
export async function listStandards(speed?: string) {
|
|
const base = `
|
|
SELECT s.*,
|
|
COUNT(t.id)::int AS transceiver_count
|
|
FROM standards s
|
|
LEFT JOIN transceivers t ON t.standard_id = s.id`;
|
|
const query = speed
|
|
? `${base} WHERE s.speed = $1 GROUP BY s.id ORDER BY s.speed_gbps DESC NULLS LAST, s.name`
|
|
: `${base} GROUP BY s.id ORDER BY s.speed_gbps DESC NULLS LAST, s.name`;
|
|
const result = await pool.query(query, speed ? [speed] : []);
|
|
return result.rows;
|
|
}
|
|
|
|
export async function getDbStats() {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
(SELECT COUNT(*) FROM vendors) as vendor_count,
|
|
(SELECT COUNT(*) FROM standards) as standard_count,
|
|
(SELECT COUNT(*) FROM transceivers) as transceiver_count,
|
|
(SELECT COUNT(*) FROM switches) as switch_count,
|
|
(SELECT COUNT(*) FROM compatibility) as compatibility_count,
|
|
(SELECT COUNT(*) FROM breakouts) as breakout_count,
|
|
(SELECT COUNT(*) FROM knowledge_base) as kb_count,
|
|
(SELECT COUNT(*) FROM documents) as document_count,
|
|
(SELECT COUNT(*) FROM news_articles) as news_count,
|
|
(SELECT COUNT(*) FROM product_documents) as product_document_count,
|
|
(SELECT COUNT(*) FROM switches WHERE image_url IS NOT NULL) as switches_with_images,
|
|
(SELECT COUNT(*) FROM switches WHERE datasheet_r2_key IS NOT NULL) as switches_with_datasheets
|
|
`);
|
|
return result.rows[0];
|
|
}
|