@tmlmobilidade/controllers 20260817.1945.33 → 20260828.1636.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +2 -0
  2. package/dist/exporter/exporter.d.ts +2 -2
  3. package/dist/exporter/exporter.js +7 -2
  4. package/dist/index.d.ts +0 -2
  5. package/dist/index.js +0 -2
  6. package/package.json +6 -4
  7. package/dist/agencies/agencies.d.ts +0 -27
  8. package/dist/agencies/agencies.js +0 -89
  9. package/dist/agencies/index.d.ts +0 -1
  10. package/dist/agencies/index.js +0 -1
  11. package/dist/operation/hashed-trips/hashed-trips.d.ts +0 -12
  12. package/dist/operation/hashed-trips/hashed-trips.js +0 -77
  13. package/dist/operation/hashed-trips/index.d.ts +0 -1
  14. package/dist/operation/hashed-trips/index.js +0 -1
  15. package/dist/operation/index.d.ts +0 -4
  16. package/dist/operation/index.js +0 -4
  17. package/dist/operation/lines/batch.d.ts +0 -2
  18. package/dist/operation/lines/batch.js +0 -111
  19. package/dist/operation/lines/controller.d.ts +0 -12
  20. package/dist/operation/lines/controller.js +0 -49
  21. package/dist/operation/lines/index.d.ts +0 -2
  22. package/dist/operation/lines/index.js +0 -2
  23. package/dist/operation/rides/index.d.ts +0 -2
  24. package/dist/operation/rides/index.js +0 -2
  25. package/dist/operation/rides/rides.d.ts +0 -31
  26. package/dist/operation/rides/rides.js +0 -191
  27. package/dist/operation/rides/watch.d.ts +0 -56
  28. package/dist/operation/rides/watch.js +0 -113
  29. package/dist/operation/stops/batch.d.ts +0 -2
  30. package/dist/operation/stops/batch.js +0 -112
  31. package/dist/operation/stops/controller.d.ts +0 -12
  32. package/dist/operation/stops/controller.js +0 -49
  33. package/dist/operation/stops/index.d.ts +0 -2
  34. package/dist/operation/stops/index.js +0 -2
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
1
  ## @tmlmobilidade/controllers
2
2
 
3
3
  These are shared controllers to retrieve and prepare common data across apps.
