Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2af7fdc
feat(categories): add createCategorySchema and updateCategorySchema
EdgarEldy Aug 26, 2026
b29c3f9
feat(categories): add CategoriesService with paginated, cached listing
EdgarEldy Aug 26, 2026
21f779f
feat(categories): add CategoriesController with Swagger annotations
EdgarEldy Aug 26, 2026
87b1028
feat(categories): declare the category CRUD routes
EdgarEldy Aug 26, 2026
9b9ea92
test(categories): cover CategoriesService's CRUD and the 409 business…
EdgarEldy Aug 26, 2026
e11f7c7
test(categories): cover the categories HTTP surface end to end
EdgarEldy Aug 26, 2026
3815c7a
refactor(categories): remove the temporary auth/role middleware smoke…
EdgarEldy Aug 26, 2026
4f0222e
fix(auth): migrate the middleware smoke test off the stub routes
EdgarEldy Aug 26, 2026
2fd8bbb
style(auth): update a stale placeholder URL in the middleware unit test
EdgarEldy Aug 26, 2026
a1797ce
chore(codegen): regenerate the controllers registry and schema snapshot
EdgarEldy Aug 26, 2026
b28a01d
fix(categories): reject non-numeric route ids with a clean 404
EdgarEldy Aug 26, 2026
a771456
refactor(categories): hoist the cache namespace and trim an existence…
EdgarEldy Aug 26, 2026
a144350
fix(categories): clear the categories cache when reseeding
EdgarEldy Aug 26, 2026
f2000f7
test(auth): add an isolated unit test for RoleMiddleware's role check
EdgarEldy Aug 26, 2026
2176440
test: add shared body() and uniqueName() test utilities
EdgarEldy Aug 26, 2026
38ee5a9
refactor(categories): use the shared body()/uniqueName() test utilities
EdgarEldy Aug 26, 2026
559dece
refactor(categories): use the shared uniqueName() test utility
EdgarEldy Aug 26, 2026
7c60d74
refactor(auth): use the shared body() test utility
EdgarEldy Aug 26, 2026
acc7543
refactor(auth): use the shared body() test utility
EdgarEldy Aug 26, 2026
1b8b6d4
chore(codegen): regenerate the Tuyau client registry and route manifest
EdgarEldy Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .adonisjs/client/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,36 @@ const routes = {
tokens: [{"old":"/api/v1/permissions/:id","type":0,"val":"api","end":""},{"old":"/api/v1/permissions/:id","type":0,"val":"v1","end":""},{"old":"/api/v1/permissions/:id","type":0,"val":"permissions","end":""},{"old":"/api/v1/permissions/:id","type":1,"val":"id","end":""}],
types: placeholder as Registry['permissions.destroy']['types'],
},
'categories.index': {
methods: ["GET","HEAD"],
pattern: '/api/v1/categories',
tokens: [{"old":"/api/v1/categories","type":0,"val":"api","end":""},{"old":"/api/v1/categories","type":0,"val":"v1","end":""},{"old":"/api/v1/categories","type":0,"val":"categories","end":""}],
types: placeholder as Registry['categories.index']['types'],
},
'categories.show': {
methods: ["GET","HEAD"],
pattern: '/api/v1/categories/:id',
tokens: [{"old":"/api/v1/categories/:id","type":0,"val":"api","end":""},{"old":"/api/v1/categories/:id","type":0,"val":"v1","end":""},{"old":"/api/v1/categories/:id","type":0,"val":"categories","end":""},{"old":"/api/v1/categories/:id","type":1,"val":"id","end":""}],
types: placeholder as Registry['categories.show']['types'],
},
'categories.store': {
methods: ["POST"],
pattern: '/api/v1/categories',
tokens: [{"old":"/api/v1/categories","type":0,"val":"api","end":""},{"old":"/api/v1/categories","type":0,"val":"v1","end":""},{"old":"/api/v1/categories","type":0,"val":"categories","end":""}],
types: placeholder as Registry['categories.store']['types'],
},
'categories.update': {
methods: ["PUT"],
pattern: '/api/v1/categories/:id',
tokens: [{"old":"/api/v1/categories/:id","type":0,"val":"api","end":""},{"old":"/api/v1/categories/:id","type":0,"val":"v1","end":""},{"old":"/api/v1/categories/:id","type":0,"val":"categories","end":""},{"old":"/api/v1/categories/:id","type":1,"val":"id","end":""}],
types: placeholder as Registry['categories.update']['types'],
},
'categories.destroy': {
methods: ["DELETE"],
pattern: '/api/v1/categories/:id',
tokens: [{"old":"/api/v1/categories/:id","type":0,"val":"api","end":""},{"old":"/api/v1/categories/:id","type":0,"val":"v1","end":""},{"old":"/api/v1/categories/:id","type":0,"val":"categories","end":""},{"old":"/api/v1/categories/:id","type":1,"val":"id","end":""}],
types: placeholder as Registry['categories.destroy']['types'],
},
} as const satisfies Record<string, AdonisEndpoint>

