Skip to content

Commit bd3cfb8

Browse files
authored
Merge branch 'geturbackend:main' into feature/db-export-pipeline
2 parents 6c6d544 + a743727 commit bd3cfb8

8 files changed

Lines changed: 146 additions & 45 deletions

File tree

apps/dashboard-api/src/__tests__/project.controller.softDelete.test.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ const mockModel = {
77
findOneAndUpdate: mockFindOneAndUpdate,
88
};
99

10-
const mockDispatchWebhooks = jest.fn();
10+
const mockWebhookQueueAdd = jest.fn();
1111

1212
jest.mock('@urbackend/common', () => ({
1313
Project: {
1414
findOne: mockFindOne,
1515
},
1616
getConnection: jest.fn().mockResolvedValue({}),
1717
getCompiledModel: jest.fn(() => mockModel),
18-
dispatchWebhooks: mockDispatchWebhooks,
18+
webhookQueue: { add: mockWebhookQueueAdd },
1919
enqueueCollectionCleanup: jest.fn().mockResolvedValue(true),
2020
syncCollectionCleanup: jest.fn().mockResolvedValue(true),
2121
AppError: class AppError extends Error {
@@ -161,11 +161,16 @@ describe('Soft Delete in dashboard project.controller', () => {
161161
message: "Document recovered from trash"
162162
});
163163

164-
expect(mockDispatchWebhooks).toHaveBeenCalledWith(expect.objectContaining({
165-
action: 'recover',
166-
document: restoredDoc,
167-
projectId: 'proj_1'
168-
}));
164+
expect(mockWebhookQueueAdd).toHaveBeenCalledWith(
165+
'trigger-webhook',
166+
expect.objectContaining({
167+
event: 'document.recovered',
168+
payload: restoredDoc,
169+
projectId: 'proj_1',
170+
collection: 'posts'
171+
}),
172+
{ removeOnComplete: true }
173+
);
169174
const { syncCollectionCleanup } = require('@urbackend/common');
170175
expect(syncCollectionCleanup).toHaveBeenCalledWith('proj_1', 'posts');
171176
});

apps/dashboard-api/src/controllers/project.controller.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const { getConnection } = require("@urbackend/common");
2222
const { getCompiledModel } = require("@urbackend/common");
2323
const { QueryEngine } = require("@urbackend/common");
2424
const { storageRegistry } = require("@urbackend/common");
25-
const { AppError, dispatchWebhooks, enqueueCollectionCleanup, syncCollectionCleanup } = require("@urbackend/common");
25+
const { AppError, webhookQueue, enqueueCollectionCleanup, syncCollectionCleanup } = require("@urbackend/common");
2626
const { resolveEffectivePlan } = require("@urbackend/common");
2727
const {
2828
deleteProjectByApiKeyCache,
@@ -1055,14 +1055,12 @@ module.exports.recoverRow = async (req, res, next) => {
10551055
return next(new AppError(404, "Document not found or recovery window expired (30 days)."));
10561056
}
10571057

1058-
dispatchWebhooks({
1058+
await webhookQueue.add('trigger-webhook', {
10591059
projectId: project._id,
1060+
event: 'document.recovered',
10601061
collection: collectionName,
1061-
action: "recover",
1062-
document: result,
1063-
documentId: id,
1064-
options: { bypassLimit: true }
1065-
});
1062+
payload: result
1063+
}, { removeOnComplete: true });
10661064

10671065
try {
10681066
await syncCollectionCleanup(projectId, collectionName);

apps/public-api/src/__tests__/softDelete.test.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jest.mock('@urbackend/common', () => {
1818
Project: {},
1919
getConnection: jest.fn().mockResolvedValue({}),
2020
getCompiledModel: jest.fn(() => mockModel),
21-
dispatchWebhooks: jest.fn(),
21+
webhookQueue: { add: jest.fn() },
2222
QueryEngine: jest.fn(),
2323
validateData: jest.fn(),
2424
validateUpdateData: jest.fn(),
@@ -138,12 +138,17 @@ describe('Soft Delete in data.controller', () => {
138138
message: "Document recovered from trash"
139139
});
140140

141-
const { dispatchWebhooks, syncCollectionCleanup } = require('@urbackend/common');
142-
expect(dispatchWebhooks).toHaveBeenCalledWith(expect.objectContaining({
143-
action: 'recover',
144-
document: restoredDoc,
145-
projectId: 'proj_1'
146-
}));
141+
const { webhookQueue, syncCollectionCleanup } = require('@urbackend/common');
142+
expect(webhookQueue.add).toHaveBeenCalledWith(
143+
'trigger-webhook',
144+
expect.objectContaining({
145+
event: 'document.recovered',
146+
payload: restoredDoc,
147+
projectId: 'proj_1',
148+
collection: 'posts'
149+
}),
150+
{ removeOnComplete: true }
151+
);
147152
expect(syncCollectionCleanup).toHaveBeenCalledWith('proj_1', 'posts');
148153
});
149154

apps/public-api/src/controllers/data.controller.js

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { Project } = require("@urbackend/common");
44
const { getConnection } = require("@urbackend/common");
55
const { getCompiledModel } = require("@urbackend/common");
66
const { QueryEngine } = require("@urbackend/common");
7-
const { validateData, validateUpdateData, aggregateSchema, dispatchWebhooks } = require("@urbackend/common");
7+
const { validateData, validateUpdateData, aggregateSchema, webhookQueue } = require("@urbackend/common");
88
const { performance } = require('perf_hooks');
99
const { z } = require("zod");
1010
const {
@@ -83,13 +83,12 @@ module.exports.insertData = async (req, res) => {
8383
);
8484
}
8585

86-
dispatchWebhooks({
86+
await webhookQueue.add('trigger-webhook', {
8787
projectId: project._id,
88+
event: 'document.inserted',
8889
collection: collectionName,
89-
action: 'insert',
90-
document: result.toObject ? result.toObject() : result,
91-
documentId: result._id,
92-
});
90+
payload: result.toObject ? result.toObject() : result
91+
}, { removeOnComplete: true });
9392

9493
if (isDebug) console.log(`[DEBUG] insert data took ${(performance.now() - start).toFixed(2)}ms`);
9594
res.status(201).json(result);
@@ -536,13 +535,12 @@ module.exports.updateSingleData = async (req, res) => {
536535

537536
if (!result) return res.status(404).json({ error: "Document not found." });
538537

539-
dispatchWebhooks({
538+
await webhookQueue.add('trigger-webhook', {
540539
projectId: project._id,
540+
event: 'document.updated',
541541
collection: collectionName,
542-
action: 'update',
543-
document: result,
544-
documentId: result._id,
545-
});
542+
payload: result
543+
}, { removeOnComplete: true });
546544

547545
res.json({ message: "Updated", data: result });
548546
} catch (err) {
@@ -610,13 +608,12 @@ module.exports.deleteSingleDoc = async (req, res) => {
610608
console.error("Failed to enqueue trash cleanup job", { projectId: String(project._id), collectionName, err });
611609
}
612610

613-
dispatchWebhooks({
611+
await webhookQueue.add('trigger-webhook', {
614612
projectId: project._id,
613+
event: 'document.deleted',
615614
collection: collectionName,
616-
action: 'delete',
617-
document: result,
618-
documentId: id,
619-
});
615+
payload: result
616+
}, { removeOnComplete: true });
620617

621618
res.json({ success: true, data: { id }, message: "Document moved to trash" });
622619
} catch (err) {
@@ -679,14 +676,12 @@ module.exports.recoverSingleDoc = async (req, res, next) => {
679676
return next(new AppError(404, "Document not found or recovery window expired (30 days)."));
680677
}
681678

682-
dispatchWebhooks({
679+
await webhookQueue.add('trigger-webhook', {
683680
projectId: project._id,
681+
event: 'document.recovered',
684682
collection: collectionName,
685-
action: 'recover',
686-
document: result,
687-
documentId: id,
688-
options: {}
689-
});
683+
payload: result
684+
}, { removeOnComplete: true });
690685

691686
try {
692687
await syncCollectionCleanup(project._id, collectionName);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { useState, useEffect } from 'react';
2+
import { ArrowUp } from 'lucide-react';
3+
4+
function BackToTop() {
5+
const [isVisible, setIsVisible] = useState(false);
6+
const [isHovered, setIsHovered] = useState(false);
7+
8+
useEffect(() => {
9+
let ticking = false;
10+
11+
const handleScroll = () => {
12+
if (!ticking) {
13+
window.requestAnimationFrame(() => {
14+
setIsVisible(window.scrollY > 300);
15+
ticking = false;
16+
});
17+
ticking = true;
18+
}
19+
};
20+
21+
window.addEventListener('scroll', handleScroll);
22+
handleScroll();
23+
24+
return () => {
25+
window.removeEventListener('scroll', handleScroll);
26+
};
27+
}, []);
28+
29+
const handleClick = () => {
30+
window.scrollTo({
31+
top: 0,
32+
behavior: 'smooth'
33+
});
34+
};
35+
36+
if (!isVisible) {
37+
return null;
38+
}
39+
40+
return (
41+
<button
42+
onClick={handleClick}
43+
title="Back to Top"
44+
aria-label="Back to Top"
45+
onMouseEnter={() => setIsHovered(true)}
46+
onMouseLeave={() => setIsHovered(false)}
47+
style={{
48+
position: 'fixed',
49+
bottom: '2rem',
50+
right: '2rem',
51+
zIndex: 1000,
52+
backgroundColor: '#00f5d4',
53+
color: '#000',
54+
border: 'none',
55+
borderRadius: '50%',
56+
width: '45px',
57+
height: '45px',
58+
cursor: 'pointer',
59+
boxShadow: '0 0 15px rgba(0, 245, 212, 0.4)',
60+
display: 'flex',
61+
alignItems: 'center',
62+
justifyContent: 'center',
63+
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
64+
transition: 'transform 0.2s ease',
65+
}}
66+
>
67+
<ArrowUp size={20} strokeWidth={2.5} />
68+
</button>
69+
);
70+
}
71+
72+
export default BackToTop;

apps/web-dashboard/src/components/Layout/MainLayout.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Sidebar from './Sidebar';
44
import Header from './Header';
55
import ProjectNavbar from './ProjectNavbar';
66
import { useLayout } from '../../context/LayoutContext';
7-
7+
import BackToTop from './BackToTop';
88
// Use the new official logo from public directory
99
const logoImage = "https://cdn.jsdelivr.net/gh/yash-pouranik/urBackend@main/frontend/public/logo.png";
1010

@@ -68,6 +68,7 @@ function MainLayout({ children }) {
6868
{children}
6969
</div>
7070
</div>
71+
<BackToTop />
7172
</div>
7273
);
7374
}

apps/web-dashboard/src/pages/LandingPage/index.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState, useEffect, useRef, useCallback } from 'react';
22
import { Link, useNavigate } from 'react-router-dom';
33
import { useAuth } from '../../context/AuthContext';
4-
4+
import BackToTop from '../../components/Layout/BackToTop';
55
import {
66
Database,
77
Shield,
@@ -907,6 +907,7 @@ function LandingPage() {
907907
</div>
908908

909909
<Footer />
910+
<BackToTop />
910911
</div>
911912
);
912913
}

packages/common/src/queues/webhookQueue.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,30 @@ function initWebhookWorker() {
8282
const worker = new Worker(
8383
"webhook-delivery-queue",
8484
async (job) => {
85+
if (job.name === "trigger-webhook") {
86+
const { projectId, event, collection, payload } = job.data;
87+
const { dispatchWebhooks } = require("../utils/webhookDispatcher");
88+
89+
let action = "update";
90+
if (event.includes("inserted")) action = "insert";
91+
if (event.includes("deleted")) action = "delete";
92+
if (event.includes("recovered")) action = "recover";
93+
94+
try {
95+
await dispatchWebhooks({
96+
projectId,
97+
collection,
98+
action,
99+
document: payload,
100+
documentId: payload?._id,
101+
});
102+
} catch (error) {
103+
console.error(`[Webhook] trigger-webhook failed for projectId: ${projectId}, collection: ${collection}, action: ${action}, documentId: ${payload?._id}`, error);
104+
throw error;
105+
}
106+
return;
107+
}
108+
85109
const { deliveryId, webhookId, attemptNumber } = job.data;
86110

87111
try {

0 commit comments

Comments
 (0)