# 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`.