Source: Router/persons.js

// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from '../types.js'
 */

/**
 * Persons Router
 *
 * Provides listing, filtering and temporal-change queries for person-like
 * objects (person, guest, extern, family, job, entry, eventJob) relative to
 * groups or direct UID sets.
 *
 * All routes require the caller to be authenticated.  Most GET routes in
 * addition enforce visibility through the `checkVisible` middleware which
 * verifies that the requesting user has at least `visible` access to the
 * target group.
 *
 * @swagger
 * components:
 *   schemas:
 *     PersonType:
 *       type: string
 *       enum: [person, guest, extern, family, job, entry, eventJob]
 *       description: |
 *         Allowed object types that can be returned by the persons router.
 *         - **person** – full club member
 *         - **guest** – temporary / guest member
 *         - **extern** – external person (trainer, referee, …)
 *         - **family** – family object (fee-discount grouping)
 *         - **job** – function / role
 *         - **entry** – internal label for persons pending approval
 *         - **eventJob** – event-specific role
 *
 *     PersonListItem:
 *       type: object
 *       description: Represents one person-like object in a listing result.
 *       properties:
 *         UID:
 *           type: string
 *           format: uuid
 *           description: Unique identifier of the object row in ObjectBase.
 *         Type:
 *           $ref: '#/components/schemas/PersonType'
 *         UIDBelongsTo:
 *           type: string
 *           format: uuid
 *           description: UID of the parent person (Member table row).
 *         Title:
 *           type: string
 *           nullable: true
 *           description: Display title / role label for the object.
 *         Display:
 *           type: string
 *           description: Human-readable display name from the Member table.
 *         SortName:
 *           type: string
 *           description: Sort key used for ordering.
 *         UIDgroup:
 *           type: string
 *           format: uuid
 *           nullable: true
 *           description: Primary group UID the object is linked to via memberA link.
 *         pGroup:
 *           type: string
 *           nullable: true
 *           description: Concatenated title + display name of the primary group.
 *         hierarchie:
 *           type: integer
 *           nullable: true
 *           description: Hierarchy level within the organisation.
 *         stage:
 *           type: integer
 *           nullable: true
 *           description: Stage / level (e.g. age group stage).
 *         gender:
 *           type: string
 *           nullable: true
 *           description: Gender field value.
 *         dindex:
 *           type: integer
 *           nullable: true
 *           description: Custom display index for manual ordering.
 *         visibility:
 *           type: string
 *           enum: [visible, changeable]
 *           description: >
 *             Visibility permission held by the requesting user for this object.
 *             Only present in GET /:UID responses.
 *
 *     DataFieldSpec:
 *       type: object
 *       description: >
 *         Specification for projecting a single JSON field out of Member.Data.
 *         Pass an array of these as the `Data` query parameter (JSON-encoded).
 *       required: [path, alias]
 *       properties:
 *         path:
 *           type: string
 *           description: MySQL JSON path expression, e.g. `$.firstName`.
 *         alias:
 *           type: string
 *           description: Column alias in the result set.
 *         query:
 *           type: boolean
 *           default: false
 *           description: >
 *             When `true` uses `JSON_QUERY` (returns objects/arrays);
 *             when `false` (default) uses `JSON_VALUE` (returns scalars).
 *
 *     PersonListResponse:
 *       type: object
 *       properties:
 *         success:
 *           type: boolean
 *         result:
 *           type: array
 *           items:
 *             $ref: '#/components/schemas/PersonListItem'
 *
 *     IsMemberResponse:
 *       type: object
 *       properties:
 *         success:
 *           type: boolean
 *         result:
 *           type: array
 *           items:
 *             type: object
 *             properties:
 *               UID:
 *                 type: string
 *                 format: uuid
 *               Type:
 *                 $ref: '#/components/schemas/PersonType'
 *
 * tags:
 *   - name: Persons
 *     description: >
 *       Person-like object listing, bulk lookup and temporal membership change
 *       queries.  Visibility is enforced server-side based on the authenticated
 *       user's Visible entries.
 */

import express from 'express';
import { checkVisible } from '../utils/authChecks.js';
import * as personsController from './persons/controller.js';

// Re-export service helpers that other router modules depend on
export { getListVisibleSql, getListing, getType } from './persons/service.js';

/** @type {express.Express} */
const api = express();

