diff --git a/.changeset/refactor-stats-api-chained-pattern.md b/.changeset/refactor-stats-api-chained-pattern.md new file mode 100644 index 0000000000000..c55c8437012bc --- /dev/null +++ b/.changeset/refactor-stats-api-chained-pattern.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Migrated `statistics`, `statistics.list`, and `statistics.telemetry` REST API endpoints from legacy `addRoute` pattern to the new chained `.get()`/`.post()` API pattern with typed response schemas and AJV query parameter validation. diff --git a/apps/meteor/app/api/server/v1/stats.ts b/apps/meteor/app/api/server/v1/stats.ts index 27cea2c310574..4721e2c345d9d 100644 --- a/apps/meteor/app/api/server/v1/stats.ts +++ b/apps/meteor/app/api/server/v1/stats.ts @@ -1,62 +1,157 @@ +import type { TelemetryEvents, TelemetryMap } from '@rocket.chat/core-services'; +import type { IStats } from '@rocket.chat/core-typings'; +import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings'; + import { getStatistics, getLastStatistics } from '../../../statistics/server'; import telemetryEvent from '../../../statistics/server/lib/telemetryEvents'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; -API.v1.addRoute( +API.v1.get( 'statistics', - { authRequired: true }, { - async get() { - const { refresh = 'false' } = this.queryParams; - - return API.v1.success( - await getLastStatistics({ - userId: this.userId, - refresh: refresh === 'true', - }), - ); + authRequired: true, + query: ajv.compile<{ refresh?: 'true' | 'false' }>({ + type: 'object', + properties: { + refresh: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, }, }, + async function action() { + const { refresh = 'false' } = this.queryParams; + + return API.v1.success( + await getLastStatistics({ + userId: this.userId, + refresh: refresh === 'true', + }), + ); + }, ); -API.v1.addRoute( +API.v1.get( 'statistics.list', - { authRequired: true }, { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - - return API.v1.success( - await getStatistics({ - userId: this.userId, - query, - pagination: { - offset, - count, - sort, - fields, + authRequired: true, + query: ajv.compile<{ fields?: string; count?: number; offset?: number; sort?: string; query?: string }>({ + type: 'object', + properties: { + fields: { type: 'string', nullable: true }, + count: { type: 'number', nullable: true }, + offset: { type: 'number', nullable: true }, + sort: { type: 'string', nullable: true }, + query: { type: 'string', nullable: true }, + }, + required: [], + additionalProperties: false, + }), + response: { + 200: ajv.compile<{ + statistics: unknown[]; + count: number; + offset: number; + total: number; + }>({ + type: 'object', + properties: { + statistics: { type: 'array' }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { + type: 'boolean', + enum: [true], }, - }), - ); + }, + required: ['statistics', 'count', 'offset', 'total', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, }, }, + async function action() { + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort, fields, query } = await this.parseJsonQuery(); + + return API.v1.success( + await getStatistics({ + userId: this.userId, + query, + pagination: { + offset, + count, + sort, + fields, + }, + }), + ); + }, ); -API.v1.addRoute( +API.v1.post( 'statistics.telemetry', - { authRequired: true }, { - post() { - const events = this.bodyParams; + authRequired: true, + body: ajv.compile<{ params: { eventName: string; [key: string]: unknown }[] }>({ + type: 'object', + properties: { + params: { + type: 'array', + items: { + type: 'object', + properties: { + eventName: { type: 'string' }, + }, + required: ['eventName'], + }, + }, + }, + required: ['params'], + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + function action() { + const events = this.bodyParams; - events?.params?.forEach((event) => { - const { eventName, ...params } = event; - void telemetryEvent.call(eventName, params); - }); + events.params.forEach((event) => { + const { eventName, ...params } = event; + void telemetryEvent.call(eventName as TelemetryEvents, params as TelemetryMap[TelemetryEvents]); + }); - return API.v1.success(); - }, + return API.v1.success(); }, );