Tracking table at bottom. Check a box (- [x]) immediately when a task is done.
These must be done first. Everything in Phase 2 depends on them.
- Change
app.jsservice loader to store each service onappviaapp.set()- Currently:
require(routesServices + file)(app)discards the return value - Target:
app.set('topicService', require('./services/topic')(app))etc. - Derive the key name from the filename (e.g.
topic.js→topicService) - Update all existing
require('../../services/...')local imports in routes to useapp.get('...')
- Currently:
- Create
services/activityLog.jsexposingwithTransaction(fn)as a thin wrapper overdb.transaction(), so route handlers no longer couple directly to Sequelize internals for transaction management. Registered asactivityLogServiceon app.
- Create
services/permissions.jsconsolidating the duplicated permission SQL from:services/topic.jslines 61–170 (hasPermission/_hasPermission)routes/api/group.jslines 39–91 (_hasPermission)- Exposes:
hasTopicPermission,hasTopicVisibility,isModerator,hasModeratorPermission,_topicPermission,hasGroupPermission,_groupPermission - Registered as
permissionsServiceon app
- Replace inline permission SQL in
routes/api/group.jswithpermissionsService.hasGroupPermissionand_groupPermission - Replace permission SQL in
services/topic.jswith lazy wrappers delegating topermissionsService
Extract all DB logic out of route files into dedicated services.
- Extract the 11 repeated
Topic.findOnepatterns fromroutes/api/topic.jsinto named helpers:-
getById(topicId, options)— base fetch -
getWithMembers(topicId, userId)— includes TopicMemberUser, TopicMemberGroup -
getWithVote(topicId, userId)— includes Vote, VoteOption -
getWithIdeation(topicId)— includes Ideation
-
- Move all
Topic.create/Topic.update/Topic.destroycalls out of route handlers into service methods - Move Etherpad-related topic operations (
cosEtherpad.*) into service methods so routes don't call them directly
- Create
services/groupService.jscovering:getById(groupId, userId)— fetch with permission levelgetByIdPublic(groupId, userId)— public read (visibility=public only)list(userId, filters)— user's groups list querycreate(data, actorId, transaction)— create + activity logupdate(group, data, actorId, transaction)— update + activity logremove(group, actorId, transaction)— soft delete + activity logupdateMemberLevel(groupId, memberId, newLevel, actorId, transaction)removeMember(groupId, memberId, actorId, transaction)
- Remove extracted logic from
routes/api/group.js, replace with service calls - Route handlers should only: validate input → call service → send response
- Create
services/discussionService.js:- Deduplicated the SQL fetch into a single
getById(discussionId)function create,update,removewith activity logginggetParticipants(discussionId)— extract DB query from route
- Deduplicated the SQL fetch into a single
- Remove extracted logic from
routes/api/discussion.js
- Create
services/ideationService.jsto consolidate 10 raw SQL queries inroutes/api/ideation.js:getById(ideationId, topicId)listIdeas(ideationId, filters, userId)— paginated, with vote countscreateIdea(ideationId, data, authorId, transaction)updateIdea(ideaId, data, actorId, transaction)deleteIdea(ideaId, actorId, transaction)getMemberList(ideationId)— the raw member SQL query
- Evaluate each raw SQL query: convert to Sequelize where possible, keep raw only where complex CTE/window functions are required
- Remove extracted logic from
routes/api/ideation.js
-
getVoteResults(voteId, userId)andgetAllVotesResultsalready exist in the service and are used from routes — no work needed.
Do after Phase 2 services exist.
- Split into logical sub-files by resource, mounted from a thin
topic.jsrouter:routes/api/topic/members.js— member CRUD endpointsroutes/api/topic/invites.js— invite endpointsroutes/api/topic/events.js— TopicEvent endpointsroutes/api/topic/attachments.js— attachment endpointsroutes/api/topic/index.js— core CRUD (create, read, update, delete, list)
- Each handler: validate → call service → respond. No raw DB calls.
- Same pattern as topic split:
routes/api/group/members.jsroutes/api/group/invites.jsroutes/api/group/index.js
- Audit all remaining raw SQL queries after Phase 2 extractions
- For each: document why raw SQL is needed, or convert to Sequelize with includes/subqueries
- Add a comment
// Raw SQL: reasonabove any raw query that must remain
- Ensure all route handlers use
res.ok(),res.created(),res.badRequest()etc. — remove anyres.status().json()calls that bypass the response middleware - Ensure all async route handlers are wrapped in
asyncMiddleware(no uncaught promise rejections)
- After all services are registered via
app.set(), remove any remaining localrequire('../../services/...')calls in route files
- Audit all
db.transaction()calls: ensuret.afterCommit()is used for side effects (email, response) consistently across all files- All files we modified (discussion.js, group.js) use
afterCommitcorrectly for success responses - Pre-existing validation/error
res.calls inside transaction blocks are intentional early-returns (roll back automatically)
- All files we modified (discussion.js, group.js) use
- Ensure no response is sent before
afterCommitwhere a transaction is involved
-
npm run eslintpasses with 0 errors (0 errors, 577 warnings — warnings are pre-existing) -
npm test— 36 pre-existing failures (Smart-ID/MobileID external services, rate-limit tests, comment ordering tests); no new failures introduced by refactoring
| Phase | Tasks | Done |
|---|---|---|
| 1 — Foundation | 5 | 5 |
| 2 — Entity Services | 14 | 14 |
| 3 — Route Cleanup | 7 | 7 |
| 4 — Quality | 4 | 4 |
| Total | 30 | 30 |