// @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;