|
@@ -61,6 +61,27 @@ export interface ScheduledJob {
|
|
|
created_at: string;
|
|
created_at: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+export interface Webhook {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ shop_id: string;
|
|
|
|
|
+ url: string;
|
|
|
|
|
+ enabled: boolean;
|
|
|
|
|
+ secret: string | null;
|
|
|
|
|
+ created_at: string;
|
|
|
|
|
+ updated_at: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface WebhookDelivery {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ webhook_id: string;
|
|
|
|
|
+ event: string;
|
|
|
|
|
+ payload: any;
|
|
|
|
|
+ status: 'success' | 'failed';
|
|
|
|
|
+ response_code: number | null;
|
|
|
|
|
+ error: string | null;
|
|
|
|
|
+ attempted_at: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export class ShopDatabase {
|
|
export class ShopDatabase {
|
|
|
private db: Database.Database;
|
|
private db: Database.Database;
|
|
|
|
|
|
|
@@ -157,6 +178,35 @@ export class ShopDatabase {
|
|
|
)
|
|
)
|
|
|
`);
|
|
`);
|
|
|
|
|
|
|
|
|
|
+ // Webhooks table
|
|
|
|
|
+ this.db.exec(`
|
|
|
|
|
+ CREATE TABLE IF NOT EXISTS webhooks (
|
|
|
|
|
+ id TEXT PRIMARY KEY,
|
|
|
|
|
+ shop_id TEXT NOT NULL,
|
|
|
|
|
+ url TEXT NOT NULL,
|
|
|
|
|
+ enabled INTEGER DEFAULT 1,
|
|
|
|
|
+ secret TEXT,
|
|
|
|
|
+ created_at TEXT NOT NULL,
|
|
|
|
|
+ updated_at TEXT NOT NULL,
|
|
|
|
|
+ FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
|
|
|
|
|
+ )
|
|
|
|
|
+ `);
|
|
|
|
|
+
|
|
|
|
|
+ // Webhook delivery log table (for tracking delivery attempts)
|
|
|
|
|
+ this.db.exec(`
|
|
|
|
|
+ CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
|
|
|
|
+ id TEXT PRIMARY KEY,
|
|
|
|
|
+ webhook_id TEXT NOT NULL,
|
|
|
|
|
+ event TEXT NOT NULL,
|
|
|
|
|
+ payload TEXT NOT NULL,
|
|
|
|
|
+ status TEXT NOT NULL,
|
|
|
|
|
+ response_code INTEGER,
|
|
|
|
|
+ error TEXT,
|
|
|
|
|
+ attempted_at TEXT NOT NULL,
|
|
|
|
|
+ FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON DELETE CASCADE
|
|
|
|
|
+ )
|
|
|
|
|
+ `);
|
|
|
|
|
+
|
|
|
logger.info('Database tables initialized');
|
|
logger.info('Database tables initialized');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -501,6 +551,118 @@ export class ShopDatabase {
|
|
|
logger.info(`${enabled ? 'Enabled' : 'Disabled'} schedule for shop ${shopId}`);
|
|
logger.info(`${enabled ? 'Enabled' : 'Disabled'} schedule for shop ${shopId}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Webhook operations
|
|
|
|
|
+ createWebhook(shopId: string, url: string, secret?: string): string {
|
|
|
|
|
+ const id = uuidv4();
|
|
|
|
|
+ const now = new Date().toISOString();
|
|
|
|
|
+
|
|
|
|
|
+ // First, disable any existing webhooks for this shop (only one webhook per shop)
|
|
|
|
|
+ this.db.prepare(`
|
|
|
|
|
+ UPDATE webhooks
|
|
|
|
|
+ SET enabled = 0
|
|
|
|
|
+ WHERE shop_id = ?
|
|
|
|
|
+ `).run(shopId);
|
|
|
|
|
+
|
|
|
|
|
+ this.db.prepare(`
|
|
|
|
|
+ INSERT INTO webhooks (id, shop_id, url, enabled, secret, created_at, updated_at)
|
|
|
|
|
+ VALUES (?, ?, ?, 1, ?, ?, ?)
|
|
|
|
|
+ `).run(id, shopId, url, secret || null, now, now);
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`Created webhook ${id} for shop ${shopId}`);
|
|
|
|
|
+ return id;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getWebhook(shopId: string): Webhook | null {
|
|
|
|
|
+ const stmt = this.db.prepare(`
|
|
|
|
|
+ SELECT * FROM webhooks
|
|
|
|
|
+ WHERE shop_id = ? AND enabled = 1
|
|
|
|
|
+ ORDER BY created_at DESC
|
|
|
|
|
+ LIMIT 1
|
|
|
|
|
+ `);
|
|
|
|
|
+ const result = stmt.get(shopId) as any;
|
|
|
|
|
+ if (!result) return null;
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: result.id,
|
|
|
|
|
+ shop_id: result.shop_id,
|
|
|
|
|
+ url: result.url,
|
|
|
|
|
+ enabled: Boolean(result.enabled),
|
|
|
|
|
+ secret: result.secret,
|
|
|
|
|
+ created_at: result.created_at,
|
|
|
|
|
+ updated_at: result.updated_at
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ updateWebhook(shopId: string, url: string, secret?: string): void {
|
|
|
|
|
+ const now = new Date().toISOString();
|
|
|
|
|
+
|
|
|
|
|
+ this.db.prepare(`
|
|
|
|
|
+ UPDATE webhooks
|
|
|
|
|
+ SET url = ?, secret = ?, updated_at = ?
|
|
|
|
|
+ WHERE shop_id = ? AND enabled = 1
|
|
|
|
|
+ `).run(url, secret || null, now, shopId);
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`Updated webhook for shop ${shopId}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ deleteWebhook(shopId: string): void {
|
|
|
|
|
+ this.db.prepare(`
|
|
|
|
|
+ DELETE FROM webhooks
|
|
|
|
|
+ WHERE shop_id = ?
|
|
|
|
|
+ `).run(shopId);
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`Deleted webhook for shop ${shopId}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setWebhookEnabled(shopId: string, enabled: boolean): void {
|
|
|
|
|
+ this.db.prepare(`
|
|
|
|
|
+ UPDATE webhooks
|
|
|
|
|
+ SET enabled = ?
|
|
|
|
|
+ WHERE shop_id = ?
|
|
|
|
|
+ `).run(enabled ? 1 : 0, shopId);
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(`${enabled ? 'Enabled' : 'Disabled'} webhook for shop ${shopId}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logWebhookDelivery(
|
|
|
|
|
+ webhookId: string,
|
|
|
|
|
+ event: string,
|
|
|
|
|
+ payload: any,
|
|
|
|
|
+ status: 'success' | 'failed',
|
|
|
|
|
+ responseCode?: number,
|
|
|
|
|
+ error?: string
|
|
|
|
|
+ ): void {
|
|
|
|
|
+ const id = uuidv4();
|
|
|
|
|
+ const now = new Date().toISOString();
|
|
|
|
|
+
|
|
|
|
|
+ this.db.prepare(`
|
|
|
|
|
+ INSERT INTO webhook_deliveries (id, webhook_id, event, payload, status, response_code, error, attempted_at)
|
|
|
|
|
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
+ `).run(id, webhookId, event, JSON.stringify(payload), status, responseCode || null, error || null, now);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getWebhookDeliveries(shopId: string, limit: number = 50): WebhookDelivery[] {
|
|
|
|
|
+ const stmt = this.db.prepare(`
|
|
|
|
|
+ SELECT wd.* FROM webhook_deliveries wd
|
|
|
|
|
+ INNER JOIN webhooks w ON wd.webhook_id = w.id
|
|
|
|
|
+ WHERE w.shop_id = ?
|
|
|
|
|
+ ORDER BY wd.attempted_at DESC
|
|
|
|
|
+ LIMIT ?
|
|
|
|
|
+ `);
|
|
|
|
|
+ const results = stmt.all(shopId, limit) as any[];
|
|
|
|
|
+
|
|
|
|
|
+ return results.map(r => ({
|
|
|
|
|
+ id: r.id,
|
|
|
|
|
+ webhook_id: r.webhook_id,
|
|
|
|
|
+ event: r.event,
|
|
|
|
|
+ payload: JSON.parse(r.payload),
|
|
|
|
|
+ status: r.status,
|
|
|
|
|
+ response_code: r.response_code,
|
|
|
|
|
+ error: r.error,
|
|
|
|
|
+ attempted_at: r.attempted_at
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
close(): void {
|
|
close(): void {
|
|
|
this.db.close();
|
|
this.db.close();
|
|
|
logger.info('Database connection closed');
|
|
logger.info('Database connection closed');
|