@tmlmobilidade/controllers 20260811.1505.45 → 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.
- package/README.md +2 -0
- package/dist/exporter/exporter.d.ts +2 -2
- package/dist/exporter/exporter.js +7 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/package.json +6 -4
- package/dist/agencies/agencies.d.ts +0 -27
- package/dist/agencies/agencies.js +0 -89
- package/dist/agencies/index.d.ts +0 -1
- package/dist/agencies/index.js +0 -1
- package/dist/operation/hashed-trips/hashed-trips.d.ts +0 -12
- package/dist/operation/hashed-trips/hashed-trips.js +0 -77
- package/dist/operation/hashed-trips/index.d.ts +0 -1
- package/dist/operation/hashed-trips/index.js +0 -1
- package/dist/operation/index.d.ts +0 -4
- package/dist/operation/index.js +0 -4
- package/dist/operation/lines/batch.d.ts +0 -2
- package/dist/operation/lines/batch.js +0 -111
- package/dist/operation/lines/controller.d.ts +0 -12
- package/dist/operation/lines/controller.js +0 -49
- package/dist/operation/lines/index.d.ts +0 -2
- package/dist/operation/lines/index.js +0 -2
- package/dist/operation/rides/index.d.ts +0 -2
- package/dist/operation/rides/index.js +0 -2
- package/dist/operation/rides/rides.d.ts +0 -31
- package/dist/operation/rides/rides.js +0 -191
- package/dist/operation/rides/watch.d.ts +0 -56
- package/dist/operation/rides/watch.js +0 -113
- package/dist/operation/stops/batch.d.ts +0 -2
- package/dist/operation/stops/batch.js +0 -112
- package/dist/operation/stops/controller.d.ts +0 -12
- package/dist/operation/stops/controller.js +0 -49
- package/dist/operation/stops/index.d.ts +0 -2
- package/dist/operation/stops/index.js +0 -2
package/README.md
CHANGED
|
@@ -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({
|
|
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
|
|
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
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmlmobilidade/controllers",
|
|
3
|
-
"version": "
|
|
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
|
-
}
|
package/dist/agencies/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './agencies.js';
|
package/dist/agencies/index.js
DELETED
|
@@ -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';
|
package/dist/operation/index.js
DELETED
|
@@ -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,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,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
|
-
}
|