40227-vm/backend/docs/search.md
2026-06-10 18:27:19 +02:00

4.8 KiB

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/).

  • file.md (the other backend infrastructure slice).
  • Architecture contract: backend/docs/backend-architecture.md.