export { routes }
Expand Down
72 changes: 66 additions & 6 deletions .adonisjs/client/registry/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ export interface Registry {
body: {}
paramsTuple: []
params: {}
query: {}
query: ExtractQueryForGet<InferInput<(typeof import('#validators/pagination_validator').paginationSchema)>>
response: ExtractResponse<Awaited<ReturnType<import('#controllers/users_controller').default['index']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/users_controller').default['index']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/users_controller').default['index']>>> | { status: 422; response: { errors: SimpleError[] } }
}
}
'users.show': {
Expand Down Expand Up @@ -194,9 +194,9 @@ export interface Registry {
body: {}
paramsTuple: []
params: {}
query: {}
query: ExtractQueryForGet<InferInput<(typeof import('#validators/pagination_validator').paginationSchema)>>
response: ExtractResponse<Awaited<ReturnType<import('#controllers/roles_controller').default['index']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/roles_controller').default['index']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/roles_controller').default['index']>>> | { status: 422; response: { errors: SimpleError[] } }
}
}
'roles.show': {
Expand Down Expand Up @@ -278,9 +278,9 @@ export interface Registry {
body: {}
paramsTuple: []
params: {}
query: {}
query: ExtractQueryForGet<InferInput<(typeof import('#validators/pagination_validator').paginationSchema)>>
response: ExtractResponse<Awaited<ReturnType<import('#controllers/permissions_controller').default['index']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/permissions_controller').default['index']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/permissions_controller').default['index']>>> | { status: 422; response: { errors: SimpleError[] } }
}
}
'permissions.store': {
Expand Down Expand Up @@ -319,4 +319,64 @@ export interface Registry {
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/permissions_controller').default['destroy']>>>
}
}
'categories.index': {
methods: ["GET","HEAD"]
pattern: '/api/v1/categories'
types: {
body: {}
paramsTuple: []
params: {}
query: ExtractQueryForGet<InferInput<(typeof import('#validators/pagination_validator').paginationSchema)>>
response: ExtractResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['index']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['index']>>> | { status: 422; response: { errors: SimpleError[] } }
}
}
'categories.show': {
methods: ["GET","HEAD"]
pattern: '/api/v1/categories/:id'
types: {
body: {}
paramsTuple: [ParamValue]
params: { id: ParamValue }
query: {}
response: ExtractResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['show']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['show']>>>
}
}
'categories.store': {
methods: ["POST"]
pattern: '/api/v1/categories'
types: {
body: ExtractBody<InferInput<(typeof import('#validators/category_validator').createCategorySchema)>>
paramsTuple: []
params: {}
query: ExtractQuery<InferInput<(typeof import('#validators/category_validator').createCategorySchema)>>
response: ExtractResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['store']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['store']>>> | { status: 422; response: { errors: SimpleError[] } }
}
}
'categories.update': {
methods: ["PUT"]
pattern: '/api/v1/categories/:id'
types: {
body: ExtractBody<InferInput<(typeof import('#validators/category_validator').updateCategorySchema)>>
paramsTuple: [ParamValue]
params: { id: ParamValue }
query: ExtractQuery<InferInput<(typeof import('#validators/category_validator').updateCategorySchema)>>
response: ExtractResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['update']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['update']>>> | { status: 422; response: { errors: SimpleError[] } }
}
}
'categories.destroy': {
methods: ["DELETE"]
pattern: '/api/v1/categories/:id'
types: {
body: {}
paramsTuple: [ParamValue]
params: { id: ParamValue }
query: {}
response: ExtractResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['destroy']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/categories_controller').default['destroy']>>>
}
}
}
7 changes: 7 additions & 0 deletions .adonisjs/client/registry/tree.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,11 @@ export interface ApiDefinition {
update: typeof routes['permissions.update']
destroy: typeof routes['permissions.destroy']
}
categories: {
index: typeof routes['categories.index']
show: typeof routes['categories.show']
store: typeof routes['categories.store']
update: typeof routes['categories.update']
destroy: typeof routes['categories.destroy']
}
}
1 change: 1 addition & 0 deletions .adonisjs/server/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

