This commit is contained in:
2026-01-21 08:21:19 +01:00
parent 4733e1a1cc
commit fd6e7c44e1
46 changed files with 3165 additions and 456 deletions

View File

@@ -0,0 +1,50 @@
import { Pool } from 'pg';
import dotenv from 'dotenv';
import path from 'path';
// Load env vars from .env file in backend root
dotenv.config({ path: path.join(__dirname, '../../.env') });
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
async function fixSchema() {
console.log('🔧 Fixing schema...');
const client = await pool.connect();
try {
// Add seo_keywords column
console.log('Adding seo_keywords column...');
await client.query(`
ALTER TABLE monitors ADD COLUMN IF NOT EXISTS seo_keywords JSONB;
`);
// Create monitor_rankings table
console.log('Creating monitor_rankings table...');
await client.query(`
CREATE TABLE IF NOT EXISTS monitor_rankings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
keyword VARCHAR(255) NOT NULL,
rank INTEGER,
url_found TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
`);
// Create indexes for monitor_rankings
await client.query(`CREATE INDEX IF NOT EXISTS idx_monitor_rankings_monitor_id ON monitor_rankings(monitor_id);`);
await client.query(`CREATE INDEX IF NOT EXISTS idx_monitor_rankings_keyword ON monitor_rankings(keyword);`);
await client.query(`CREATE INDEX IF NOT EXISTS idx_monitor_rankings_created_at ON monitor_rankings(created_at);`);
console.log('✅ Schema fixed successfully!');
} catch (err) {
console.error('❌ Schema fix failed:', err);
} finally {
client.release();
await pool.end();
}
}
fixSchema();

View File

