Skip to content

Commit dc96277

Browse files
committed
fix(dashboard-api): apply PR review findings for createProject
- Extracted finalizeProjectCreation helper for consistent success responses across transactional and retry paths - Updated transaction error detection to strictly check MongoDB code 20 or IllegalOperation - Fixed error response formats to consistently use { success, data, message } (replacing 'error' with 'data' and adding 'data: {}' for generic errors) - Added error logging for abortTransaction failures - Reused getDefaultRlsForCollection for template collections RLS setup - Cleaned up exploratory comments - Added test coverage for transactional success path
1 parent dab9a3c commit dc96277

2 files changed

Lines changed: 61 additions & 38 deletions

File tree

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe('Project Controller - Clone Template', () => {
174174

175175
// Simulate standalone MongoDB (no replica set) — triggers fallback path
176176
jest.spyOn(mongoose, 'startSession').mockRejectedValue(
177-
new Error('Transaction numbers are only allowed on a replica set member or mongos'),
177+
Object.assign(new Error('Transaction numbers are only allowed on a replica set member or mongos'), { code: 20 })
178178
);
179179

180180
// Mock save to behave like Mongoose save
@@ -285,4 +285,26 @@ describe('Project Controller - Clone Template', () => {
285285
expect(emailField).toBeDefined();
286286
expect(passwordField).toBeDefined();
287287
});
288+
289+
it('should create project successfully using transactional path', async () => {
290+
const mockSession = {
291+
startTransaction: jest.fn(),
292+
commitTransaction: jest.fn().mockResolvedValue(),
293+
abortTransaction: jest.fn().mockResolvedValue(),
294+
endSession: jest.fn()
295+
};
296+
mongoose.startSession.mockResolvedValue(mockSession);
297+
298+
req.body = { name: 'Transactional Project' };
299+
300+
await createProject(req, res);
301+
302+
expect(mockSession.startTransaction).toHaveBeenCalled();
303+
expect(mockSession.commitTransaction).toHaveBeenCalled();
304+
expect(mockSession.endSession).toHaveBeenCalled();
305+
expect(res.status).toHaveBeenCalledWith(201);
306+
expect(res.json).toHaveBeenCalledWith(
307+
expect.objectContaining({ success: true, message: 'Project created successfully' })
308+
);
309+
});
288310
});

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

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,22 @@ const bestEffortDeleteUploadedObject = async (project, filePath) => {
267267
};
268268