export const controllers = {
Auth: () => import('#controllers/auth_controller'),
Categories: () => import('#controllers/categories_controller'),
Health: () => import('#controllers/health_controller'),
Permissions: () => import('#controllers/permissions_controller'),
Roles: () => import('#controllers/roles_controller'),
Expand Down
12 changes: 12 additions & 0 deletions .adonisjs/server/routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export type ScannedRoutes = {
'permissions.store': { paramsTuple?: []; params?: {} }
'permissions.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'permissions.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'categories.index': { paramsTuple?: []; params?: {} }
'categories.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'categories.store': { paramsTuple?: []; params?: {} }
'categories.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'categories.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
}
GET: {
'health.index': { paramsTuple?: []; params?: {} }
Expand All @@ -39,6 +44,8 @@ export type ScannedRoutes = {
'roles.index': { paramsTuple?: []; params?: {} }
'roles.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'permissions.index': { paramsTuple?: []; params?: {} }
'categories.index': { paramsTuple?: []; params?: {} }
'categories.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
}
HEAD: {
'health.index': { paramsTuple?: []; params?: {} }
Expand All @@ -48,6 +55,8 @@ export type ScannedRoutes = {
'roles.index': { paramsTuple?: []; params?: {} }
'roles.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'permissions.index': { paramsTuple?: []; params?: {} }
'categories.index': { paramsTuple?: []; params?: {} }
'categories.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
}
POST: {
'auth.register': { paramsTuple?: []; params?: {} }
Expand All @@ -61,18 +70,21 @@ export type ScannedRoutes = {
'roles.store': { paramsTuple?: []; params?: {} }
'roles.assign_permission': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'permissions.store': { paramsTuple?: []; params?: {} }
'categories.store': { paramsTuple?: []; params?: {} }
}
PUT: {
'users.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'roles.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'permissions.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'categories.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
}
DELETE: {
'users.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'users.revoke_role': { paramsTuple: [ParamValue,ParamValue]; params: {'id': ParamValue,'roleId': ParamValue} }
'roles.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'roles.revoke_permission': { paramsTuple: [ParamValue,ParamValue]; params: {'id': ParamValue,'permissionId': ParamValue} }
'permissions.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'categories.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
}
}
declare module '@adonisjs/core/types/http' {
Expand Down
94 changes: 94 additions & 0 deletions app/controllers/categories_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'

import { respond } from '#helpers/api_response'
import CategoriesService from '#services/categories_service'
import { createCategorySchema, updateCategorySchema } from '#validators/category_validator'
import { paginationSchema } from '#validators/pagination_validator'

/**
* Thin HTTP layer over CategoriesService. GET routes are public; the
* mutation routes (store, update, destroy) are declared behind
* `middleware.auth()` + `middleware.role({ roles: ['ADMIN'] })` in
* start/routes.ts, the same pattern RolesController and
* PermissionsController already use for their own mutation routes.
*/
@inject()
export default class CategoriesController {
constructor(private categoriesService: CategoriesService) {}

/**
* @index
* @summary List categories
* @description Returns a paginated list of categories. Public endpoint, cached by page and limit.
* @paramQuery page - Page number - @type(number)
* @paramQuery limit - Items per page, capped at 100 - @type(number)
* @responseBody 200 - <Category[]>.paginated()
*/
async index({ request, response }: HttpContext) {
const { page, limit } = await request.validateUsing(paginationSchema)
const result = await this.categoriesService.findAll(page ?? 1, limit ?? 10)
return response.ok(respond(result))
}

/**
* @show
* @summary Get a category
* @description Returns a single category by id. Public endpoint.
* @paramPath id - The category id - @type(number) @required
* @responseBody 200 - <Category>
* @responseBody 404 - Category not found
*/
async show({ params, response }: HttpContext) {
const category = await this.categoriesService.findOne(Number(params.id))
return response.ok(respond(category, 'Category detail'))
}

/**
* @store
* @summary Create a category
* @description Creates a new category. ADMIN only.
* @requestBody <createCategorySchema>
* @responseBody 201 - <Category>
* @responseBody 401 - Missing or invalid token
* @responseBody 403 - Caller is not an ADMIN
*/
async store({ request, response }: HttpContext) {
const data = await request.validateUsing(createCategorySchema)
const category = await this.categoriesService.create(data)
return response.created(respond(category, 'Category created'))
}

/**
* @update
* @summary Update a category
* @description Partially updates an existing category. ADMIN only.
* @paramPath id - The category id - @type(number) @required
* @requestBody <updateCategorySchema>
* @responseBody 200 - <Category>
* @responseBody 401 - Missing or invalid token
* @responseBody 403 - Caller is not an ADMIN
* @responseBody 404 - Category not found
*/
async update({ params, request, response }: HttpContext) {
const data = await request.validateUsing(updateCategorySchema)
const category = await this.categoriesService.update(Number(params.id), data)
return response.ok(respond(category, 'Category updated'))
}

/**
* @destroy
* @summary Delete a category
* @description Deletes a category. ADMIN only. Fails with 409 when the category still has products associated with it.
* @paramPath id - The category id - @type(number) @required
* @responseBody 200 - Category deleted
* @responseBody 401 - Missing or invalid token
* @responseBody 403 - Caller is not an ADMIN
* @responseBody 404 - Category not found
* @responseBody 409 - Category still has products associated with it
*/
async destroy({ params, response }: HttpContext) {
await this.categoriesService.remove(Number(params.id))
return response.ok(respond(null, 'Category deleted'))
}
}
Loading
Loading