/**
 * @swagger
 * /api/persons/{UID}:
 *   get:
 *     summary: List persons belonging to a group (paginated)
 *     description: |
 *       Returns a paginated subset of person-like objects that are linked to
 *       the group/object identified by `UID`.  Pagination is activated by
 *       supplying the `__page` query parameter.  Without `__page` the full
 *       result set is returned by the second handler registered on this path.
 *
 *       Visibility is enforced: only objects the requesting user is allowed to
 *       see are included.
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UID of the group or object whose members should be listed.
 *       - in: query
 *         name: __page
 *         required: true
 *         schema:
 *           type: integer
 *           minimum: 1
 *         description: 1-based page number.  Required to trigger pagination.
 *       - in: query
 *         name: type
 *         schema:
 *           oneOf:
 *             - type: string
 *             - type: array
 *               items:
 *                 $ref: '#/components/schemas/PersonType'
 *         description: >
 *           JSON-encoded array (or comma-separated string) of object types to
 *           include.  Defaults to `["person"]`.  Only values from the
 *           `PersonType` enum are accepted; others are silently dropped.
 *       - in: query
 *         name: timestamp
 *         schema:
 *           type: integer
 *         description: Unix timestamp – retrieve a historical snapshot of the data.
 *       - in: query
 *         name: since
 *         schema:
 *           type: integer
 *         description: >
 *           Unix timestamp – return only members linked (validFrom) after this
 *           point and whose link is still active (validUntil > NOW()).
 *       - in: query
 *         name: hierarchie
 *         schema:
 *           type: integer
 *         description: Filter by hierarchy level.
 *       - in: query
 *         name: stage
 *         schema:
 *           type: integer
 *         description: Filter by stage / level.
 *       - in: query
 *         name: gender
 *         schema:
 *           type: string
 *         description: Filter by gender value.
 *       - in: query
 *         name: Data
 *         schema:
 *           oneOf:
 *             - type: string
 *               enum: [all]
 *             - type: string
 *               description: JSON-encoded array of DataFieldSpec objects.
 *         description: >
 *           `"all"` to include the full `Member.Data` JSON column, or a
 *           JSON-encoded array of `DataFieldSpec` objects to project individual
 *           JSON path values.
 *       - in: query
 *         name: ExtraData
 *         schema:
 *           type: boolean
 *         description: Include `ObjectBase.Data` as `ExtraData` column.
 *       - in: query
 *         name: dataFilter
 *         schema:
 *           type: string
 *         description: >
 *           JSON-encoded filter expression evaluated against each row's
 *           `Member.Data` by `@commtool/object-filter`.  Rows that do not
 *           match are excluded from the result.
 *       - in: query
 *         name: grouped
 *         schema:
 *           type: boolean
 *         description: >
 *           When truthy, collapse multiple object rows per person into a single
 *           row, preferring `person` type over `guest` / `entry`.
 *       - in: query
 *         name: groupBanner
 *         schema:
 *           type: boolean
 *         description: Include `$.banner` from the primary group's Data as `groupBanner`.
 *       - in: query
 *         name: siblings
 *         schema:
 *           type: boolean
 *         description: >
 *           **Deprecated / obsolete.**  When set, returns sibling objects of
 *           the same parent instead of direct group members.
 *     responses:
 *       200:
 *         description: Paginated list of person objects.
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 result:
 *                   type: array
 *                   items:
 *                     $ref: '#/components/schemas/PersonListItem'
 *                 total:
 *                   type: integer
 *                   description: Total number of records (before pagination).
 *                 page:
 *                   type: integer
 *                 pageSize:
 *                   type: integer
 */
// @ts-ignore
api.get('/:UID', checkVisible, personsController.getListingPaginatedController);

/**
 * @swagger
 * /api/persons/{UID}:
 *   get:
 *     summary: List all persons belonging to a group
 *     description: |
 *       Returns the complete (non-paginated) list of person-like objects linked
 *       to the group/object identified by `UID`.
 *
 *       This handler is invoked when `__page` is **not** present in the query
 *       string (the paginated handler with the same path runs first and calls
 *       `next()` when `__page` is absent).
 *
 *       Supports all the same filtering and projection query parameters as the
 *       paginated variant.
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UID of the target group / object.
 *       - in: query
 *         name: type
 *         schema:
 *           type: string
 *         description: JSON-encoded array of PersonType values. Defaults to `["person"]`.
 *       - in: query
 *         name: timestamp
 *         schema:
 *           type: integer
 *         description: Unix timestamp for point-in-time query.
 *       - in: query
 *         name: since
 *         schema:
 *           type: integer
 *         description: Return only members linked after this Unix timestamp.
 *       - in: query
 *         name: hierarchie
 *         schema:
 *           type: integer
 *       - in: query
 *         name: stage
 *         schema:
 *           type: integer
 *       - in: query
 *         name: gender
 *         schema:
 *           type: string
 *       - in: query
 *         name: Data
 *         schema:
 *           type: string
 *         description: "`all"` or JSON array of DataFieldSpec.
 *       - in: query
 *         name: ExtraData
 *         schema:
 *           type: boolean
 *       - in: query
 *         name: dataFilter
 *         schema:
 *           type: string
 *         description: JSON filter expression for Member.Data.
 *       - in: query
 *         name: grouped
 *         schema:
 *           type: boolean
 *       - in: query
 *         name: groupBanner
 *         schema:
 *           type: boolean
 *     responses:
 *       200:
 *         description: Full list of person objects.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/PersonListResponse'
 */