269269
module.exports.createProject = async (req, res) => {
270+
const finalizeProjectCreation = (projectObj, newProject) => {
271+
markDeveloperOnboardingStep(req.user._id, 'projectCreated', { projectId: newProject._id })
272+
.then(() => {
273+
// Also reset subsequent steps since this is a new project
274+
return Promise.all([
275+
markDeveloperOnboardingStep(req.user._id, 'collectionCreated', { _reset: true }),
276+
markDeveloperOnboardingStep(req.user._id, 'firstApiCall', { _reset: true })
277+
]);
278+
})
279+
.catch((err) => {
280+
console.error('[onboarding] Failed to mark projectCreated:', err.message);
281+
});
282+
emitEvent(req.user._id, 'project_created', { projectName: projectObj.name }, newProject._id);
283+
return res.status(201).json({ success: true, data: projectObj, message: "Project created successfully" });
284+
};
285+
270286
const executeOperation = async (session) => {
271287
const { name, description, siteUrl, templateId } = createProjectSchema.parse(req.body);
272288

@@ -307,20 +323,18 @@ module.exports.createProject = async (req, res) => {
307323
const template = PROJECT_TEMPLATES[templateId];
308324
if (template.isAuthEnabled) {
309325
newProject.isAuthEnabled = true;
310-
// The default users collection is appended later or explicitly, let's see.
311-
// Wait, does 'users' collection get automatically added if isAuthEnabled is set?
312-
// Let's add it explicitly if not present.
313326
}
314327
if (template.collections && template.collections.length > 0) {
315328
newProject.collections = template.collections.map(col => {
316329
const mode = typeof col.rls === 'string' ? col.rls : (col.rls?.mode || 'public-read');
330+
const defaultRls = getDefaultRlsForCollection(col.name, col.model);
317331
return {
318332
name: col.name,
319333
model: col.model,
320334
rls: {
321335
enabled: mode !== 'public-read',
322336
mode,
323-
ownerField: col.name === 'users' ? '_id' : 'userId',
337+
ownerField: defaultRls.ownerField,
324338
requireAuthForWrite: true
325339
}
326340
};
@@ -364,54 +378,41 @@ module.exports.createProject = async (req, res) => {
364378

365379
await session.commitTransaction();
366380
session.endSession();
367-
368-
markDeveloperOnboardingStep(req.user._id, 'projectCreated', { projectId: newProject._id })
369-
.then(() => {
370-
// Also reset subsequent steps since this is a new project
371-
return Promise.all([
372-
markDeveloperOnboardingStep(req.user._id, 'collectionCreated', { _reset: true }),
373-
markDeveloperOnboardingStep(req.user._id, 'firstApiCall', { _reset: true })
374-
]);
375-
})
376-
.catch((err) => {
377-
console.error('[onboarding] Failed to mark projectCreated:', err.message);
378-
});
379-
emitEvent(req.user._id, 'project_created', { projectName: projectObj.name }, newProject._id);
380-
return res.status(201).json({ success: true, data: projectObj, message: "Project created successfully" });
381+
return finalizeProjectCreation(projectObj, newProject);
381382
} catch (err) {
382383
if (session) {
383-
try { await session.abortTransaction(); } catch (e) {}
384+
try {
385+
await session.abortTransaction();
386+
} catch (e) {
387+
console.error("Failed to abort transaction:", e.message);
388+
}
384389
session.endSession();
385390
}
386391

387-
const isTransactionError = err.message && (
388-
err.message.includes("Transaction numbers are only allowed") ||
389-
err.message.includes("buffering timed out") ||
390-
err.message.includes("standalone") ||
391-
err.message.includes("replica set") ||
392-
err.message.includes("Session")
393-
);
392+
const isTransactionError = err.message &&
393+
err.message.includes("Transaction numbers are only allowed") &&
394+
(err.code === 20 || err.codeName === 'IllegalOperation');
394395

395396
if (isTransactionError) {
396397
try {
397398
const { projectObj, newProject } = await executeOperation(null);
398-
markDeveloperOnboardingStep(req.user._id, 'projectCreated', { projectId: newProject._id }).catch((err) => {
399-
console.error('[onboarding] Failed to mark projectCreated:', err.message);
400-
});
401-
emitEvent(req.user._id, 'project_created', { projectName: projectObj.name }, newProject._id);
402-
return res.status(201).json({ success: true, data: projectObj, message: "Project created successfully" });
399+
return finalizeProjectCreation(projectObj, newProject);
403400
} catch (retryErr) {
404-
if (retryErr instanceof z.ZodError) return res.status(400).json({ success: false, message: "Validation failed", error: retryErr.issues });
405-
const statusCode = (retryErr instanceof AppError ? retryErr.statusCode : null) || 500;
401+
if (retryErr instanceof z.ZodError) {
402+
return res.status(400).json({ success: false, data: retryErr.issues, message: "Validation failed" });
403+
}
404+
const statusCode = retryErr instanceof AppError ? retryErr.statusCode : 500;
406405
const message = statusCode === 500 ? "Internal server error" : retryErr.message;
407-
return res.status(statusCode).json({ success: false, message: message });
406+
return res.status(statusCode).json({ success: false, data: {}, message });
408407
}
409408
}
410409

411-
if (err instanceof z.ZodError) return res.status(400).json({ success: false, message: "Validation failed", error: err.issues });
412-
const statusCode = (err instanceof AppError ? err.statusCode : null) || 500;
410+
if (err instanceof z.ZodError) {
411+
return res.status(400).json({ success: false, data: err.issues, message: "Validation failed" });
412+
}
413+
const statusCode = err instanceof AppError ? err.statusCode : 500;
413414
const message = statusCode === 500 ? "Internal server error" : err.message;
414-
return res.status(statusCode).json({ success: false, message: message });
415+
return res.status(statusCode).json({ success: false, data: {}, message });
415416
}
416417
};
417418

0 commit comments

Comments
 (0)