Skip to content

Commit ca8a587

Browse files
Merge branch 'main' into fix/template-clone-instructions
2 parents 9cca8dd + cbdb5f1 commit ca8a587

67 files changed

Lines changed: 14233 additions & 5460 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Main apps:
88
- `apps/dashboard-api`: admin/project management API for the dashboard
99
- `apps/public-api`: public project API for data, auth, storage
1010
- `apps/web-dashboard`: React/Vite dashboard
11+
- `apps/consumer`: Consumer Worker for background jobs and webhooks
12+
- `apps/python-service`: Python AI Microservice (FastAPI/Groq)
1113
- `packages/common`: shared models, validation, middleware, encryption, DB/model utilities
1214

1315
SDKs:
@@ -18,6 +20,13 @@ SDKs:
1820

1921
Workspace scripts are defined in [package.json](/package.json).
2022

23+
## Production Deployment Endpoints & Domains
24+
25+
- **Frontend Landing**: `https://urbackend.bitbros.in`
26+
- **Dashboard App**: `https://app.urbackend.bitbros.in`
27+
- **Dashboard API (Internal/Admin)**: `https://api.urbackend.bitbros.in`
28+
- **Public API (SDK & User Auth)**: `https://api.ub.bitbros.in`
29+
2130
## Important project rules
2231