// @ts-ignore
api.get('/:UID', checkVisible, personsController.getListingController);

/**
 * @swagger
 * /api/persons:
 *   post:
 *     summary: Fetch persons by explicit UID list
 *     description: |
 *       Returns person-like objects whose UIDs are provided in the request body
 *       as a JSON array of UUID strings.  Only objects visible to the
 *       authenticated user are returned.
 *
 *       Useful when the client already knows the specific UIDs it wants to
 *       retrieve (e.g. after a search or filter operation).
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: query
 *         name: type
 *         schema:
 *           type: string
 *         description: JSON-encoded array of PersonType values. Defaults to `["person"]`.
 *       - in: query
 *         name: timestamp
 *         schema:
 *           type: integer
 *         description: Unix timestamp for point-in-time query.
 *       - in: query
 *         name: since
 *         schema:
 *           type: integer
 *         description: History range start (Unix timestamp).
 *       - in: query
 *         name: hierarchie
 *         schema:
 *           type: integer
 *       - in: query
 *         name: stage
 *         schema:
 *           type: integer
 *       - in: query
 *         name: gender
 *         schema:
 *           type: string
 *       - in: query
 *         name: Data
 *         schema:
 *           type: string
 *         description: "`all"` or JSON array of DataFieldSpec.
 *       - in: query
 *         name: ExtraData
 *         schema:
 *           type: boolean
 *       - in: query
 *         name: dataFilter
 *         schema:
 *           type: string
 *       - in: query
 *         name: groupBanner
 *         schema:
 *           type: boolean
 *     requestBody:
 *       required: true
 *       description: JSON array of UUID strings identifying the objects to fetch.
 *       content:
 *         application/json:
 *           schema:
 *             type: array
 *             items:
 *               type: string
 *               format: uuid
 *             example:
 *               - "550e8400-e29b-41d4-a716-446655440000"
 *               - "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
 *     responses:
 *       200:
 *         description: Persons matching the supplied UIDs.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/PersonListResponse'
 *       200:
 *         description: Empty result when no valid UIDs were supplied.
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 result:
 *                   type: array
 *                   items: {}
 *                 message:
 *                   type: string
 *                   example: "no valid UID array supplied in body"
 */
// @ts-ignore
api.post('/', personsController.getPersonsByUIDsController);

/**
 * @swagger
 * /api/persons/groups:
 *   post:
 *     summary: Fetch persons belonging to one or more groups
 *     description: |
 *       Accepts a JSON array of group UIDs in the request body and returns all
 *       person-like objects that hold an active `member` or `memberA` link to
 *       **any** of those groups.
 *
 *       Only objects visible to the requesting user are included.
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: query
 *         name: type
 *         schema:
 *           type: string
 *         description: JSON-encoded array of PersonType values.
 *       - in: query
 *         name: timestamp
 *         schema:
 *           type: integer
 *         description: Unix timestamp for point-in-time query.
 *       - in: query
 *         name: hierarchie
 *         schema:
 *           type: integer
 *       - in: query
 *         name: stage
 *         schema:
 *           type: integer
 *       - in: query
 *         name: gender
 *         schema:
 *           type: string
 *       - in: query
 *         name: Data
 *         schema:
 *           type: string
 *         description: "`all"` or JSON array of DataFieldSpec.
 *       - in: query
 *         name: ExtraData
 *         schema:
 *           type: boolean
 *       - in: query
 *         name: dataFilter
 *         schema:
 *           type: string
 *       - in: query
 *         name: groupBanner
 *         schema:
 *           type: boolean
 *     requestBody:
 *       required: true
 *       description: JSON array of group UUID strings.
 *       content:
 *         application/json:
 *           schema:
 *             type: array
 *             items:
 *               type: string
 *               format: uuid
 *     responses:
 *       200:
 *         description: Persons belonging to the specified groups.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/PersonListResponse'
 */
// @ts-ignore
api.post('/groups/', personsController.getPersonsByGroupsController);