4
+
5
+ -
@@ -1,5 +1,5 @@
1
- import { type FastifyReply, type FastifyRequest } from '@tmlmobilidade/fastify';
2
- import { type CreateFileExportDto, type FileExport } from '@tmlmobilidade/types';
1
+ import { type FastifyReply, type FastifyRequest } from '@tmlmobilidade/go-clients-fastify';
2
+ import { type CreateFileExportDto, type FileExport } from '@tmlmobilidade/go-types-downloads';
3
3
  export declare class ExporterSharedController {
4
4
  /**
5
5
  * Returns an Agency by ID.
@@ -13,7 +13,12 @@ export class ExporterSharedController {
13
13
  */
14
14
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
15
  static async create(request, reply) {
16
- const fileExportData = await goDb.core.exports.insertOne({ ...request.body, created_by: request.me._id, updated_by: request.me._id });
16
+ const fileExportData = await goDb.core.exports.insertOne({
17
+ ...request.body,
18
+ created_by: request.me._id,
19
+ processing_status: 'waiting',
20
+ updated_by: request.me._id,
21
+ });
17
22
  return reply.send({ data: fileExportData, error: null, statusCode: HTTP_STATUS.CREATED });
18
23
  }
19
24
  /**
@@ -59,7 +64,7 @@ export class ExporterSharedController {
59
64
  const filters = {
60
65
  created_by: request.me._id,
61
66
  };
62
- const allFileExport = await goDb.core.exports.findMany(filters, { sort: { created_at: 1 } });
67
+ const allFileExport = await goDb.core.exports.findMany(filters);
63
68
  return reply.send({ data: allFileExport, error: null, statusCode: HTTP_STATUS.OK });
64
69
  }
65
70
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1 @@
1
- export * from './agencies/index.js';
2
1
  export * from './exporter/index.js';
3
- export * from './operation/index.js';
package/dist/index.js CHANGED
@@ -1,3 +1 @@
1
- export * from './agencies/index.js';
2
1
  export * from './exporter/index.js';
3
- export * from './operation/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/controllers",
3
- "version": "20260817.1945.33",
3
+ "version": "20260828.1636.54",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"
@@ -37,18 +37,20 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@tmlmobilidade/consts": "*",
40
- "@tmlmobilidade/fastify": "*",
40
+ "@tmlmobilidade/go-clients-fastify": "*",
41
41
  "@tmlmobilidade/go-clients-mongo": "*",
42
42
  "@tmlmobilidade/go-interfaces-godb": "*",
43
43
  "@tmlmobilidade/go-providers-storage": "*",
44
+ "@tmlmobilidade/go-types-core": "*",
45
+ "@tmlmobilidade/go-types-downloads": "*",
46
+ "@tmlmobilidade/go-utils-dates": "*",
44
47
  "@tmlmobilidade/interfaces": "*",
45
48
  "@tmlmobilidade/logger": "*",
46
49
  "@tmlmobilidade/normalizers": "*",
47
50
  "@tmlmobilidade/utils": "*"
48
51
  },
49
52
  "devDependencies": {
50
- "@tmlmobilidade/tsconfig": "*",
51
- "@tmlmobilidade/types": "*",
53
+ "@tmlmobilidade/go-utils-tsconfig": "*",
52
54
  "@types/node": "26.1.2",
53
55
  "@types/ws": "8.18.1",
54
56
  "resolve-tspaths": "0.8.23",
@@ -1,27 +0,0 @@
1
- import { type FastifyReply, type FastifyRequest } from '@tmlmobilidade/fastify';
2
- import { type ActionsOf, type Agency, type Permission } from '@tmlmobilidade/types';
3
- interface GetAllAgenciesQuery {
4
- actions?: string | string[];
5
- scope?: Permission['scope'];
6
- }
7
- export declare class AgenciesSharedController {
8
- /**
9
- * Returns all Agencies sorted by ID.
10
- * @param request The request object
11
- * @param reply The reply object
12
- */
13
- static getAll(request: FastifyRequest<{
14
- Querystring: GetAllAgenciesQuery;
15
- }>, reply: FastifyReply<Agency[]>): Promise<never>;
16
- /**
17
- * Returns an Agency by ID.
18
- * @param request The request object
19
- * @param reply The reply object
20
- */
21
- static getById<S extends Permission['scope']>(request: FastifyRequest<{
22
- Params: {
23
- id: string;
24
- };
25
- }>, reply: FastifyReply<Agency>, scope: S, action: ActionsOf<S>): Promise<never>;
26
- }
27
- export {};
@@ -1,89 +0,0 @@
1
- /* * */
2
- import { HTTP_STATUS, HttpException } from '@tmlmobilidade/consts';
3
- import { goDb } from '@tmlmobilidade/go-interfaces-godb';
4
- import { Logger } from '@tmlmobilidade/logger';
5
- import { PermissionCatalog } from '@tmlmobilidade/types';
6
- function getAgencyIdsFromPermission(permission) {
7
- return permission?.resources?.agency_ids ?? [];
8
- }
9
- /* * */
10
- export class AgenciesSharedController {
11
- //
12
- /**
13
- * Returns all Agencies sorted by ID.
14
- * @param request The request object
15
- * @param reply The reply object
16
- */
17
- static async getAll(request, reply) {
18
- //
19
- // A. Setup variables
20
- const scope = request.query.scope;
21
- const actions = (Array.isArray(request.query.actions) ? request.query.actions : request.query.actions?.split(','))?.filter(Boolean);
22
- if (!scope || !actions?.length) {
23
- throw new HttpException(HTTP_STATUS.BAD_REQUEST, 'Missing scope or actions query parameters');
24
- }
25
- //
26
- // Detect which agency_ids the user has access to,
27
- // based on their permissions. If none, return an empty array.
28
- const permittedAgencyIds = actions.flatMap(action => getAgencyIdsFromPermission(PermissionCatalog.get(request.permissions, scope, action)));
29
- if (!permittedAgencyIds.length) {
30
- Logger.issue({
31
- context: {
32
- action: 'getAll',
33
- feature: 'agencies',
34
- request,
35
- },
36
- level: 'info',
37
- messageOrError: 'No agency_ids found in permissions',
38
- });
39
- return reply.send({ data: [], error: null, statusCode: HTTP_STATUS.OK });
40
- }
41
- const allowAllAgencies = permittedAgencyIds.includes(PermissionCatalog.ALLOW_ALL_FLAG);
42
- const queryFilters = allowAllAgencies ? {} : { _id: { $in: permittedAgencyIds } };
43
- const allAgencies = await goDb.core.agencies.findMany(queryFilters, { sort: { _id: 1 } });
44
- reply.send({ data: allAgencies, error: null, statusCode: HTTP_STATUS.OK });
45
- }
46
- /**
47
- * Returns an Agency by ID.
48
- * @param request The request object
49
- * @param reply The reply object
50
- */
51
- static async getById(request, reply, scope, action) {
52
- //
53
- // Detect which agency_ids the user has access to,
54
- // based on their permissions. If none, return an empty array.
55
- const agenciesPermission = PermissionCatalog.get(request.permissions, scope, action);
56
- const permittedAgencyIds = getAgencyIdsFromPermission(agenciesPermission);
57
- if (!permittedAgencyIds.length) {
58
- Logger.issue({
59
- context: {
60
- action: 'getById',
61
- feature: 'agencies',
62
- request,
63
- value: request.params.id,
64
- },
65
- level: 'info',
66
- messageOrError: 'No agency_ids found in permissions',
67
- });
68
- return reply.send({ data: null, error: null, statusCode: HTTP_STATUS.OK });
69
- }
70
- const allowAllAgencies = permittedAgencyIds.includes(PermissionCatalog.ALLOW_ALL_FLAG);
71
- const queryFilters = allowAllAgencies ? { _id: request.params.id } : { _id: { $eq: request.params.id, $in: permittedAgencyIds } };
72
- const agencyData = await goDb.core.agencies.findOne(queryFilters);
73
- if (!agencyData) {
74
- const error = new HttpException(HTTP_STATUS.NOT_FOUND, 'Agency not found');
75
- Logger.issue({
76
- context: {
77
- action: 'getById',
78
- feature: 'agencies',
79
- request,
80
- value: request.params.id,
81
- },
82
- level: 'error',
83
- messageOrError: error,
84
- });
85
- throw error;
86
- }
87
- reply.send({ data: agencyData, error: null, statusCode: HTTP_STATUS.OK });
88
- }
89
- }
@@ -1 +0,0 @@
1
- export * from './agencies.js';
@@ -1 +0,0 @@
1
- export * from './agencies.js';
@@ -1,12 +0,0 @@
1
- import { type FastifyReply, type FastifyRequest } from '@tmlmobilidade/fastify';
2
- import { type ActionsOf, type GetRidesBatchQuery, type HashedTrip, type Permission } from '@tmlmobilidade/types';
3
- export declare class HashedTripsSharedController {
4
- /**
5
- * Gets a batch of HashedTrips built with an aggregation pipeline.
6
- * @param request The Fastify request object.
7
- * @param reply The Fastify reply object.
8
- */
9
- static getBatch<S extends Permission['scope']>(request: FastifyRequest<{
10
- Querystring: GetRidesBatchQuery;
11
- }>, reply: FastifyReply<HashedTrip[]>, scope: S, action: ActionsOf<S>): Promise<never>;
12
- }
@@ -1,77 +0,0 @@
1
- /* * */
2
- import { HTTP_STATUS } from '@tmlmobilidade/consts';
3
- import { goDb } from '@tmlmobilidade/go-interfaces-godb';
4
- import { ridesBatchAggregationPipeline } from '@tmlmobilidade/interfaces';
5
- import { Logger } from '@tmlmobilidade/logger';
6
- import { GetRidesBatchQuerySchema, PermissionCatalog } from '@tmlmobilidade/types';
7
- /* * */
8
- export class HashedTripsSharedController {
9
- //
10
- /**
11
- * Gets a batch of HashedTrips built with an aggregation pipeline.
12
- * @param request The Fastify request object.
13
- * @param reply The Fastify reply object.
14
- */
15
- static async getBatch(request, reply, scope, action) {
16
- //
17
- //
18
- // Validate the request query parameters
19
- const parsedQuery = GetRidesBatchQuerySchema.parse(request.query);
20
- //
21
- // Detect which agency_ids the user has access to,
22
- // based on their permissions. If none, return an empty array.
23
- const ridesPermission = PermissionCatalog.get(request.permissions, scope, action);
24
- if (!ridesPermission['resources']?.agency_ids?.length) {
25
- Logger.issue({
26
- context: {
27
- action: 'getBatch',
28
- feature: 'hashedTrips',
29
- request,
30
- },
31
- level: 'info',
32
- messageOrError: 'No agency_ids found in permissions',
33
- });
34
- return reply.send({ data: [], error: null, statusCode: HTTP_STATUS.OK });
35
- }
36
- const allowAllAgencies = ridesPermission['resources'].agency_ids.includes(PermissionCatalog.ALLOW_ALL_FLAG);
37
- //
38
- // Get the rides batch using native MongoDB cursor
39
- // with batchSize to prevent memory issues
40
- const pipeline = ridesBatchAggregationPipeline({
41
- acceptance_status: parsedQuery.acceptance_status,
42
- agency_ids: parsedQuery.agency_ids?.filter(id => allowAllAgencies || ridesPermission['resources'].agency_ids.includes(id)) ?? [],
43
- analysis_ended_at_last_stop_grade: parsedQuery.analysis_ended_at_last_stop_grade,
44
- analysis_expected_apex_validation_interval: parsedQuery.analysis_expected_apex_validation_interval,
45
- analysis_simple_three_vehicle_events_grade: parsedQuery.analysis_simple_three_vehicle_events_grade,
46
- analysis_transaction_sequentiality: parsedQuery.analysis_transaction_sequentiality,
47
- date_end: parsedQuery.date_end,
48
- date_start: parsedQuery.date_start,
49
- delay_statuses: parsedQuery.delay_statuses,
50
- line_ids: parsedQuery.line_ids,
51
- operational_statuses: parsedQuery.operational_statuses,
52
- search: parsedQuery.search,
53
- seen_statuses: parsedQuery.seen_statuses,
54
- stop_ids: parsedQuery.stop_ids,
55
- });
56
- //
57
- // Limit the number of rides to 2000 and sort by start_time_scheduled
58
- pipeline.push({ $limit: 2000 }, { $project: { hashed_trip_id: 1 } }, { $sort: { start_time_scheduled: 1 } });
59
- //
60
- // Fetch the rides batch from the database
61
- const ridesBatch = await goDb.operation.rides.aggregate(pipeline);
62
- Logger.info({ message: `HashedTripsSharedController.getBatch - ridesBatch count: ${ridesBatch?.length ?? 0}` });
63
- //
64
- // From the given batch of hashed_trip_ids,
65
- // fetch the full HashedTrip documents with a single query.
66
- const hashedTripIds = ridesBatch.map(ride => ride.hashed_trip_id);
67
- const hashedTripsBatch = await goDb.operation.hashedTrips.findMany({ _id: { $in: hashedTripIds } });
68
- //
69
- // Send the response
70
- reply.send({
71
- data: hashedTripsBatch ?? [],
72
- error: null,
73
- statusCode: HTTP_STATUS.OK,
74
- });
75
- //
76
- }
77
- }
@@ -1 +0,0 @@
1
- export * from './hashed-trips.js';
@@ -1 +0,0 @@
1
- export * from './hashed-trips.js';
@@ -1,4 +0,0 @@
1
- export * from './hashed-trips/index.js';
2
- export * from './lines/index.js';
3
- export * from './rides/index.js';
4
- export * from './stops/index.js';
@@ -1,4 +0,0 @@
1
- export * from './hashed-trips/index.js';
2
- export * from './lines/index.js';
3
- export * from './rides/index.js';
4
- export * from './stops/index.js';
@@ -1,2 +0,0 @@
1
- import { type GetOperationalLinesBatchQuery, type OperationalLine } from '@tmlmobilidade/types';
2
- export declare function getOperationalLinesBatch(query: GetOperationalLinesBatchQuery): Promise<OperationalLine[]>;
@@ -1,111 +0,0 @@
1
- /* * */
2
- import { goDb } from '@tmlmobilidade/go-interfaces-godb';
3
- import { Logger } from '@tmlmobilidade/logger';
4
- /* * */
5
- export async function getOperationalLinesBatch(query) {
6
- //
7
- //
8
- // Use Rides as the baseline to fetch distinct hashed_pattern_ids matching the query parameters.
9
- // Rides are the glue between the different entities (Patterns, Lines, Stops, etc...) that compose an Operation,
10
- // Stream the rides to build the Operation Lines batch on the fly, avoiding loading everything in memory at once.
11
- const pipeline = [
12
- {
13
- $match: {
14
- agency_id: { $in: query.agency_ids ?? [] },
15
- start_time_scheduled: { $gte: query.date_start, $lte: query.date_end },
16
- },
17
- },
18
- {
19
- $group: {
20
- _id: '$hashed_pattern_id',
21
- latest_ride: {
22
- $top: {
23
- output: {
24
- agency_id: '$agency_id',
25
- hashed_pattern_id: '$hashed_pattern_id',
26
- operational_date: '$operational_date',
27
- plan_id: '$plan_id',
28
- start_time_scheduled: '$start_time_scheduled',
29
- },
30
- // deterministic tie-break if same timestamp appears multiple times
31
- // eslint-disable-next-line perfectionist/sort-objects
32
- sortBy: { start_time_scheduled: -1, _id: -1 },
33
- },
34
- },
35
- },
36
- },
37
- {
38
- $replaceRoot: {
39
- newRoot: '$latest_ride',
40
- },
41
- },
42
- {
43
- $sort: {
44
- start_time_scheduled: -1,
45
- },
46
- },
47
- {
48
- $lookup: {
49
- as: 'hashed_pattern_doc',
50
- foreignField: '_id',
51
- from: 'hashed-patterns',
52
- localField: 'hashed_pattern_id',
53
- },
54
- },
55
- {
56
- $unwind: {
57
- path: '$hashed_pattern_doc',
58
- preserveNullAndEmptyArrays: true,
59
- },
60
- },
61
- {
62
- $project: {
63
- _id: 0,
64
- agency_id: 1,
65
- hashed_pattern_doc: 1, // full joined document
66
- operational_date: 1,
67
- plan_id: 1,
68
- start_time_scheduled: 1,
69
- },
70
- },
71
- ];
72
- const ridesCollection = await goDb.operation.rides.getCollection();
73
- const pipelineResult = await ridesCollection
74
- .aggregate(pipeline, { allowDiskUse: true })
75
- .toArray();
76
- Logger.info({ message: `OperationalLinesController.getBatch - pipeline result count: ${pipelineResult?.length ?? 0}` });
77
- //
78
- // Setup the final Map to keep track of the Operation Lines,
79
- // using the line_id as the key to avoid duplicates,
80
- // since multiple hashed_pattern_ids can belong to the same line_id.
81
- const operationalLinesMap = new Map();
82
- pipelineResult.forEach((item) => {
83
- // Initialize the line in the map if it doesn't exist yet
84
- if (!operationalLinesMap.has(item.hashed_pattern_doc.line_id)) {
85
- operationalLinesMap.set(item.hashed_pattern_doc.line_id, {
86
- agency_id: item.agency_id,
87
- hashed_patterns: [],
88
- last_operational_date: item.operational_date,
89
- last_plan_id: item.plan_id,
90
- line_id: item.hashed_pattern_doc.line_id,
91
- line_long_name: item.hashed_pattern_doc.line_long_name,
92
- line_short_name: item.hashed_pattern_doc.line_short_name,
93
- pattern_ids: [],
94
- route_color: item.hashed_pattern_doc.route_color,
95
- route_ids: [],
96
- stop_ids: [],
97
- });
98
- }
99
- // Get the saved line from the map
100
- const savedOperationalLine = operationalLinesMap.get(item.hashed_pattern_doc.line_id);
101
- // Update the object with the latest fields
102
- savedOperationalLine.route_ids = Array.from(new Set([...savedOperationalLine.route_ids, item.hashed_pattern_doc.route_id]));
103
- savedOperationalLine.pattern_ids = Array.from(new Set([...savedOperationalLine.pattern_ids, item.hashed_pattern_doc.pattern_id]));
104
- savedOperationalLine.stop_ids = Array.from(new Set([...savedOperationalLine.stop_ids, ...(item.hashed_pattern_doc.path.map(stop => stop.stop_id) ?? [])]));
105
- savedOperationalLine.hashed_patterns.push(item.hashed_pattern_doc);
106
- });
107
- //
108
- // Send the response
109
- return Array.from(operationalLinesMap.values());
110
- //
111
- }
@@ -1,12 +0,0 @@
1
- import { type FastifyReply, type FastifyRequest } from '@tmlmobilidade/fastify';
2
- import { type ActionsOf, type GetOperationalLinesBatchQuery, type OperationalLine, type Permission } from '@tmlmobilidade/types';
3
- export declare class OperationalLinesSharedController {
4
- /**
5
- * Gets a batch of Operation Lines built with an aggregation pipeline.
6
- * @param request The Fastify request object.
7
- * @param reply The Fastify reply object.
8
- */
9
- static getBatch<S extends Permission['scope']>(request: FastifyRequest<{
10
- Querystring: GetOperationalLinesBatchQuery;
11
- }>, reply: FastifyReply<OperationalLine[]>, scope: S, action: ActionsOf<S>): Promise<never>;
12
- }
@@ -1,49 +0,0 @@
1
- /* * */
2
- import { getOperationalLinesBatch } from './batch.js';
3
- import { HTTP_STATUS } from '@tmlmobilidade/consts';
4
- import { Logger } from '@tmlmobilidade/logger';
5
- import { GetOperationalLinesBatchQuerySchema, PermissionCatalog } from '@tmlmobilidade/types';
6
- /* * */
7
- export class OperationalLinesSharedController {
8
- //
9
- /**
10
- * Gets a batch of Operation Lines built with an aggregation pipeline.
11
- * @param request The Fastify request object.
12
- * @param reply The Fastify reply object.
13
- */
14
- static async getBatch(request, reply, scope, action) {
15
- //
16
- const parsedQuery = GetOperationalLinesBatchQuerySchema.parse(request.query);
17
- //
18
- // Detect which agency_ids the user has access to,
19
- // based on their permissions. If none, return an empty array.
20
- const ridesPermission = PermissionCatalog.get(request.permissions, scope, action);
21
- if (!ridesPermission['resources']?.agency_ids?.length) {
22
- Logger.issue({
23
- context: {
24
- action: 'getBatch',
25
- feature: 'operationalLines',
26
- request,
27
- },
28
- level: 'info',
29
- messageOrError: 'No agency_ids found in permissions',
30
- });
31
- return reply.send({ data: [], error: null, statusCode: HTTP_STATUS.OK });
32
- }
33
- const allowAllAgencies = ridesPermission['resources'].agency_ids.includes(PermissionCatalog.ALLOW_ALL_FLAG);
34
- //
35
- // Run the query
36
- const result = await getOperationalLinesBatch({
37
- ...parsedQuery,
38
- agency_ids: parsedQuery.agency_ids?.filter(id => allowAllAgencies || ridesPermission['resources'].agency_ids.includes(id)) ?? [],
39
- });
40
- //
41
- // Send the response
42
- reply.send({
43
- data: result,
44
- error: null,
45
- statusCode: HTTP_STATUS.OK,
46
- });
47
- //
48
- }
49
- }
@@ -1,2 +0,0 @@
1
- export * from './batch.js';
2
- export * from './controller.js';
@@ -1,2 +0,0 @@
1
- export * from './batch.js';
2
- export * from './controller.js';
@@ -1,2 +0,0 @@
1
- export * from './rides.js';
2
- export * from './watch.js';
@@ -1,2 +0,0 @@
1
- export * from './rides.js';
2
- export * from './watch.js';
@@ -1,31 +0,0 @@
1
- import { type FastifyReply, type FastifyRequest } from '@tmlmobilidade/fastify';
2
- import { type ActionsOf, type GetRidesBatchQuery, type Permission, type RideNormalized } from '@tmlmobilidade/types';
3
- import { type WebSocket } from 'ws';
4
- export declare class RidesSharedController {
5
- /**
6
- * Gets a batch of Rides built with an aggregation pipeline.
7
- * @param request The Fastify request object.
8
- * @param reply The Fastify reply object.
9
- */
10
- static getBatch<S extends Permission['scope']>(request: FastifyRequest<{
11
- Querystring: GetRidesBatchQuery;
12
- }>, reply: FastifyReply<RideNormalized[]>, scope: S, action: ActionsOf<S>): Promise<never>;
13
- /**
14
- * Get a Ride by ID.
15
- * @param request The Fastify request object.
16
- * @param reply The Fastify reply object.
17
- */
18
- static getRideById<S extends Permission['scope']>(request: FastifyRequest, reply: FastifyReply<RideNormalized>, scope: S, action: ActionsOf<S>): Promise<never>;
19
- /**
20
- * Get a Ride by multiple IDs.
21
- * @param request The Fastify request object.
22
- * @param reply The Fastify reply object.
23
- */
24
- n: any;
25
- static getRideByIds<S extends Permission['scope']>(request: FastifyRequest, reply: FastifyReply<RideNormalized[]>, scope: S, action: ActionsOf<S>): Promise<never>;
26
- /**
27
- * WebSocket event handler.
28
- * @param socket The WebSocket object.
29
- */
30
- static websocket(socket: WebSocket): () => void;
31
- }
@@ -1,191 +0,0 @@
1
- /* * */
2
- import { ridesChangeStream } from './watch.js';
3
- import { HTTP_STATUS } from '@tmlmobilidade/consts';
4
- import { goDb } from '@tmlmobilidade/go-interfaces-godb';
5
- import { ridesBatchAggregationPipeline } from '@tmlmobilidade/interfaces';
6
- import { Logger } from '@tmlmobilidade/logger';
7
- import { normalizeRide } from '@tmlmobilidade/normalizers';
8
- import { GetRidesBatchQuerySchema, PermissionCatalog } from '@tmlmobilidade/types';
9
- /* * */
10
- export class RidesSharedController {
11
- //
12
- /**
13
- * Gets a batch of Rides built with an aggregation pipeline.
14
- * @param request The Fastify request object.
15
- * @param reply The Fastify reply object.
16
- */
17
- static async getBatch(request, reply, scope, action) {
18
- //
19
- //
20
- // Validate the request query parameters
21
- const parsedQuery = GetRidesBatchQuerySchema.parse(request.query);
22
- //
23
- // Detect which agency_ids the user has access to,
24
- // based on their permissions. If none, return an empty array.
25
- const ridesPermission = PermissionCatalog.get(request.permissions, scope, action);
26
- if (!ridesPermission['resources']?.agency_ids?.length) {
27
- Logger.issue({
28
- context: {
29
- action: 'getBatch',
30
- feature: 'rides',
31
- request,
32
- },
33
- level: 'info',
34
- messageOrError: 'No agency_ids found in permissions',
35
- });
36
- return reply.send({ data: [], error: null, statusCode: HTTP_STATUS.OK });
37
- }
38
- const allowAllAgencies = ridesPermission['resources'].agency_ids.includes(PermissionCatalog.ALLOW_ALL_FLAG);
39
- //
40
- // If search is provided, immediately try to find the ride by ID.
41
- // If found, return it as the only result. This optimizes
42
- // for the common case of searching by ride ID.
43
- const searchQuery = parsedQuery.search?.trim() ?? '';
44
- const foundRideById = await goDb.operation.rides.findOne({
45
- _id: searchQuery,
46
- ...(allowAllAgencies ? {} : { agency_id: { $in: ridesPermission['resources'].agency_ids } }),
47
- });
48
- if (foundRideById) {
49
- const normalizedRide = normalizeRide(foundRideById);
50
- return reply.send({ data: [normalizedRide], error: null, statusCode: HTTP_STATUS.OK });
51
- }
52
- //
53
- // Get the rides batch using native MongoDB cursor
54
- // with batchSize to prevent memory issues
55
- const pipeline = ridesBatchAggregationPipeline({
56
- acceptance_status: parsedQuery.acceptance_status,
57
- agency_ids: parsedQuery.agency_ids?.filter(id => allowAllAgencies || ridesPermission['resources'].agency_ids.includes(id)) ?? [],
58
- analysis_ended_at_last_stop_grade: parsedQuery.analysis_ended_at_last_stop_grade,
59
- analysis_expected_apex_validation_interval: parsedQuery.analysis_expected_apex_validation_interval,
60
- analysis_simple_three_vehicle_events_grade: parsedQuery.analysis_simple_three_vehicle_events_grade,
61
- analysis_transaction_sequentiality: parsedQuery.analysis_transaction_sequentiality,
62
- date_end: parsedQuery.date_end,
63
- date_start: parsedQuery.date_start,
64
- delay_statuses: parsedQuery.delay_statuses,
65
- line_ids: parsedQuery.line_ids,
66
- operational_statuses: parsedQuery.operational_statuses,
67
- search: parsedQuery.search,
68
- seen_statuses: parsedQuery.seen_statuses,
69
- stop_ids: parsedQuery.stop_ids,
70
- //
71
- ticketing_status: parsedQuery.ticketing_status,
72
- });
73
- //
74
- // Limit the number of rides to 2000 and sort by start_time_scheduled
75
- pipeline.push({ $limit: 2000 }, { $sort: { start_time_scheduled: 1 } });
76
- //
77
- // Fetch the rides batch from the database
78
- const ridesBatch = await goDb.operation.rides.aggregate(pipeline);
79
- //
80
- // Send the response
81
- reply.send({
82
- data: ridesBatch ?? [],
83
- error: null,
84
- statusCode: HTTP_STATUS.OK,
85
- });
86
- //
87
- }
88
- /**
89
- * Get a Ride by ID.
90
- * @param request The Fastify request object.
91
- * @param reply The Fastify reply object.
92
- */
93
- static async getRideById(request, reply, scope, action) {
94
- //
95
- //
96
- // Detect which agency_ids the user has access to,
97
- // based on their permissions. If none, return an empty array.
98
- const ridesPermission = PermissionCatalog.get(request.permissions, scope, action);
99
- if (!ridesPermission['resources']?.agency_ids?.length) {
100
- Logger.issue({
101
- context: {
102
- action: 'getRideById',
103
- feature: 'rides',
104
- request,
105
- value: request.params['id'],
106
- },
107
- level: 'info',
108
- messageOrError: 'No agency_ids found in permissions',
109
- });
110
- return reply.send({ data: null, error: null, statusCode: HTTP_STATUS.OK });
111
- }
112
- const allowAllAgencies = ridesPermission['resources'].agency_ids.includes(PermissionCatalog.ALLOW_ALL_FLAG);
113
- //
114
- // If search is provided, immediately try to find the ride by ID.
115
- // If found, return it as the only result. This optimizes
116
- // for the common case of searching by ride ID.
117
- const foundRideById = await goDb.operation.rides.findOne({
118
- _id: request.params['id'],
119
- ...(allowAllAgencies ? {} : { agency_id: { $in: ridesPermission['resources'].agency_ids } }),
120
- });
121
- if (foundRideById) {
122
- const normalizedRide = normalizeRide(foundRideById);
123
- return reply.send({ data: normalizedRide, error: null, statusCode: HTTP_STATUS.OK });
124
- }
125
- //
126
- }
127
- /**
128
- * Get a Ride by multiple IDs.
129
- * @param request The Fastify request object.
130
- * @param reply The Fastify reply object.
131
- */
132
- n;
133
- static async getRideByIds(request, reply, scope, action) {
134
- //
135
- //
136
- // Detect which agency_ids the user has access to,
137
- // based on their permissions. If none, return an empty array.
138
- const ridesPermission = PermissionCatalog.get(request.permissions, scope, action);
139
- if (!ridesPermission['resources']?.agency_ids?.length) {
140
- Logger.issue({
141
- context: {
142
- action: 'getRideByIds',
143
- feature: 'rides',
144
- request,
145
- value: request.query['ids'],
146
- },
147
- level: 'info',
148
- messageOrError: 'No agency_ids found in permissions',
149
- });
150
- return reply.send({ data: null, error: null, statusCode: HTTP_STATUS.OK });
151
- }
152
- const allowAllAgencies = ridesPermission['resources'].agency_ids.includes(PermissionCatalog.ALLOW_ALL_FLAG);
153
- //
154
- // If search is provided, immediately try to find the ride by ID.
155
- // If found, return it as the only result. This optimizes
156
- // for the common case of searching by ride ID.
157
- const ids = request.query['ids']?.split(',') ?? [];
158
- const foundRidesByIds = await goDb.operation.rides.findMany({
159
- _id: { $in: ids },
160
- ...(allowAllAgencies ? {} : { agency_id: { $in: ridesPermission['resources'].agency_ids } }),
161
- });
162
- return reply.send({ data: foundRidesByIds.map(ride => normalizeRide(ride)), error: null, statusCode: HTTP_STATUS.OK });
163
- //
164
- }
165
- /**
166
- * WebSocket event handler.
167
- * @param socket The WebSocket object.
168
- */
169
- static websocket(socket) {
170
- //
171
- //
172
- // Create a listener that sends updates to this WebSocket client
173
- const listener = (message) => {
174
- if (socket.readyState === socket.OPEN && socket.bufferedAmount < 1_000_000) {
175
- socket.send(JSON.stringify(message));
176
- }
177
- };
178
- //
179
- // Subscribe to the singleton change stream immediately.
180
- // Sockets do not emit 'open' or 'connection' events server-side.
181
- ridesChangeStream.subscribe(listener);
182
- //
183
- // Cleanup the subscription to the singleton change stream
184
- const cleanup = () => {
185
- ridesChangeStream.unsubscribe(listener);
186
- };
187
- socket.on('close', cleanup);
188
- socket.on('error', cleanup);
189
- return cleanup;
190
- }
191
- }
@@ -1,56 +0,0 @@
1
- import { RideNormalized } from '@tmlmobilidade/types';
2
- import { HttpResponse } from '@tmlmobilidade/utils';
3
- /**
4
- * A listener function for ride changes.
5
- * Receives normalized ride data wrapped in an HTTP response format.
6
- */
7
- export type RideChangeListener = (message: HttpResponse<RideNormalized>) => void;
8
- /**
9
- * Singleton manager for MongoDB change streams with pub/sub capabilities.
10
- *
11
- * This class creates a single MongoDB change stream that watches for ride updates
12
- * and broadcasts them to multiple subscribers using an in-memory EventEmitter.
13
- *
14
- * The singleton pattern ensures only one change stream is active, regardless of
15
- * how many WebSocket clients are connected.
16
- */
17
- declare class RidesChangeStreamManager {
18
- private static instance;
19
- /**
20
- * In-memory pub/sub event emitter. (@see https://nodejs.org/api/events.html#class-eventemitter)
21
- *
22
- * This EventEmitter acts as the pub/sub broker between the MongoDB change stream
23
- * and WebSocket clients:
24
- * - The MongoDB change stream publishes to this emitter when rides change
25
- * - WebSocket clients subscribe to this emitter to receive updates
26
- * - When unsubscribing, clients are removed from the emitter's listener list
27
- *
28
- * Setting maxListeners to 0 allows unlimited subscribers without warnings,
29
- * which is necessary since we may have many concurrent WebSocket connections.
30
- */
31
- private emitter;
32
- private initialized;
33
- /**
34
- * Private constructor enforces singleton pattern.
35
- * Use `getInstance()` to access the instance.
36
- */
37
- private constructor();
38
- static getInstance(): Promise<RidesChangeStreamManager>;
39
- subscribe(listener: RideChangeListener): void;
40
- unsubscribe(listener: RideChangeListener): void;
41
- /**
42
- * Initializes the MongoDB change stream.
43
- *
44
- * This is called once during getInstance() to set up the change stream.
45
- * The stream watches all operations on the rides collection and publishes
46
- * changes to the EventEmitter.
47
- *
48
- * Flow:
49
- * 1. MongoDB detects a change → 2. Change stream emits 'change' event →
50
- * 3. Normalizes the ride data → 4. Publishes to EventEmitter →
51
- * 5. All subscribed listeners receive the update
52
- */
53
- private init;
54
- }
55
- export declare const ridesChangeStream: RidesChangeStreamManager;
56
- export {};
@@ -1,113 +0,0 @@
1
- /* * */
2
- /**
3
- * Rides Change Stream Manager
4
- *
5
- * This module implements a singleton pattern for MongoDB change streams with in-memory pub/sub:
6
- *
7
- * Architecture:
8
- * ```
9
- * MongoDB Change Stream (1 singleton)
10
- * ↓
11
- * EventEmitter pub/sub
12
- * ↓
13
- * WebSocket clients
14
- * ```
15
- *
16
- * Instead of creating a MongoDB change stream per WebSocket connection (inefficient),
17
- * this creates a single change stream that publishes to an in-memory EventEmitter.
18
- * Multiple WebSocket clients can subscribe/unsubscribe to receive real-time updates.
19
- *
20
- * Benefits:
21
- * - Single MongoDB change stream regardless of number of clients
22
- * - Lazy initialization - stream only starts when first client connects through "asyncSingletonProxy"
23
- * - Clean subscription management per client
24
- * - Reduced database load and network overhead
25
- */
26
- import { HTTP_STATUS } from '@tmlmobilidade/consts';
27
- import { goDb } from '@tmlmobilidade/go-interfaces-godb';
28
- import { normalizeRide } from '@tmlmobilidade/normalizers';
29
- import { asyncSingletonProxy } from '@tmlmobilidade/utils';
30
- import EventEmitter from 'events';
31
- /**
32
- * Singleton manager for MongoDB change streams with pub/sub capabilities.
33
- *
34
- * This class creates a single MongoDB change stream that watches for ride updates
35
- * and broadcasts them to multiple subscribers using an in-memory EventEmitter.
36
- *
37
- * The singleton pattern ensures only one change stream is active, regardless of
38
- * how many WebSocket clients are connected.
39
- */
40
- class RidesChangeStreamManager {
41
- //
42
- static instance = null;
43
- /**
44
- * In-memory pub/sub event emitter. (@see https://nodejs.org/api/events.html#class-eventemitter)
45
- *
46
- * This EventEmitter acts as the pub/sub broker between the MongoDB change stream
47
- * and WebSocket clients:
48
- * - The MongoDB change stream publishes to this emitter when rides change
49
- * - WebSocket clients subscribe to this emitter to receive updates
50
- * - When unsubscribing, clients are removed from the emitter's listener list
51
- *
52
- * Setting maxListeners to 0 allows unlimited subscribers without warnings,
53
- * which is necessary since we may have many concurrent WebSocket connections.
54
- */
55
- emitter = new EventEmitter();
56
- initialized = false;
57
- /**
58
- * Private constructor enforces singleton pattern.
59
- * Use `getInstance()` to access the instance.
60
- */
61
- constructor() {
62
- this.emitter.setMaxListeners(0); // Allow unlimited listeners
63
- }
64
- static async getInstance() {
65
- if (!RidesChangeStreamManager.instance) {
66
- RidesChangeStreamManager.instance = new RidesChangeStreamManager();
67
- await RidesChangeStreamManager.instance.init();
68
- }
69
- return RidesChangeStreamManager.instance;
70
- }
71
- subscribe(listener) {
72
- this.emitter.on('change', listener);
73
- }
74
- unsubscribe(listener) {
75
- this.emitter.off('change', listener);
76
- }
77
- /**
78
- * Initializes the MongoDB change stream.
79
- *
80
- * This is called once during getInstance() to set up the change stream.
81
- * The stream watches all operations on the rides collection and publishes
82
- * changes to the EventEmitter.
83
- *
84
- * Flow:
85
- * 1. MongoDB detects a change → 2. Change stream emits 'change' event →
86
- * 3. Normalizes the ride data → 4. Publishes to EventEmitter →
87
- * 5. All subscribed listeners receive the update
88
- */
89
- async init() {
90
- if (this.initialized)
91
- return;
92
- const ridesCollection = await goDb.operation.rides.getCollection();
93
- // Watch all operations with full document updates
94
- ridesCollection
95
- .watch([], { fullDocument: 'updateLookup' })
96
- .on('change', (databaseOperation) => {
97
- if (typeof databaseOperation['fullDocument'] === 'undefined') {
98
- console.log('Undefined document:', databaseOperation);
99
- return;
100
- }
101
- const normalizedRide = normalizeRide(databaseOperation['fullDocument']);
102
- const message = {
103
- data: normalizedRide,
104
- error: null,
105
- statusCode: HTTP_STATUS.OK,
106
- };
107
- // Publish to all subscribers via EventEmitter
108
- this.emitter.emit('change', message);
109
- });
110
- this.initialized = true;
111
- }
112
- }
113
- export const ridesChangeStream = asyncSingletonProxy(RidesChangeStreamManager);
@@ -1,2 +0,0 @@
1
- import { type GetOperationalStopsBatchQuery, type OperationalStop } from '@tmlmobilidade/types';
2
- export declare function getOperationalStopsBatch(query: GetOperationalStopsBatchQuery): Promise<OperationalStop[]>;
@@ -1,112 +0,0 @@
1
- /* * */
2
- import { goDb } from '@tmlmobilidade/go-interfaces-godb';
3
- import { Logger } from '@tmlmobilidade/logger';
4
- /* * */
5
- export async function getOperationalStopsBatch(query) {
6
- //
7
- //
8
- // Use Rides as the baseline to fetch distinct hashed_pattern_ids matching the query parameters.
9
- // Rides are the glue between the different entities (Patterns, Lines, Stops, etc...) that compose an Operation,
10
- // Stream the rides to build the Operation Stops batch on the fly, avoiding loading everything in memory at once.
11
- const pipeline = [
12
- {
13
- $match: {
14
- agency_id: { $in: query.agency_ids ?? [] },
15
- start_time_scheduled: { $gte: query.date_start, $lte: query.date_end },
16
- },
17
- },
18
- {
19
- $group: {
20
- _id: '$hashed_pattern_id',
21
- latest_ride: {
22
- $top: {
23
- output: {
24
- agency_id: '$agency_id',
25
- hashed_pattern_id: '$hashed_pattern_id',
26
- operational_date: '$operational_date',
27
- plan_id: '$plan_id',
28
- start_time_scheduled: '$start_time_scheduled',
29
- },
30
- // deterministic tie-break if same timestamp appears multiple times
31
- // eslint-disable-next-line perfectionist/sort-objects
32
- sortBy: { start_time_scheduled: -1, _id: -1 },
33
- },
34
- },
35
- },
36
- },
37
- {
38
- $replaceRoot: {
39
- newRoot: '$latest_ride',
40
- },
41
- },
42
- {
43
- $sort: {
44
- start_time_scheduled: -1,
45
- },
46
- },
47
- {
48
- $lookup: {
49
- as: 'hashed_pattern_doc',
50
- foreignField: '_id',
51
- from: 'hashed-patterns',
52
- localField: 'hashed_pattern_id',
53
- },
54
- },
55
- {
56
- $unwind: {
57
- path: '$hashed_pattern_doc',
58
- preserveNullAndEmptyArrays: true,
59
- },
60
- },
61
- {
62
- $project: {
63
- _id: 0,
64
- agency_id: 1,
65
- hashed_pattern_doc: 1, // full joined document
66
- operational_date: 1,
67
- plan_id: 1,
68
- start_time_scheduled: 1,
69
- },
70
- },
71
- ];
72
- const ridesCollection = await goDb.operation.rides.getCollection();
73
- const pipelineResult = await ridesCollection
74
- .aggregate(pipeline, { allowDiskUse: true })
75
- .toArray();
76
- Logger.info({ message: `OperationalStopsController.getBatch - pipeline result count: ${pipelineResult?.length ?? 0}` });
77
- //
78
- // Setup the final Map to keep track of the Operation Stops,
79
- // using the stop_id as the key to avoid duplicates,
80
- // since multiple hashed_pattern_ids can belong to the same stop_id.
81
- const operationalStopsMap = new Map();
82
- pipelineResult.forEach((item) => {
83
- item.hashed_pattern_doc.path.forEach((waypoint) => {
84
- // Initialize the stop in the map if it doesn't exist yet
85
- if (!operationalStopsMap.has(waypoint.stop_id)) {
86
- operationalStopsMap.set(waypoint.stop_id, {
87
- agency_ids: [],
88
- hashed_patterns: [],
89
- last_operational_date: item.operational_date,
90
- last_plan_id: item.plan_id,
91
- line_ids: [],
92
- pattern_ids: [],
93
- route_ids: [],
94
- stop_id: waypoint.stop_id,
95
- stop_name: waypoint.stop_name,
96
- });
97
- }
98
- // Get the saved stop from the map
99
- const savedOperationalStop = operationalStopsMap.get(waypoint.stop_id);
100
- // Update the object with the latest fields
101
- savedOperationalStop.agency_ids = Array.from(new Set([...savedOperationalStop.agency_ids, item.agency_id]));
102
- savedOperationalStop.line_ids = Array.from(new Set([...savedOperationalStop.line_ids, item.hashed_pattern_doc.line_id]));
103
- savedOperationalStop.route_ids = Array.from(new Set([...savedOperationalStop.route_ids, item.hashed_pattern_doc.route_id]));
104
- savedOperationalStop.pattern_ids = Array.from(new Set([...savedOperationalStop.pattern_ids, item.hashed_pattern_doc.pattern_id]));
105
- savedOperationalStop.hashed_patterns.push(item.hashed_pattern_doc);
106
- });
107
- });
108
- //
109
- // Send the response
110
- return Array.from(operationalStopsMap.values());
111
- //
112
- }
@@ -1,12 +0,0 @@
1
- import { type FastifyReply, type FastifyRequest } from '@tmlmobilidade/fastify';
2
- import { type ActionsOf, type GetOperationalStopsBatchQuery, type OperationalStop, type Permission } from '@tmlmobilidade/types';
3
- export declare class OperationalStopsSharedController {
4
- /**
5
- * Gets a batch of Operational Stops built with an aggregation pipeline.
6
- * @param request The Fastify request object.
7
- * @param reply The Fastify reply object.
8
- */
9
- static getBatch<S extends Permission['scope']>(request: FastifyRequest<{
10
- Querystring: GetOperationalStopsBatchQuery;
11
- }>, reply: FastifyReply<OperationalStop[]>, scope: S, action: ActionsOf<S>): Promise<never>;
12
- }
@@ -1,49 +0,0 @@
1
- /* * */
2
- import { getOperationalStopsBatch } from './batch.js';
3
- import { HTTP_STATUS } from '@tmlmobilidade/consts';
4
- import { Logger } from '@tmlmobilidade/logger';
5
- import { GetOperationalStopsBatchQuerySchema, PermissionCatalog } from '@tmlmobilidade/types';
6
- /* * */
7
- export class OperationalStopsSharedController {
8
- //
9
- /**
10
- * Gets a batch of Operational Stops built with an aggregation pipeline.
11
- * @param request The Fastify request object.
12
- * @param reply The Fastify reply object.
13
- */
14
- static async getBatch(request, reply, scope, action) {
15
- //
16
- const parsedQuery = GetOperationalStopsBatchQuerySchema.parse(request.query);
17
- //
18
- // Detect which agency_ids the user has access to,
19
- // based on their permissions. If none, return an empty array.
20
- const ridesPermission = PermissionCatalog.get(request.permissions, scope, action);
21
- if (!ridesPermission['resources']?.agency_ids?.length) {
22
- Logger.issue({
23
- context: {
24
- action: 'getBatch',
25
- feature: 'operationalStops',
26
- request,
27
- },
28
- level: 'info',
29
- messageOrError: 'No agency_ids found in permissions',
30
- });
31
- return reply.send({ data: [], error: null, statusCode: HTTP_STATUS.OK });
32
- }
33
- const allowAllAgencies = ridesPermission['resources'].agency_ids.includes(PermissionCatalog.ALLOW_ALL_FLAG);
34
- //
35
- // Run the query
36
- const result = await getOperationalStopsBatch({
37
- ...parsedQuery,
38
- agency_ids: parsedQuery.agency_ids?.filter(id => allowAllAgencies || ridesPermission['resources'].agency_ids.includes(id)) ?? [],
39
- });
40
- //
41
- // Send the response
42
- reply.send({
43
- data: result,
44
- error: null,
45
- statusCode: HTTP_STATUS.OK,
46
- });
47
- //
48
- }
49
- }
@@ -1,2 +0,0 @@
1
- export * from './batch.js';
2
- export * from './controller.js';
@@ -1,2 +0,0 @@
1
- export * from './batch.js';
2
- export * from './controller.js';