2332
1. Do not treat `users` like a normal collection.
@@ -151,6 +160,12 @@ cd apps/web-dashboard
151160
npm run build
152161
```
153162

163+
Run python service tests:
164+
```bash
165+
cd apps/python-service
166+
pytest
167+
```
168+
154169
Run SDK tests:
155170
```bash
156171
# JS Core SDK
@@ -169,6 +184,7 @@ pytest
169184
Before shipping auth, RLS, or schema changes:
170185
- run `apps/public-api` tests
171186
- run `apps/dashboard-api` tests
187+
- run `apps/python-service` tests
172188
- run `@urbackend/sdk` tests
173189
- run `@urbackend/react` tests
174190
- run `apps/web-dashboard` lint
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
'use strict';
2+
3+
const mockFindOne = jest.fn();
4+
const mockDeleteProjectById = jest.fn();
5+
const mockSetProjectById = jest.fn();
6+
const mockDeleteProjectByApiKeyCache = jest.fn();
7+
8+
jest.mock('@urbackend/common', () => ({
9+
Project: {
10+
findOne: mockFindOne,
11+
},
12+
getProjectAccessQuery: jest.fn((userId) => ({ $or: [{ owner: userId }, { "members.user": userId }] })),
13+
editCollectionSchema: {
14+
parse: jest.fn()
15+
},
16+
deleteProjectById: mockDeleteProjectById,
17+
setProjectById: mockSetProjectById,
18+
deleteProjectByApiKeyCache: mockDeleteProjectByApiKeyCache,
19+
AppError: class AppError extends Error {
20+
constructor(statusCode, message) {
21+
super(message);
22+
this.statusCode = statusCode;
23+
this.status = statusCode;
24+
}
25+
},
26+
getConnection: jest.fn(() => ({
27+
db: {
28+
listCollections: jest.fn(() => ({
29+
hasNext: jest.fn(() => Promise.resolve(false))
30+
}))
31+
}
32+
})),
33+
getCompiledModel: jest.fn(() => ({})),
34+
createUniqueIndexes: jest.fn(() => Promise.resolve()),
35+
clearCompiledModel: jest.fn()
36+
}));
37+
38+
const mockEmitEvent = jest.fn();
39+
40+
jest.mock('../utils/emitEvent', () => ({
41+
emitEvent: mockEmitEvent
42+
}));
43+
44+
const projectController = require('../controllers/project.controller');
45+
const { editCollectionSchema, Project } = require('@urbackend/common');
46+
const { z } = require('zod');
47+
48+
describe('projectController.updateCollection', () => {
49+
let req;
50+
let res;
51+
let next;
52+
53+
beforeEach(() => {
54+
jest.clearAllMocks();
55+
req = {
56+
user: { _id: 'user123' },
57+
params: { projectId: 'project123', collectionName: 'myCollection' },
58+
body: {
59+
schema: [
60+
{ key: 'title', type: 'String', required: true }
61+
]
62+
}
63+
};
64+
65+
res = {
66+
status: jest.fn().mockReturnThis(),
67+
json: jest.fn()
68+
};
69+
70+
next = jest.fn();
71+
72+
editCollectionSchema.parse.mockReturnValue({
73+
projectId: req.params.projectId,
74+
collectionName: req.params.collectionName,
75+
schema: req.body.schema
76+
});
77+
});
78+
79+
it('should return 400 if validation fails', async () => {
80+
const zodError = new z.ZodError([{ message: 'Invalid data' }]);
81+
editCollectionSchema.parse.mockImplementation(() => { throw zodError; });
82+
83+
await projectController.updateCollection(req, res, next);
84+
85+
expect(next).toHaveBeenCalledWith(expect.any(Error));
86+
const error = next.mock.calls[0][0];
87+
expect(error.statusCode).toBe(400);
88+
});
89+
90+
it('should return 404 if project not found', async () => {
91+
Project.findOne.mockResolvedValue(null);
92+
await projectController.updateCollection(req, res, next);
93+
94+
expect(next).toHaveBeenCalledWith(expect.any(Error));
95+
const error = next.mock.calls[0][0];
96+
expect(error.statusCode).toBe(404);
97+
expect(error.message).toBe('Project not found');
98+
});
99+
100+
it('should return 404 if collection not found in project', async () => {
101+
const mockProject = {
102+
_id: 'project123',
103+
collections: [
104+
{ name: 'otherCollection', model: [] }
105+
],
106+
save: jest.fn()
107+
};
108+
Project.findOne.mockResolvedValue(mockProject);
109+
110+
await projectController.updateCollection(req, res, next);
111+
112+
expect(next).toHaveBeenCalledWith(expect.any(Error));
113+
const error = next.mock.calls[0][0];
114+
expect(error.statusCode).toBe(404);
115+
expect(error.message).toBe('Collection not found');
116+
});
117+
118+
it('should return 422 if users validation fails when editing users collection', async () => {
119+
req.params.collectionName = 'users';
120+
req.body.schema = [{ key: 'username', type: 'String', required: true }]; // Invalid users schema
121+
editCollectionSchema.parse.mockReturnValue({
122+
projectId: req.params.projectId,
123+
collectionName: req.params.collectionName,
124+
schema: req.body.schema
125+
});
126+
127+
const mockProject = {
128+
_id: 'project123',
129+
collections: [
130+
{ name: 'users', model: [] }
131+
],
132+
save: jest.fn()
133+
};
134+
Project.findOne.mockResolvedValue(mockProject);
135+
136+
await projectController.updateCollection(req, res, next);
137+
138+
expect(next).toHaveBeenCalledWith(expect.any(Error));
139+
const error = next.mock.calls[0][0];
140+
expect(error.statusCode).toBe(422);
141+
expect(error.message).toContain("The 'users' collection must have required 'email' and 'password' string fields.");
142+
});
143+
144+
it('should successfully update a collection schema', async () => {
145+
const mockProject = {
146+
_id: 'project123',
147+
collections: [
148+
{
149+
name: 'myCollection',
150+
model: [{ key: 'oldTitle', type: 'String' }]
151+
}
152+
],
153+
resources: { db: { isExternal: false } },
154+
save: jest.fn().mockResolvedValue(true),
155+
toObject: jest.fn().mockReturnValue({
156+
_id: 'project123',
157+
collections: [
158+
{ name: 'myCollection', model: [{ key: 'title', type: 'String', required: true }] }
159+
]
160+
})
161+
};
162+
Project.findOne.mockResolvedValue(mockProject);
163+
164+
await projectController.updateCollection(req, res, next);
165+
166+
expect(mockProject.collections[0].model).toEqual(req.body.schema);
167+
expect(mockProject.save).toHaveBeenCalled();
168+
expect(mockDeleteProjectById).toHaveBeenCalledWith('project123');
169+
expect(mockSetProjectById).toHaveBeenCalled();
170+
expect(mockEmitEvent).toHaveBeenCalledWith('user123', 'collection_updated', { collectionName: 'myCollection', isUsersCollection: false }, 'project123');
171+
expect(res.status).toHaveBeenCalledWith(200);
172+
});
173+
174+
it('should successfully update users collection if valid', async () => {
175+
req.params.collectionName = 'users';
176+
req.body.schema = [
177+
{ key: 'email', type: 'String', required: true },
178+
{ key: 'password', type: 'String', required: true }
179+
];
180+
editCollectionSchema.parse.mockReturnValue({
181+
projectId: req.params.projectId,
182+
collectionName: req.params.collectionName,
183+
schema: req.body.schema
184+
});
185+
186+
const mockProject = {
187+
_id: 'project123',
188+
collections: [
189+
{ name: 'users', model: [] }
190+
],
191+
resources: { db: { isExternal: false } },
192+
save: jest.fn().mockResolvedValue(true),
193+
toObject: jest.fn().mockReturnValue({
194+
_id: 'project123',
195+
collections: [
196+
{ name: 'users', model: req.body.schema }
197+
]
198+
})
199+
};
200+
Project.findOne.mockResolvedValue(mockProject);
201+
202+
await projectController.updateCollection(req, res, next);
203+
204+
expect(mockProject.collections[0].model).toEqual(req.body.schema);
205+
expect(mockProject.save).toHaveBeenCalled();
206+
expect(mockEmitEvent).toHaveBeenCalledWith('user123', 'collection_updated', { collectionName: 'users', isUsersCollection: true }, 'project123');
207+
expect(res.status).toHaveBeenCalledWith(200);
208+
});
209+
});

