|
@@ -10,6 +10,7 @@ export interface Shop {
|
|
|
sitemap_url: string;
|
|
sitemap_url: string;
|
|
|
webshop_type: string;
|
|
webshop_type: string;
|
|
|
qdrant_enabled: boolean;
|
|
qdrant_enabled: boolean;
|
|
|
|
|
+ deleted_at: string | null; // Soft delete timestamp
|
|
|
created_at: string;
|
|
created_at: string;
|
|
|
updated_at: string;
|
|
updated_at: string;
|
|
|
}
|
|
}
|
|
@@ -163,6 +164,7 @@ export class ShopDatabase {
|
|
|
sitemap_url TEXT NOT NULL,
|
|
sitemap_url TEXT NOT NULL,
|
|
|
webshop_type TEXT NOT NULL,
|
|
webshop_type TEXT NOT NULL,
|
|
|
qdrant_enabled INTEGER DEFAULT 0,
|
|
qdrant_enabled INTEGER DEFAULT 0,
|
|
|
|
|
+ deleted_at TEXT,
|
|
|
created_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
|
updated_at TEXT NOT NULL
|
|
updated_at TEXT NOT NULL
|
|
|
)
|
|
)
|
|
@@ -190,6 +192,17 @@ export class ShopDatabase {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Add deleted_at column if it doesn't exist (for existing databases)
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.db.exec(`ALTER TABLE shops ADD COLUMN deleted_at TEXT`);
|
|
|
|
|
+ logger.info('Added deleted_at column to shops table');
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ // Column likely already exists, ignore error
|
|
|
|
|
+ if (!error.message.includes('duplicate column name')) {
|
|
|
|
|
+ logger.error('Error adding deleted_at column:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Shop analytics table
|
|
// Shop analytics table
|
|
|
this.db.exec(`
|
|
this.db.exec(`
|
|
|
CREATE TABLE IF NOT EXISTS shop_analytics (
|
|
CREATE TABLE IF NOT EXISTS shop_analytics (
|
|
@@ -403,8 +416,8 @@ export class ShopDatabase {
|
|
|
const qdrant_enabled = qdrantEnabled !== undefined ? qdrantEnabled : true; // Default true for new shops
|
|
const qdrant_enabled = qdrantEnabled !== undefined ? qdrantEnabled : true; // Default true for new shops
|
|
|
|
|
|
|
|
const stmt = this.db.prepare(`
|
|
const stmt = this.db.prepare(`
|
|
|
- INSERT INTO shops (id, custom_id, url, sitemap_url, webshop_type, qdrant_enabled, created_at, updated_at)
|
|
|
|
|
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
|
|
+ INSERT INTO shops (id, custom_id, url, sitemap_url, webshop_type, qdrant_enabled, deleted_at, created_at, updated_at)
|
|
|
|
|
+ VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
|
|
`);
|
|
`);
|
|
|
|
|
|
|
|
stmt.run(id, custom_id, url, sitemapUrl, webshopType, qdrant_enabled ? 1 : 0, now, now);
|
|
stmt.run(id, custom_id, url, sitemapUrl, webshopType, qdrant_enabled ? 1 : 0, now, now);
|
|
@@ -424,25 +437,26 @@ export class ShopDatabase {
|
|
|
sitemap_url: sitemapUrl,
|
|
sitemap_url: sitemapUrl,
|
|
|
webshop_type: webshopType,
|
|
webshop_type: webshopType,
|
|
|
qdrant_enabled,
|
|
qdrant_enabled,
|
|
|
|
|
+ deleted_at: null,
|
|
|
created_at: now,
|
|
created_at: now,
|
|
|
updated_at: now
|
|
updated_at: now
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
getShopByUrl(url: string): Shop | null {
|
|
getShopByUrl(url: string): Shop | null {
|
|
|
- const stmt = this.db.prepare('SELECT * FROM shops WHERE url = ?');
|
|
|
|
|
|
|
+ const stmt = this.db.prepare('SELECT * FROM shops WHERE url = ? AND deleted_at IS NULL');
|
|
|
const result = stmt.get(url) as any;
|
|
const result = stmt.get(url) as any;
|
|
|
return result ? { ...result, qdrant_enabled: Boolean(result.qdrant_enabled) } : null;
|
|
return result ? { ...result, qdrant_enabled: Boolean(result.qdrant_enabled) } : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
getShopById(id: string): Shop | null {
|
|
getShopById(id: string): Shop | null {
|
|
|
- const stmt = this.db.prepare('SELECT * FROM shops WHERE id = ?');
|
|
|
|
|
|
|
+ const stmt = this.db.prepare('SELECT * FROM shops WHERE id = ? AND deleted_at IS NULL');
|
|
|
const result = stmt.get(id) as any;
|
|
const result = stmt.get(id) as any;
|
|
|
return result ? { ...result, qdrant_enabled: Boolean(result.qdrant_enabled) } : null;
|
|
return result ? { ...result, qdrant_enabled: Boolean(result.qdrant_enabled) } : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
getShopByCustomId(customId: string): Shop | null {
|
|
getShopByCustomId(customId: string): Shop | null {
|
|
|
- const stmt = this.db.prepare('SELECT * FROM shops WHERE custom_id = ?');
|
|
|
|
|
|
|
+ const stmt = this.db.prepare('SELECT * FROM shops WHERE custom_id = ? AND deleted_at IS NULL');
|
|
|
const result = stmt.get(customId) as any;
|
|
const result = stmt.get(customId) as any;
|
|
|
return result ? { ...result, qdrant_enabled: Boolean(result.qdrant_enabled) } : null;
|
|
return result ? { ...result, qdrant_enabled: Boolean(result.qdrant_enabled) } : null;
|
|
|
}
|
|
}
|
|
@@ -458,7 +472,13 @@ export class ShopDatabase {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
getAllShops(): Shop[] {
|
|
getAllShops(): Shop[] {
|
|
|
- const stmt = this.db.prepare('SELECT * FROM shops ORDER BY created_at DESC');
|
|
|
|
|
|
|
+ const stmt = this.db.prepare('SELECT * FROM shops WHERE deleted_at IS NULL ORDER BY created_at DESC');
|
|
|
|
|
+ const results = stmt.all() as any[];
|
|
|
|
|
+ return results.map(result => ({ ...result, qdrant_enabled: Boolean(result.qdrant_enabled) }));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getDeletedShops(): Shop[] {
|
|
|
|
|
+ const stmt = this.db.prepare('SELECT * FROM shops WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC');
|
|
|
const results = stmt.all() as any[];
|
|
const results = stmt.all() as any[];
|
|
|
return results.map(result => ({ ...result, qdrant_enabled: Boolean(result.qdrant_enabled) }));
|
|
return results.map(result => ({ ...result, qdrant_enabled: Boolean(result.qdrant_enabled) }));
|
|
|
}
|
|
}
|
|
@@ -513,16 +533,56 @@ export class ShopDatabase {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
deleteShop(identifier: string): void {
|
|
deleteShop(identifier: string): void {
|
|
|
- // Find the shop first to get the internal ID for logging
|
|
|
|
|
- const shop = this.getShopByAnyId(identifier);
|
|
|
|
|
|
|
+ // Find the shop first (using a query that doesn't filter deleted_at for internal operations)
|
|
|
|
|
+ const stmt = this.db.prepare('SELECT * FROM shops WHERE id = ? OR custom_id = ?');
|
|
|
|
|
+ const result = stmt.get(identifier, identifier) as any;
|
|
|
|
|
+ const shop = result ? { ...result, qdrant_enabled: Boolean(result.qdrant_enabled) } : null;
|
|
|
|
|
+
|
|
|
if (!shop) {
|
|
if (!shop) {
|
|
|
logger.warn(`Attempted to delete non-existent shop: ${identifier}`);
|
|
logger.warn(`Attempted to delete non-existent shop: ${identifier}`);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Use internal ID for deletion (cascade delete will handle related records)
|
|
|
|
|
- this.db.prepare('DELETE FROM shops WHERE id = ?').run(shop.id);
|
|
|
|
|
- logger.info(`Deleted shop ${shop.id} (${shop.custom_id ? `custom: ${shop.custom_id}` : 'no custom ID'}) and all related data`);
|
|
|
|
|
|
|
+ // Soft delete: set deleted_at timestamp
|
|
|
|
|
+ const now = new Date().toISOString();
|
|
|
|
|
+ this.db.prepare('UPDATE shops SET deleted_at = ?, updated_at = ? WHERE id = ?')
|
|
|
|
|
+ .run(now, now, shop.id);
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`Soft deleted shop ${shop.id} (${shop.custom_id ? `custom: ${shop.custom_id}` : 'no custom ID'})`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Force delete a shop and all related data immediately
|
|
|
|
|
+ * This bypasses soft delete and removes everything from database and queues Qdrant cleanup
|
|
|
|
|
+ */
|
|
|
|
|
+ forceDeleteShop(identifier: string): void {
|
|
|
|
|
+ // Find the shop first (using a query that doesn't filter deleted_at)
|
|
|
|
|
+ const stmt = this.db.prepare('SELECT * FROM shops WHERE id = ? OR custom_id = ?');
|
|
|
|
|
+ const result = stmt.get(identifier, identifier) as any;
|
|
|
|
|
+ const shop = result ? { ...result, qdrant_enabled: Boolean(result.qdrant_enabled) } : null;
|
|
|
|
|
+
|
|
|
|
|
+ if (!shop) {
|
|
|
|
|
+ logger.warn(`Attempted to force delete non-existent shop: ${identifier}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // If shop has Qdrant enabled, queue deletion for cleanup
|
|
|
|
|
+ if (shop.qdrant_enabled && shop.custom_id) {
|
|
|
|
|
+ // Schedule Qdrant deletion immediately (1 minute from now)
|
|
|
|
|
+ const deletionDate = new Date();
|
|
|
|
|
+ deletionDate.setMinutes(deletionDate.getMinutes() + 1);
|
|
|
|
|
+
|
|
|
|
|
+ this.db.prepare(`
|
|
|
|
|
+ INSERT INTO qdrant_deletion_queue (shop_id, custom_id, scheduled_deletion_at, deletion_status, created_at)
|
|
|
|
|
+ VALUES (?, ?, ?, 'queued', ?)
|
|
|
|
|
+ `).run(shop.id, shop.custom_id, deletionDate.toISOString(), new Date().toISOString());
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`Queued Qdrant collections for deletion for shop ${shop.id}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Delete shop immediately using internal delete method
|
|
|
|
|
+ this.deleteShopInternal(shop.id);
|
|
|
|
|
+ logger.info(`Force deleted shop ${shop.id} (${shop.custom_id ? `custom: ${shop.custom_id}` : 'no custom ID'}) and all related data`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Analytics operations
|
|
// Analytics operations
|