/**
 * @swagger
 * /api/persons/added/{UID}/{timestamp}:
 *   get:
 *     summary: Persons added to a group after a timestamp
 *     description: |
 *       Returns person-like objects that did **not** have an active membership
 *       link to the group at `timestamp` but do have one now (i.e. they were
 *       added after `timestamp`).
 *
 *       Uses `FOR SYSTEM_TIME` temporal tables internally.
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: Group UID.
 *       - in: path
 *         name: timestamp
 *         required: true
 *         schema:
 *           type: string
 *           description: >
 *             Reference point in time.  Interpreted as a MySQL-compatible
 *             timestamp string (e.g. `"2024-01-15 12:00:00"`).
 *       - in: query
 *         name: type
 *         schema:
 *           type: string
 *         description: JSON-encoded array of PersonType values.
 *     responses:
 *       200:
 *         description: Persons added since `timestamp`.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/PersonListResponse'
 */
// @ts-ignore
api.get('/added/:UID/:timestamp', checkVisible, personsController.getAddedPersonsController);

/**
 * @swagger
 * /api/persons/removed/{UID}/{timestamp}:
 *   get:
 *     summary: Persons removed from a group after a timestamp
 *     description: |
 *       Returns person-like objects that **had** an active membership link to
 *       the group at `timestamp` but no longer have one (i.e. they were
 *       removed after `timestamp`).
 *
 *       Uses `FOR SYSTEM_TIME` temporal queries.
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: Group UID.
 *       - in: path
 *         name: timestamp
 *         required: true
 *         schema:
 *           type: string
 *       - in: query
 *         name: type
 *         schema:
 *           type: string
 *         description: JSON-encoded array of PersonType values.
 *     responses:
 *       200:
 *         description: Persons removed since `timestamp`.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/PersonListResponse'
 */
// @ts-ignore
api.get('/removed/:UID/:timestamp', checkVisible, personsController.getRemovedPersonsController);

/**
 * @swagger
 * /api/persons/entered/{UID}/{timestamp}:
 *   get:
 *     summary: Persons who became full members (extern → person) after a timestamp
 *     description: |
 *       Returns persons who were `extern` at `timestamp` (or not yet linked to
 *       the group) and are now `person` type members of the group.
 *
 *       This captures the "entry" lifecycle event, i.e. people who officially
 *       joined the club / became full members.
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: Group UID.
 *       - in: path
 *         name: timestamp
 *         required: true
 *         schema:
 *           type: string
 *     responses:
 *       200:
 *         description: Persons who entered (became member) since `timestamp`.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/PersonListResponse'
 */
// @ts-ignore
api.get('/entered/:UID/:timestamp', checkVisible, personsController.getEnteredPersonsController);

/**
 * @swagger
 * /api/persons/exited/{UID}/{timestamp}:
 *   get:
 *     summary: Persons who left (person → extern) after a timestamp
 *     description: |
 *       Returns persons who were `person` type (full members) at `timestamp`
 *       and whose type is now `extern`, i.e. they left the club.
 *
 *       Uses a historical snapshot of `ObjectBase` at `timestamp` joined
 *       against the current state.
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: Group UID.
 *       - in: path
 *         name: timestamp
 *         required: true
 *         schema:
 *           type: string
 *     responses:
 *       200:
 *         description: Persons who exited since `timestamp`.
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 result:
 *                   type: array
 *                   items:
 *                     type: object
 *                     properties:
 *                       UID:
 *                         type: string
 *                         format: uuid
 *                       Type:
 *                         $ref: '#/components/schemas/PersonType'
 *                       UIDBelongsTo:
 *                         type: string
 *                         format: uuid
 *                       Title:
 *                         type: string
 *                         nullable: true
 *                       Display:
 *                         type: string
 *                       SortName:
 *                         type: string
 */
// @ts-ignore
api.get('/exited/:UID/:timestamp', checkVisible, personsController.getExitedPersonsController);

/**
 * @swagger
 * /api/persons/isMember/{UID}/{person}:
 *   get:
 *     summary: Check whether a person is a member of a group
 *     description: |
 *       Returns any active `member` or `memberA` link row that connects the
 *       `person` object to the group identified by `UID`.
 *
 *       An empty `result` array means the person is **not** a current member.
 *       A non-empty array confirms membership and exposes the linked object's
 *       UID and Type.
 *
 *       No visibility middleware is applied – the check is intentionally
 *       accessible to any authenticated user since it only reveals membership
 *       status (not personal data).
 *     tags:
 *       - Persons
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UID of the group to check membership in.
 *       - in: path
 *         name: person
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UID of the person to check.
 *     responses:
 *       200:
 *         description: >
 *           Membership check result.  `result` is an empty array when not a
 *           member, or contains the linked object(s) when the person is a member.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/IsMemberResponse'
 */
// @ts-ignore
api.get('/isMember/:UID/:person', personsController.checkIsMemberController);


export default api;