@@ -13,7 +13,7 @@ function toCamelCase<T>(obj: any): T {
let value = obj[key];
// Parse JSON fields that are stored as strings in the database
if ((key === 'ignore_rules' || key === 'keyword_rules') && typeof value === 'string') {
if ((key === 'ignore_rules' || key === 'keyword_rules' || key === 'seo_keywords') && typeof value === 'string') {
try {
value = JSON.parse(value);
} catch (e) {
@@ -164,8 +164,8 @@ export const db = {
const result = await query(
`INSERT INTO monitors (
user_id, url, name, frequency, status, element_selector,
ignore_rules, keyword_rules
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
ignore_rules, keyword_rules, seo_keywords, seo_interval
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`,
[
data.userId,
data.url,
@@ -175,6 +175,8 @@ export const db = {
data.elementSelector || null,
data.ignoreRules ? JSON.stringify(data.ignoreRules) : null,
data.keywordRules ? JSON.stringify(data.keywordRules) : null,
data.seoKeywords ? JSON.stringify(data.seoKeywords) : null,
data.seoInterval || 'off',
]
);
return toCamelCase<Monitor>(result.rows[0]);
@@ -220,9 +222,12 @@ export const db = {
Object.entries(updates).forEach(([key, value]) => {
if (value !== undefined) {
const snakeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
if (key === 'ignoreRules' || key === 'keywordRules') {
if (key === 'ignoreRules' || key === 'keywordRules' || key === 'seoKeywords') {
fields.push(`${snakeKey} = $${paramCount}`);
values.push(JSON.stringify(value));
} else if (key === 'seoInterval') {
fields.push(`seo_interval = $${paramCount}`);
values.push(value);
} else {
fields.push(`${snakeKey} = $${paramCount}`);
values.push(value);
@@ -433,6 +438,47 @@ export const db = {
return result.rows.map(row => toCamelCase<any>(row));
},
},
rankings: {
async create(data: {
monitorId: string;
keyword: string;
rank: number | null;
urlFound: string | null;
}): Promise<any> {
const result = await query(
`INSERT INTO monitor_rankings (
monitor_id, keyword, rank, url_found
) VALUES ($1, $2, $3, $4) RETURNING *`,
[data.monitorId, data.keyword, data.rank, data.urlFound]
);
return toCamelCase<any>(result.rows[0]);
},
async findLatestByMonitorId(monitorId: string, limit = 50): Promise<any[]> {
// Gets the latest check per keyword for this monitor
// Using DISTINCT ON is efficient in Postgres
const result = await query(
`SELECT DISTINCT ON (keyword) *
FROM monitor_rankings
WHERE monitor_id = $1
ORDER BY keyword, created_at DESC`,
[monitorId]
);
return result.rows.map(row => toCamelCase<any>(row));
},
async findHistoryByMonitorId(monitorId: string, limit = 100): Promise<any[]> {
const result = await query(
`SELECT * FROM monitor_rankings
WHERE monitor_id = $1
ORDER BY created_at DESC
LIMIT $2`,
[monitorId, limit]
);
return result.rows.map(row => toCamelCase<any>(row));
}
},
};
export default db;

View File

@@ -0,0 +1,2 @@
ALTER TABLE monitors ADD COLUMN IF NOT EXISTS seo_interval VARCHAR(20) DEFAULT 'off';
ALTER TABLE monitors ADD COLUMN IF NOT EXISTS last_seo_check_at TIMESTAMP;

View File

@@ -0,0 +1,34 @@
import { Pool } from 'pg';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
dotenv.config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
async function runManualMigration() {
console.log('🔄 Running manual migration 006...');
try {
const client = await pool.connect();
try {
const sqlPath = path.join(__dirname, 'migrations/006_add_seo_scheduling.sql');
const sql = fs.readFileSync(sqlPath, 'utf-8');
console.log('📝 Executing SQL:', sql);
await client.query(sql);
console.log('✅ Migration 006 applied successfully!');
} finally {
client.release();
}
} catch (error) {
console.error('❌ Migration 006 failed:', error);
process.exit(1);
} finally {
await pool.end();
}
}
runManualMigration();

View File

@@ -19,8 +19,8 @@ CREATE TABLE IF NOT EXISTS users (
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_plan ON users(plan);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_plan ON users(plan);
CREATE INDEX IF NOT EXISTS idx_users_webhook_enabled ON users(webhook_enabled) WHERE webhook_enabled = true;
CREATE INDEX IF NOT EXISTS idx_users_slack_enabled ON users(slack_enabled) WHERE slack_enabled = true;
@@ -42,9 +42,9 @@ CREATE TABLE IF NOT EXISTS monitors (
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_monitors_user_id ON monitors(user_id);
CREATE INDEX idx_monitors_status ON monitors(status);
CREATE INDEX idx_monitors_last_checked_at ON monitors(last_checked_at);
CREATE INDEX IF NOT EXISTS idx_monitors_user_id ON monitors(user_id);
CREATE INDEX IF NOT EXISTS idx_monitors_status ON monitors(status);
CREATE INDEX IF NOT EXISTS idx_monitors_last_checked_at ON monitors(last_checked_at);
-- Snapshots table
CREATE TABLE IF NOT EXISTS snapshots (
@@ -62,9 +62,9 @@ CREATE TABLE IF NOT EXISTS snapshots (
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_snapshots_monitor_id ON snapshots(monitor_id);
CREATE INDEX idx_snapshots_created_at ON snapshots(created_at);
CREATE INDEX idx_snapshots_changed ON snapshots(changed);
CREATE INDEX IF NOT EXISTS idx_snapshots_monitor_id ON snapshots(monitor_id);
CREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON snapshots(created_at);
CREATE INDEX IF NOT EXISTS idx_snapshots_changed ON snapshots(changed);
-- Alerts table
CREATE TABLE IF NOT EXISTS alerts (
@@ -81,10 +81,10 @@ CREATE TABLE IF NOT EXISTS alerts (
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_alerts_user_id ON alerts(user_id);
CREATE INDEX idx_alerts_monitor_id ON alerts(monitor_id);
CREATE INDEX idx_alerts_created_at ON alerts(created_at);
CREATE INDEX idx_alerts_read_at ON alerts(read_at);
CREATE INDEX IF NOT EXISTS idx_alerts_user_id ON alerts(user_id);
CREATE INDEX IF NOT EXISTS idx_alerts_monitor_id ON alerts(monitor_id);
CREATE INDEX IF NOT EXISTS idx_alerts_created_at ON alerts(created_at);
CREATE INDEX IF NOT EXISTS idx_alerts_read_at ON alerts(read_at);
-- Update timestamps trigger
CREATE OR REPLACE FUNCTION update_updated_at_column()
@@ -132,4 +132,20 @@ CREATE TABLE IF NOT EXISTS waitlist_leads (
);
CREATE INDEX IF NOT EXISTS idx_waitlist_leads_email ON waitlist_leads(email);
CREATE INDEX IF NOT EXISTS idx_waitlist_leads_created_at ON waitlist_leads(created_at);
-- SEO Rankings table
CREATE TABLE IF NOT EXISTS monitor_rankings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
keyword VARCHAR(255) NOT NULL,
rank INTEGER, -- Null if not found in top 100
url_found TEXT, -- The specific URL that ranked
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_monitor_rankings_monitor_id ON monitor_rankings(monitor_id);
CREATE INDEX IF NOT EXISTS idx_monitor_rankings_keyword ON monitor_rankings(keyword);
CREATE INDEX IF NOT EXISTS idx_monitor_rankings_created_at ON monitor_rankings(created_at);
-- Add seo_keywords to monitors if it doesn't exist
ALTER TABLE monitors ADD COLUMN IF NOT EXISTS seo_keywords JSONB;