89 lines
4.8 KiB
Markdown
89 lines
4.8 KiB
Markdown
# 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[<table>]` 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_<TABLE>')`: the user must have a matching
|
|
custom permission or an app-role permission named `READ_<TABLE>` (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 = <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); `students`
|
|
(student_number, first_name, last_name, email, phone, address); `guardians` (full_name, phone,
|
|
email, address); `staff` (employee_number, job_title); `classes` (name, section); `timetables`
|
|
(name); `timetable_periods` (room); `attendance_sessions` (notes); `attendance_records` (remarks);
|
|
`fee_plans` (name, notes); `invoices` (invoice_number, notes); `payments` (receipt_number,
|
|
reference_code, notes); `assessments` (name, instructions); `assessment_results` (remarks);
|
|
`messages` (subject, body); `message_recipients` (recipient_label, destination); `documents`
|
|
(entity_reference, name, notes).
|
|
- Numeric columns (`COLUMNS_INT`, cast to varchar before matching): `grades` (sort_order); `classes`
|
|
(capacity); `attendance_records` (minutes_late); `fee_plans` (total_amount); `invoices` (subtotal,
|
|
discount_amount, tax_amount, total_amount, balance_due); `payments` (amount); `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_<TABLE>` 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`.
|