apps/dashboard-api/src/__tests__/routes.auth.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jest.mock('../middlewares/authMiddleware', () =>
1212
// Mock auth_limiter to pass all requests through.
1313
jest.mock('../middlewares/auth_limiter', () => ({
1414
authLimiter: jest.fn((_req, _res, next) => next()),
15+
publicLimiter: jest.fn((_req, _res, next) => next()),
1516
}));
1617

1718
// Mock express-rate-limit so dashboardLimiter also passes all requests.

apps/dashboard-api/src/__tests__/routes.projects.storage.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ jest.mock('../controllers/project.controller', () => {
9191
inviteMember: jest.fn(ok),
9292
updateMemberRole: jest.fn(ok),
9393
removeMember: jest.fn(ok),
94+
updateCollection: jest.fn(ok),
9495
};
9596
});
9697

apps/dashboard-api/src/app.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const whitelist = (function() {
5454
app.use(cors({
5555
origin: whitelist.get(),
5656
credentials: true,
57+
maxAge: 86400
5758
}));
5859

5960
app.use(express.json({
@@ -87,7 +88,10 @@ app.use((req, res, next) => {
8788
const authHeader = req.headers.authorization || '';
8889
const isPATRequest = authHeader.startsWith('Bearer ubpat_');
8990

90-
if (req.path === '/api/billing/webhook' || isCliRoute || isPATRequest) {
91+
const bypassKey = process.env.LOADTEST_BYPASS_KEY;
92+
const isLoadTestBypass = bypassKey && req.headers['x-bypass-rate-limit'] === bypassKey;
93+
94+
if (req.path === '/api/billing/webhook' || isCliRoute || isPATRequest || isLoadTestBypass) {
9195
return next();
9296
}
9397
csrfProtection(req, res, next);

0 commit comments

Comments
 (0)