From 9ff3ca112c9bb0412977e3bb28d245d9b2bf5e34 Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 17 Aug 2025 00:39:51 +0200 Subject: [PATCH] refactor: split links controller into multiple controllers --- .adonisjs/api.ts | 14 +- app/admin/controllers/admin_controller.ts | 8 +- .../controllers/base_collection_controller.ts | 32 ----- .../create_collection_controller.ts | 12 +- .../delete_collection_controller.ts | 9 +- .../show_collections_controller.ts | 13 +- .../update_collection_controller.ts | 11 +- .../services/collection_service.ts | 27 +++- .../validators/collection_id_validator.ts | 7 + .../controllers/create_link_controller.ts | 30 ++++ .../controllers/delete_link_controller.ts | 39 ++++++ app/links/controllers/links_controller.ts | 129 ------------------ .../controllers/toggle_favorite_controller.ts | 19 +++ .../controllers/update_link_controller.ts | 34 +++++ app/links/routes/links_routes.ts | 24 ++-- app/links/services/link_service.ts | 60 ++++++++ 16 files changed, 260 insertions(+), 208 deletions(-) delete mode 100644 app/collections/controllers/base_collection_controller.ts create mode 100644 app/collections/validators/collection_id_validator.ts create mode 100644 app/links/controllers/create_link_controller.ts create mode 100644 app/links/controllers/delete_link_controller.ts delete mode 100644 app/links/controllers/links_controller.ts create mode 100644 app/links/controllers/toggle_favorite_controller.ts create mode 100644 app/links/controllers/update_link_controller.ts create mode 100644 app/links/services/link_service.ts diff --git a/.adonisjs/api.ts b/.adonisjs/api.ts index bebfcdc..9fdd8f3 100644 --- a/.adonisjs/api.ts +++ b/.adonisjs/api.ts @@ -99,7 +99,7 @@ type FaviconGetHead = { type LinksCreateGetHead = { request: unknown; response: MakeTuyauResponse< - import('../app/links/controllers/links_controller.ts').default['showCreatePage'], + import('../app/links/controllers/delete_link_controller.js').default['showCreatePage'], false >; }; @@ -110,14 +110,14 @@ type LinksPost = { > >; response: MakeTuyauResponse< - import('../app/links/controllers/links_controller.ts').default['store'], + import('../app/links/controllers/delete_link_controller.js').default['store'], true >; }; type LinksEditGetHead = { request: unknown; response: MakeTuyauResponse< - import('../app/links/controllers/links_controller.ts').default['showEditPage'], + import('../app/links/controllers/delete_link_controller.js').default['showEditPage'], false >; }; @@ -128,7 +128,7 @@ type LinksIdPut = { > >; response: MakeTuyauResponse< - import('../app/links/controllers/links_controller.ts').default['update'], + import('../app/links/controllers/delete_link_controller.js').default['update'], true >; }; @@ -139,14 +139,14 @@ type LinksIdFavoritePut = { > >; response: MakeTuyauResponse< - import('../app/links/controllers/links_controller.ts').default['toggleFavorite'], + import('../app/links/controllers/delete_link_controller.js').default['toggleFavorite'], true >; }; type LinksDeleteGetHead = { request: unknown; response: MakeTuyauResponse< - import('../app/links/controllers/links_controller.ts').default['showDeletePage'], + import('../app/links/controllers/delete_link_controller.js').default['showDeletePage'], false >; }; @@ -157,7 +157,7 @@ type LinksIdDelete = { > >; response: MakeTuyauResponse< - import('../app/links/controllers/links_controller.ts').default['delete'], + import('../app/links/controllers/delete_link_controller.js').default['execute'], true >; }; diff --git a/app/admin/controllers/admin_controller.ts b/app/admin/controllers/admin_controller.ts index 6e24e0a..71679c0 100644 --- a/app/admin/controllers/admin_controller.ts +++ b/app/admin/controllers/admin_controller.ts @@ -1,6 +1,6 @@ import AuthController from '#auth/controllers/auth_controller'; -import CollectionsController from '#collections/controllers/show_collections_controller'; -import LinksController from '#links/controllers/links_controller'; +import { CollectionService } from '#collections/services/collection_service'; +import LinksController from '#links/controllers/delete_link_controller'; import User from '#user/models/user'; import { inject } from '@adonisjs/core'; import { HttpContext } from '@adonisjs/core/http'; @@ -28,14 +28,14 @@ export default class AdminController { constructor( protected usersController: AuthController, protected linksController: LinksController, - protected collectionsController: CollectionsController + protected collectionService: CollectionService ) {} async index({ inertia }: HttpContext) { const users = await this.usersController.getAllUsersWithTotalRelations(); const linksCount = await this.linksController.getTotalLinksCount(); const collectionsCount = - await this.collectionsController.getTotalCollectionsCount(); + await this.collectionService.getTotalCollectionsCount(); return inertia.render('admin/dashboard', { users: users.map((user) => new UserWithRelationCountDto(user).toJson()), diff --git a/app/collections/controllers/base_collection_controller.ts b/app/collections/controllers/base_collection_controller.ts deleted file mode 100644 index 297e189..0000000 --- a/app/collections/controllers/base_collection_controller.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Collection from '#collections/models/collection'; -import { HttpContext } from '@adonisjs/core/http'; -import vine from '@vinejs/vine'; - -export default class BaseCollectionController { - protected collectionIdValidator = vine.compile( - vine.object({ - collectionId: vine.number().positive().optional(), - }) - ); - - async validateCollectionId(collectionIdRequired: boolean = true) { - const ctx = HttpContext.getOrFail(); - const { collectionId } = await ctx.request.validateUsing( - this.collectionIdValidator - ); - if (!collectionId && collectionIdRequired) { - console.log('redirecting to dashboard'); - ctx.response.redirectToNamedRoute('dashboard'); - return null; - } - console.log('collectionId', collectionId); - return collectionId; - } - - redirectToCollectionId(collectionId: Collection['id']) { - const ctx = HttpContext.getOrFail(); - return ctx.response.redirectToNamedRoute('dashboard', { - qs: { collectionId }, - }); - } -} diff --git a/app/collections/controllers/create_collection_controller.ts b/app/collections/controllers/create_collection_controller.ts index d993006..17aba1b 100644 --- a/app/collections/controllers/create_collection_controller.ts +++ b/app/collections/controllers/create_collection_controller.ts @@ -1,17 +1,15 @@ -import BaseCollectionController from '#collections/controllers/base_collection_controller'; import { CollectionService } from '#collections/services/collection_service'; import { createCollectionValidator } from '#collections/validators/create_collection_validator'; import { inject } from '@adonisjs/core'; import { type HttpContext } from '@adonisjs/core/http'; @inject() -export default class CreateCollectionController extends BaseCollectionController { - constructor(private collectionService: CollectionService) { - super(); - } +export default class CreateCollectionController { + constructor(private collectionService: CollectionService) {} async render({ inertia }: HttpContext) { - const collections = await this.collectionService.getCollectionsByAuthorId(); + const collections = + await this.collectionService.getCollectionsForAuthenticatedUser(); return inertia.render('collections/create', { disableHomeLink: collections.length === 0, }); @@ -20,6 +18,6 @@ export default class CreateCollectionController extends BaseCollectionController async execute({ request }: HttpContext) { const payload = await request.validateUsing(createCollectionValidator); const collection = await this.collectionService.createCollection(payload); - return this.redirectToCollectionId(collection.id); + return this.collectionService.redirectToCollectionId(collection.id); } } diff --git a/app/collections/controllers/delete_collection_controller.ts b/app/collections/controllers/delete_collection_controller.ts index 4b35766..00fd762 100644 --- a/app/collections/controllers/delete_collection_controller.ts +++ b/app/collections/controllers/delete_collection_controller.ts @@ -1,17 +1,14 @@ -import BaseCollectionController from '#collections/controllers/base_collection_controller'; import { CollectionService } from '#collections/services/collection_service'; import { deleteCollectionValidator } from '#collections/validators/delete_collection_validator'; import { inject } from '@adonisjs/core'; import { HttpContext } from '@adonisjs/core/http'; @inject() -export default class DeleteCollectionController extends BaseCollectionController { - constructor(private collectionService: CollectionService) { - super(); - } +export default class DeleteCollectionController { + constructor(private collectionService: CollectionService) {} async render({ inertia }: HttpContext) { - const collectionId = await this.validateCollectionId(); + const collectionId = await this.collectionService.validateCollectionId(); if (!collectionId) return; const collection = diff --git a/app/collections/controllers/show_collections_controller.ts b/app/collections/controllers/show_collections_controller.ts index cbeb5a6..b3433d1 100644 --- a/app/collections/controllers/show_collections_controller.ts +++ b/app/collections/controllers/show_collections_controller.ts @@ -1,18 +1,17 @@ -import BaseCollectionController from '#collections/controllers/base_collection_controller'; import { CollectionService } from '#collections/services/collection_service'; import { inject } from '@adonisjs/core'; import type { HttpContext } from '@adonisjs/core/http'; @inject() -export default class ShowCollectionsController extends BaseCollectionController { - constructor(private collectionService: CollectionService) { - super(); - } +export default class ShowCollectionsController { + constructor(private collectionService: CollectionService) {} // Dashboard async render({ inertia, response }: HttpContext) { - const activeCollectionId = await this.validateCollectionId(false); - const collections = await this.collectionService.getCollectionsByAuthorId(); + const activeCollectionId = + await this.collectionService.validateCollectionId(false); + const collections = + await this.collectionService.getCollectionsForAuthenticatedUser(); if (collections.length === 0) { return response.redirectToNamedRoute('collection.create-form'); } diff --git a/app/collections/controllers/update_collection_controller.ts b/app/collections/controllers/update_collection_controller.ts index 09fe854..26f4036 100644 --- a/app/collections/controllers/update_collection_controller.ts +++ b/app/collections/controllers/update_collection_controller.ts @@ -1,17 +1,14 @@ -import BaseCollectionController from '#collections/controllers/base_collection_controller'; import { CollectionService } from '#collections/services/collection_service'; import { updateCollectionValidator } from '#collections/validators/update_collection_validator'; import { inject } from '@adonisjs/core'; import { HttpContext } from '@adonisjs/core/http'; @inject() -export default class UpdateCollectionController extends BaseCollectionController { - constructor(private collectionService: CollectionService) { - super(); - } +export default class UpdateCollectionController { + constructor(private collectionService: CollectionService) {} async render({ inertia }: HttpContext) { - const collectionId = await this.validateCollectionId(); + const collectionId = await this.collectionService.validateCollectionId(); if (!collectionId) return; const collection = @@ -28,6 +25,6 @@ export default class UpdateCollectionController extends BaseCollectionController } = await request.validateUsing(updateCollectionValidator); await this.collectionService.updateCollection(collectionId, payload); - return this.redirectToCollectionId(collectionId); + return this.collectionService.redirectToCollectionId(collectionId); } } diff --git a/app/collections/services/collection_service.ts b/app/collections/services/collection_service.ts index 01f0805..db4cf31 100644 --- a/app/collections/services/collection_service.ts +++ b/app/collections/services/collection_service.ts @@ -1,5 +1,6 @@ import { Visibility } from '#collections/enums/visibility'; import Collection from '#collections/models/collection'; +import { collectionIdValidator } from '#collections/validators/collection_id_validator'; import { HttpContext } from '@adonisjs/core/http'; import db from '@adonisjs/lucid/services/db'; @@ -20,7 +21,7 @@ export class CollectionService { .firstOrFail(); } - async getCollectionsByAuthorId() { + async getCollectionsForAuthenticatedUser() { const context = this.getAuthContext(); return await Collection.query() .where('author_id', context.auth.user!.id) @@ -67,4 +68,28 @@ export class CollectionService { } return context; } + + async validateCollectionId(collectionIdRequired: boolean = true) { + const ctx = HttpContext.getOrFail(); + const { collectionId } = await ctx.request.validateUsing( + collectionIdValidator + ); + if (!collectionId && collectionIdRequired) { + this.redirectToDashboard(); + return null; + } + return collectionId; + } + + redirectToCollectionId(collectionId: Collection['id']) { + const ctx = HttpContext.getOrFail(); + return ctx.response.redirectToNamedRoute('dashboard', { + qs: { collectionId }, + }); + } + + redirectToDashboard() { + const ctx = HttpContext.getOrFail(); + return ctx.response.redirectToNamedRoute('dashboard'); + } } diff --git a/app/collections/validators/collection_id_validator.ts b/app/collections/validators/collection_id_validator.ts new file mode 100644 index 0000000..27ce486 --- /dev/null +++ b/app/collections/validators/collection_id_validator.ts @@ -0,0 +1,7 @@ +import vine from '@vinejs/vine'; + +export const collectionIdValidator = vine.compile( + vine.object({ + collectionId: vine.number().positive().optional(), + }) +); diff --git a/app/links/controllers/create_link_controller.ts b/app/links/controllers/create_link_controller.ts new file mode 100644 index 0000000..8c28558 --- /dev/null +++ b/app/links/controllers/create_link_controller.ts @@ -0,0 +1,30 @@ +import { CollectionService } from '#collections/services/collection_service'; +import { LinkService } from '#links/services/link_service'; +import { createLinkValidator } from '#links/validators/create_link_validator'; +import { inject } from '@adonisjs/core'; +import { HttpContext } from '@adonisjs/core/http'; + +@inject() +export default class CreateLinkController { + constructor( + private linkService: LinkService, + private collectionsService: CollectionService + ) {} + + async render({ inertia }: HttpContext) { + const collections = + await this.collectionsService.getCollectionsForAuthenticatedUser(); + return inertia.render('links/create', { collections }); + } + + async execute({ request }: HttpContext) { + const { collectionId, ...payload } = + await request.validateUsing(createLinkValidator); + + await this.linkService.createLink({ + ...payload, + collectionId, + }); + return this.collectionsService.redirectToCollectionId(collectionId); + } +} diff --git a/app/links/controllers/delete_link_controller.ts b/app/links/controllers/delete_link_controller.ts new file mode 100644 index 0000000..6cf9a7e --- /dev/null +++ b/app/links/controllers/delete_link_controller.ts @@ -0,0 +1,39 @@ +import { CollectionService } from '#collections/services/collection_service'; +import { LinkService } from '#links/services/link_service'; +import { deleteLinkValidator } from '#links/validators/delete_link_validator'; +import { inject } from '@adonisjs/core'; +import { HttpContext } from '@adonisjs/core/http'; +import db from '@adonisjs/lucid/services/db'; + +@inject() +export default class DeleteLinkController { + constructor( + protected collectionsService: CollectionService, + protected linkService: LinkService + ) {} + + async render({ auth, inertia, request }: HttpContext) { + const linkId = request.qs()?.linkId; + if (!linkId) { + return this.collectionsService.redirectToDashboard(); + } + + const link = await this.linkService.getLinkById(linkId, auth.user!.id); + await link.load('collection'); + return inertia.render('links/delete', { link }); + } + + async execute({ request, auth }: HttpContext) { + const { params } = await request.validateUsing(deleteLinkValidator); + + const link = await this.linkService.getLinkById(params.id, auth.user!.id); + await this.linkService.deleteLink(params.id); + + return this.collectionsService.redirectToCollectionId(link.collectionId); + } + + async getTotalLinksCount() { + const totalCount = await db.from('links').count('* as total'); + return Number(totalCount[0].total); + } +} diff --git a/app/links/controllers/links_controller.ts b/app/links/controllers/links_controller.ts deleted file mode 100644 index 645554a..0000000 --- a/app/links/controllers/links_controller.ts +++ /dev/null @@ -1,129 +0,0 @@ -import CollectionsController from '#collections/controllers/show_collections_controller'; -import Link from '#links/models/link'; -import { createLinkValidator } from '#links/validators/create_link_validator'; -import { deleteLinkValidator } from '#links/validators/delete_link_validator'; -import { updateLinkFavoriteStatusValidator } from '#links/validators/update_favorite_link_validator'; -import { updateLinkValidator } from '#links/validators/update_link_validator'; -import { inject } from '@adonisjs/core'; -import { HttpContext } from '@adonisjs/core/http'; -import db from '@adonisjs/lucid/services/db'; - -@inject() -export default class LinksController { - constructor(protected collectionsController: CollectionsController) {} - - async showCreatePage({ auth, inertia }: HttpContext) { - const collections = - await this.collectionsController.getCollectionsByAuthorId(auth.user!.id); - return inertia.render('links/create', { collections }); - } - - async store({ auth, request, response }: HttpContext) { - const { collectionId, ...payload } = - await request.validateUsing(createLinkValidator); - - await this.collectionsController.getCollectionById( - collectionId, - auth.user!.id - ); - await Link.create({ - ...payload, - collectionId, - authorId: auth.user?.id!, - }); - return this.collectionsController.redirectToCollectionId( - response, - collectionId - ); - } - - async showEditPage({ auth, inertia, request, response }: HttpContext) { - const linkId = request.qs()?.linkId; - if (!linkId) { - return response.redirectToNamedRoute('dashboard'); - } - - const userId = auth.user!.id; - const collections = - await this.collectionsController.getCollectionsByAuthorId(userId); - const link = await this.getLinkById(linkId, userId); - - return inertia.render('links/edit', { collections, link }); - } - - async update({ request, auth, response }: HttpContext) { - const { params, ...payload } = - await request.validateUsing(updateLinkValidator); - - // Throw if invalid link id provided - await this.getLinkById(params.id, auth.user!.id); - - await Link.updateOrCreate( - { - id: params.id, - }, - payload - ); - - return response.redirectToNamedRoute('dashboard', { - qs: { collectionId: payload.collectionId }, - }); - } - - async toggleFavorite({ request, auth, response }: HttpContext) { - const { params, favorite } = await request.validateUsing( - updateLinkFavoriteStatusValidator - ); - - // Throw if invalid link id provided - await this.getLinkById(params.id, auth.user!.id); - - await Link.updateOrCreate( - { - id: params.id, - }, - { favorite } - ); - - return response.json({ status: 'ok' }); - } - - async showDeletePage({ auth, inertia, request, response }: HttpContext) { - const linkId = request.qs()?.linkId; - if (!linkId) { - return response.redirectToNamedRoute('dashboard'); - } - - const link = await this.getLinkById(linkId, auth.user!.id); - await link.load('collection'); - return inertia.render('links/delete', { link }); - } - - async delete({ request, auth, response }: HttpContext) { - const { params } = await request.validateUsing(deleteLinkValidator); - - const link = await this.getLinkById(params.id, auth.user!.id); - await link.delete(); - - return response.redirectToNamedRoute('dashboard', { - qs: { collectionId: link.id }, - }); - } - - async getTotalLinksCount() { - const totalCount = await db.from('links').count('* as total'); - return Number(totalCount[0].total); - } - - /** - * Get link by id. - * - * /!\ Only return private link (create by the current user) - */ - private async getLinkById(id: Link['id'], userId: Link['id']) { - return await Link.query() - .where('id', id) - .andWhere('author_id', userId) - .firstOrFail(); - } -} diff --git a/app/links/controllers/toggle_favorite_controller.ts b/app/links/controllers/toggle_favorite_controller.ts new file mode 100644 index 0000000..545c2be --- /dev/null +++ b/app/links/controllers/toggle_favorite_controller.ts @@ -0,0 +1,19 @@ +import { LinkService } from '#links/services/link_service'; +import { updateLinkFavoriteStatusValidator } from '#links/validators/update_favorite_link_validator'; +import { inject } from '@adonisjs/core'; +import { HttpContext } from '@adonisjs/core/http'; + +@inject() +export default class ToggleFavoriteController { + constructor(private linkService: LinkService) {} + + async toggleFavorite({ request, response }: HttpContext) { + const { params, favorite } = await request.validateUsing( + updateLinkFavoriteStatusValidator + ); + + await this.linkService.updateFavorite(params.id, favorite); + + return response.json({ status: 'ok' }); + } +} diff --git a/app/links/controllers/update_link_controller.ts b/app/links/controllers/update_link_controller.ts new file mode 100644 index 0000000..c516123 --- /dev/null +++ b/app/links/controllers/update_link_controller.ts @@ -0,0 +1,34 @@ +import { CollectionService } from '#collections/services/collection_service'; +import { LinkService } from '#links/services/link_service'; +import { updateLinkValidator } from '#links/validators/update_link_validator'; +import { inject } from '@adonisjs/core'; +import { HttpContext } from '@adonisjs/core/http'; + +@inject() +export default class UpdateLinkController { + constructor( + private linkService: LinkService, + private collectionsService: CollectionService + ) {} + + async render({ auth, inertia, request, response }: HttpContext) { + const linkId = request.qs()?.linkId; + if (!linkId) { + return response.redirectToNamedRoute('dashboard'); + } + + const collections = + await this.collectionsService.getCollectionsForAuthenticatedUser(); + const link = await this.linkService.getLinkById(linkId, auth.user!.id); + + return inertia.render('links/edit', { collections, link }); + } + + async execute({ request }: HttpContext) { + const { params, ...payload } = + await request.validateUsing(updateLinkValidator); + + await this.linkService.updateLink(params.id, payload); + return this.collectionsService.redirectToCollectionId(payload.collectionId); + } +} diff --git a/app/links/routes/links_routes.ts b/app/links/routes/links_routes.ts index f7c01fe..ed62cc1 100644 --- a/app/links/routes/links_routes.ts +++ b/app/links/routes/links_routes.ts @@ -1,6 +1,14 @@ import { middleware } from '#start/kernel'; import router from '@adonisjs/core/services/router'; -const LinksController = () => import('#links/controllers/links_controller'); + +const CreateLinkController = () => + import('#links/controllers/create_link_controller'); +const DeleteLinkController = () => + import('#links/controllers/delete_link_controller'); +const UpdateLinkController = () => + import('#links/controllers/update_link_controller'); +const ToggleFavoriteController = () => + import('#links/controllers/toggle_favorite_controller'); /** * Routes for authenticated users @@ -8,21 +16,21 @@ const LinksController = () => import('#links/controllers/links_controller'); router .group(() => { router - .get('/create', [LinksController, 'showCreatePage']) + .get('/create', [CreateLinkController, 'render']) .as('link.create-form'); - router.post('/', [LinksController, 'store']).as('link.create'); + router.post('/', [CreateLinkController, 'execute']).as('link.create'); - router.get('/edit', [LinksController, 'showEditPage']).as('link.edit-form'); - router.put('/:id', [LinksController, 'update']).as('link.edit'); + router.get('/edit', [UpdateLinkController, 'render']).as('link.edit-form'); + router.put('/:id', [UpdateLinkController, 'execute']).as('link.edit'); router - .put('/:id/favorite', [LinksController, 'toggleFavorite']) + .put('/:id/favorite', [ToggleFavoriteController, 'toggleFavorite']) .as('link.toggle-favorite'); router - .get('/delete', [LinksController, 'showDeletePage']) + .get('/delete', [DeleteLinkController, 'render']) .as('link.delete-form'); - router.delete('/:id', [LinksController, 'delete']).as('link.delete'); + router.delete('/:id', [DeleteLinkController, 'execute']).as('link.delete'); }) .middleware([middleware.auth()]) .prefix('/links'); diff --git a/app/links/services/link_service.ts b/app/links/services/link_service.ts new file mode 100644 index 0000000..27fffff --- /dev/null +++ b/app/links/services/link_service.ts @@ -0,0 +1,60 @@ +import Link from '#links/models/link'; +import { HttpContext } from '@adonisjs/core/http'; + +type CreateLinkPayload = { + name: string; + description?: string; + url: string; + favorite: boolean; + collectionId: number; +}; + +type UpdateLinkPayload = CreateLinkPayload; + +export class LinkService { + createLink(payload: CreateLinkPayload) { + const context = this.getAuthContext(); + return Link.create({ + ...payload, + authorId: context.auth.user!.id, + }); + } + + updateLink(id: number, payload: UpdateLinkPayload) { + const context = this.getAuthContext(); + return Link.query() + .where('id', id) + .andWhere('author_id', context.auth.user!.id) + .update(payload); + } + + deleteLink(id: number) { + const context = this.getAuthContext(); + return Link.query() + .where('id', id) + .andWhere('author_id', context.auth.user!.id) + .delete(); + } + + async getLinkById(id: Link['id'], userId: Link['id']) { + return await Link.query() + .where('id', id) + .andWhere('author_id', userId) + .firstOrFail(); + } + + updateFavorite(id: number, favorite: boolean) { + return Link.query() + .where('id', id) + .andWhere('author_id', this.getAuthContext().auth.user!.id) + .update({ favorite }); + } + + getAuthContext() { + const context = HttpContext.getOrFail(); + if (!context.auth.user || !context.auth.user.id) { + throw new Error('User not authenticated'); + } + return context; + } +}