# Search Backend ## Purpose The search slice provides a single cross-entity text search endpoint. It scans a fixed set of tables for a query string across predefined text and numeric columns, applies per-table permission and tenant filtering, and returns the matching records annotated with which columns matched. ## Slice Files (by layer) - Route: `src/routes/search.ts` (thin wiring; `POST /`, guarded by `permissions.checkCrudPermissions('search')`). - Controller: `src/api/controllers/search.controller.ts` (custom — `search`). - Service (BLL): `src/services/search.ts` (`SearchService.search`; queries run through `db.sequelize.models[tableName]` inside the service, no separate `db/api/search.ts`). - Repository (DAL): queries via `db.sequelize.models[]` in the service. - Shared used: `api/http/request.ts` (`wrapAsync`), `middlewares/check-permissions.ts` (`permissions.checkCrudPermissions`), `shared/logger.ts`, `shared/errors/validation.ts` (`ValidationError`), `db/models` (`db`). ## API - `POST /api/search` -> `200` JSON array of matched records. Requires authentication (the route is mounted with the `authenticated` middleware in `src/index.ts`) and the `search` CRUD permission (`checkCrudPermissions('search')`). - Request body: `{ searchQuery, organizationId }`. - Missing/empty `searchQuery` -> `400` `{ error: 'Please enter a search query' }`. - On service error -> `500` `{ error: 'Internal Server Error' }` (logged via `logger.error`). - Success -> `200` with the array returned by `SearchService.search`. ## Access Rules - The route requires the `search` CRUD permission via `checkCrudPermissions('search')`. - `globalAccess` is read from `req.currentUser.app_role.globalAccess` (default `false`). - Per table, the service calls `checkPermissions('READ_
')`: the user must have a matching custom permission or an app-role permission named `READ_
` (uppercased table name); tables the user cannot read are skipped. A missing `currentUser` raises `ValidationError('auth.unauthorized')`; a present user with no `app_role` raises `ValidationError('auth.forbidden')`. ## Tenant Scope For each table other than `organizations`, when `globalAccess` is false and an `organizationId` is supplied, the query adds `organizationId = ` to the where clause. With `globalAccess` true, or for the `organizations` table, no organization filter is applied. ## Data Contract The searched tables and columns are fixed in the service: - Text columns (`TABLE_COLUMNS`): `users` (firstName, lastName, phoneNumber, email); `organizations` (name); `campuses` (name, code, address, phone, email); `academic_years` (name); `grades` (name, code, description); `subjects` (name, code, description); `staff` (employee_number, job_title); `classes` (name, section); `timetables` (name); `timetable_periods` (room); `attendance_sessions` (notes); `attendance_records` (remarks); `assessments` (name, instructions); `assessment_results` (remarks); `messages` (subject, body); `message_recipients` (recipient_label, destination). - Numeric columns (`COLUMNS_INT`, cast to varchar before matching): `grades` (sort_order); `classes` (capacity); `attendance_records` (minutes_late); `assessments` (max_score); `assessment_results` (score). Text columns match with `Op.iLike '%searchQuery%'`; numeric columns are cast to `varchar` and matched the same way; conditions are combined with `Op.or`. Each returned record is the plain row limited to the searched columns plus `id`, with two added fields: `matchAttribute` (the list of columns whose value contained the lowercased query) and `tableName`. Results from all permitted tables are concatenated into one flat array. ## Behavior / Notes - The service iterates tables in `TABLE_COLUMNS` order and skips any table the user lacks `READ_
` permission for. - Records are fetched with `attributes` restricted to the searched text columns, `id`, and the numeric columns; `matchAttribute` is recomputed in JS against the lowercased query. - `SearchService.search` also independently raises `ValidationError('iam.errors.searchQueryRequired')` if `searchQuery` is falsy, though the controller already rejects empty queries with `400`. ## Tests None yet (no `search` unit/e2e test in `src/`). ## Related - `file.md` (the other backend infrastructure slice). - Architecture contract: `backend/docs/backend-architecture.md`.