gitea +
This commit is contained in:
50
backend/src/db/fix_schema.ts
Normal file
50
backend/src/db/fix_schema.ts
Normal 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();
|
||||
@@ -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;
|
||||
|
||||
2
backend/src/db/migrations/006_add_seo_scheduling.sql
Normal file
2
backend/src/db/migrations/006_add_seo_scheduling.sql
Normal 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;
|
||||
34
backend/src/db/run_migration_006.ts
Normal file
34
backend/src/db/run_migration_006.ts
Normal 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();
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user