@tmlmobilidade/interfaces 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.
Files changed (34) hide show
  1. package/dist/common/aggregation-pipeline.d.ts +1 -1
  2. package/dist/common/enrich-user-refs.js +1 -1
  3. package/dist/common/mongo-collection.d.ts +1 -1
  4. package/dist/common/mongo-collection.js +1 -1
  5. package/dist/interfaces/index.d.ts +0 -4
  6. package/dist/interfaces/index.js +0 -4
  7. package/dist/interfaces/metrics/metrics.d.ts +1 -1
  8. package/dist/interfaces/metrics/metrics.js +1 -1
  9. package/dist/interfaces/sams/aggregations/apex-versions.d.ts +1 -1
  10. package/dist/interfaces/sams/aggregations/batch-list.d.ts +1 -1
  11. package/dist/interfaces/sams/aggregations/export-analysis.d.ts +1 -1
  12. package/dist/interfaces/sams/build-sams-match.d.ts +1 -7
  13. package/dist/interfaces/sams/build-sams-match.js +88 -87
  14. package/dist/interfaces/sams/index.d.ts +0 -1
  15. package/dist/interfaces/sams/index.js +0 -1
  16. package/dist/interfaces/sams/sams.d.ts +1 -1
  17. package/dist/interfaces/sams/sams.js +1 -1
  18. package/package.json +7 -5
  19. package/dist/interfaces/notifications/index.d.ts +0 -1
  20. package/dist/interfaces/notifications/index.js +0 -1
  21. package/dist/interfaces/notifications/notifications.d.ts +0 -26
  22. package/dist/interfaces/notifications/notifications.js +0 -107
  23. package/dist/interfaces/proposed-changes/index.d.ts +0 -1
  24. package/dist/interfaces/proposed-changes/index.js +0 -1
  25. package/dist/interfaces/proposed-changes/proposed-changes.d.ts +0 -16
  26. package/dist/interfaces/proposed-changes/proposed-changes.js +0 -34
  27. package/dist/interfaces/rides/index.d.ts +0 -1
  28. package/dist/interfaces/rides/index.js +0 -1
  29. package/dist/interfaces/rides/pipelines.d.ts +0 -124
  30. package/dist/interfaces/rides/pipelines.js +0 -480
  31. package/dist/interfaces/vehicle-events/index.d.ts +0 -1
  32. package/dist/interfaces/vehicle-events/index.js +0 -1
  33. package/dist/interfaces/vehicle-events/simplified-vehicle-events.d.ts +0 -17
  34. package/dist/interfaces/vehicle-events/simplified-vehicle-events.js +0 -37
@@ -1,4 +1,4 @@
1
- import { type OptionalIf } from '@tmlmobilidade/types';
1
+ import { type OptionalIf } from '@tmlmobilidade/go-types-shared';
2
2
  import { Filter } from 'mongodb';
3
3
  interface MatchStage<T> {
4
4
  $match: Filter<T>;
@@ -1,5 +1,5 @@
1
1
  import { goDb } from '@tmlmobilidade/go-interfaces-godb';
2
- import { UserDisplayFields } from '@tmlmobilidade/types';
2
+ import { UserDisplayFields } from '@tmlmobilidade/go-types-core';
3
3
  const isUserRefKey = (k) => k === 'created_by' || k === 'updated_by';
4
4
  const isEligibleId = (v) => typeof v === 'string' && v && v !== 'system';
5
5
  function collectUserIds(input, out) {
@@ -1,6 +1,6 @@
1
1
  import { type AggregationPipeline } from './aggregation-pipeline.js';
2
+ import { type UnixTimestamp } from '@tmlmobilidade/go-types-shared';
2
3
  import { MongoConnector } from '@tmlmobilidade/mongo';
3
- import { type UnixTimestamp } from '@tmlmobilidade/types';
4
4
  import { type AggregateOptions, type AggregationCursor, type Collection, type DeleteOptions, type DeleteResult, type Document, type Filter, type FindOptions, Flatten, type IndexDescription, type InsertManyResult, type InsertOneOptions, type InsertOneResult, type MongoClientOptions, type UpdateOptions, type UpdateResult, type WithId } from 'mongodb';
5
5
  import { z } from 'zod';
6
6
  export declare abstract class MongoCollectionClass<T extends Document, TCreate, TUpdate> {
@@ -1,6 +1,6 @@
1
1
  /* * */
2
2
  import { HTTP_STATUS, HttpException } from '@tmlmobilidade/consts';
3
- import { Dates } from '@tmlmobilidade/dates';
3
+ import { Dates } from '@tmlmobilidade/go-utils-dates';
4
4
  import { MongoConnector } from '@tmlmobilidade/mongo';
5
5
  import { generateRandomString } from '@tmlmobilidade/strings';
6
6
  /* * */
@@ -1,7 +1,3 @@
1
1
  export * from './metrics/index.js';
2
- export * from './notifications/index.js';
3
- export * from './proposed-changes/index.js';
4
- export * from './rides/index.js';
5
2
  export * from './sams/index.js';
6
3
  export * from './simplified-apex/index.js';
7
- export * from './vehicle-events/index.js';
@@ -1,7 +1,3 @@
1
1
  export * from './metrics/index.js';
2
- export * from './notifications/index.js';
3
- export * from './proposed-changes/index.js';
4
- export * from './rides/index.js';
5
2
  export * from './sams/index.js';
6
3
  export * from './simplified-apex/index.js';
7
- export * from './vehicle-events/index.js';
@@ -1,5 +1,5 @@
1
1
  import { MongoCollectionClass } from '../../common/mongo-collection.js';
2
- import { MetricSchema } from '@tmlmobilidade/types';
2
+ import { MetricSchema } from '@tmlmobilidade/go-types-performance';
3
3
  import { IndexDescription } from 'mongodb';
4
4
  import { z } from 'zod';
5
5
  export type MetricDocument = z.infer<typeof MetricSchema>;
@@ -1,6 +1,6 @@
1
1
  /* * */
2
2
  import { MongoCollectionClass } from '../../common/mongo-collection.js';
3
- import { MetricSchema } from '@tmlmobilidade/types';
3
+ import { MetricSchema } from '@tmlmobilidade/go-types-performance';
4
4
  import { asyncSingletonProxy } from '@tmlmobilidade/utils';
5
5
  /* * */
6
6
  class MetricsClass extends MongoCollectionClass {
@@ -1,5 +1,5 @@
1
1
  import { type AggregationPipeline } from '../../../common/aggregation-pipeline.js';
2
- import { type Sam } from '@tmlmobilidade/types';
2
+ import { type Sam } from '@tmlmobilidade/go-types-operation';
3
3
  /**
4
4
  * Builds a pipeline that enumerates all distinct `latest_apex_version`
5
5
  * values present in SAM documents (ignores per-analysis versions).
@@ -1,5 +1,5 @@
1
1
  import { type AggregationPipeline } from '../../../common/aggregation-pipeline.js';
2
- import { type Sam, type SamTimelineSummary } from '@tmlmobilidade/types';
2
+ import { type Sam, type SamTimelineSummary } from '@tmlmobilidade/go-types-operation';
3
3
  /**
4
4
  * `timeline_summary` on SAM list responses: months with `key`, `month`, `count`,
5
5
  * `successful_count`, `failed_count`, plus optional `undated` (computed from `analysis`).
@@ -1,5 +1,5 @@
1
1
  import { type AggregationPipeline } from '../../../common/aggregation-pipeline.js';
2
- import { type Sam } from '@tmlmobilidade/types';
2
+ import { type Sam } from '@tmlmobilidade/go-types-operation';
3
3
  /**
4
4
  * Optional per-analysis filters after `$unwind` (e.g. export file `start_time` / `end_time`).
5
5
  */
@@ -1,7 +1 @@
1
- import { type GetSamsBatchQuery } from '@tmlmobilidade/types';
2
- /**
3
- * Builds `$match` AND clauses for SAM list / export from a parsed batch query (same semantics as the controller list).
4
- */
5
- export declare function buildSamsMatch(parsedQuery: GetSamsBatchQuery, options?: {
6
- includeApexVersionFilter?: boolean;
7
- }): Record<string, unknown>[];
1
+ export {};
@@ -1,87 +1,88 @@
1
- /* * */
2
- import { parseSamsDeviceSearch, parseSamsVehicleSearch } from './regex-search.js';
3
- import { PermissionCatalog } from '@tmlmobilidade/types';
4
- /* * */
5
- /**
6
- * Escapes a regex string.
7
- */
8
- function escapeRegex(value) {
9
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
- }
11
- /**
12
- * Reserved query fields that are not used in the aggregation pipeline.
13
- */
14
- const RESERVED_QUERY_FIELDS = new Set(['agency_ids', 'search']);
15
- const RANGE_QUERY_FIELDS = {
16
- seen_first_at: { field: 'seen_first_at', operator: '$gte' },
17
- seen_last_at: { field: 'seen_last_at', operator: '$lte' },
18
- };
19
- /**
20
- * Builds `$match` AND clauses for SAM list / export from a parsed batch query (same semantics as the controller list).
21
- */
22
- export function buildSamsMatch(parsedQuery, options = {}) {
23
- const { includeApexVersionFilter = true } = options;
24
- const matchAnd = [];
25
- const agencyIdsForMatch = parsedQuery.agency_ids.filter(id => id !== PermissionCatalog.ALLOW_ALL_FLAG);
26
- if (agencyIdsForMatch.length > 0) {
27
- matchAnd.push({ agency_id: { $in: agencyIdsForMatch } });
28
- }
29
- const searchRaw = parsedQuery.search?.trim() ?? '';
30
- if (searchRaw.length > 0) {
31
- const vehicleIds = parseSamsVehicleSearch(searchRaw);
32
- if (vehicleIds.length > 0) {
33
- matchAnd.push({
34
- $or: [
35
- { vehicle_id: { $in: vehicleIds } },
36
- { 'analysis.vehicle_id': { $in: vehicleIds } },
37
- ],
38
- });
39
- }
40
- else {
41
- const deviceIds = parseSamsDeviceSearch(searchRaw);
42
- if (deviceIds.length > 0) {
43
- matchAnd.push({
44
- $or: [
45
- { device_id: { $in: deviceIds } },
46
- { 'analysis.device_id': { $in: deviceIds } },
47
- ],
48
- });
49
- }
50
- else {
51
- const escaped = escapeRegex(searchRaw);
52
- matchAnd.push({
53
- $or: [
54
- { $expr: { $regexMatch: { input: { $toString: '$_id' }, options: 'i', regex: escaped } } },
55
- { agency_id: { $options: 'i', $regex: escaped } },
56
- { 'analysis.first_transaction_id': { $options: 'i', $regex: escaped } },
57
- { 'analysis.last_transaction_id': { $options: 'i', $regex: escaped } },
58
- ],
59
- });
60
- }
61
- }
62
- }
63
- for (const [key, value] of Object.entries(parsedQuery)) {
64
- if (RESERVED_QUERY_FIELDS.has(key) || value === undefined || value === null || value === '')
65
- continue;
66
- if (!includeApexVersionFilter && key === 'latest_apex_version')
67
- continue;
68
- if (key in RANGE_QUERY_FIELDS) {
69
- const rangeKey = key;
70
- const rangeConfig = RANGE_QUERY_FIELDS[rangeKey];
71
- matchAnd.push({ [rangeConfig.field]: { [rangeConfig.operator]: value } });
72
- continue;
73
- }
74
- if (Array.isArray(value)) {
75
- if (value.length > 0) {
76
- if (key === 'latest_apex_version') {
77
- matchAnd.push({ latest_apex_version: { $in: value } });
78
- continue;
79
- }
80
- matchAnd.push({ [key]: { $in: value } });
81
- }
82
- continue;
83
- }
84
- matchAnd.push({ [key]: value });
85
- }
86
- return matchAnd;
87
- }
1
+ // /* * */
2
+ export {};
3
+ // import { parseSamsDeviceSearch, parseSamsVehicleSearch } from './regex-search.js';
4
+ // import { PermissionCatalog } from '@tmlmobilidade/consts';
5
+ // /* * */
6
+ // /**
7
+ // * Escapes a regex string.
8
+ // */
9
+ // function escapeRegex(value: string): string {
10
+ // return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11
+ // }
12
+ // /**
13
+ // * Reserved query fields that are not used in the aggregation pipeline.
14
+ // */
15
+ // const RESERVED_QUERY_FIELDS = new Set(['agency_ids', 'search']);
16
+ // const RANGE_QUERY_FIELDS = {
17
+ // seen_first_at: { field: 'seen_first_at', operator: '$gte' },
18
+ // seen_last_at: { field: 'seen_last_at', operator: '$lte' },
19
+ // } as const;
20
+ // /**
21
+ // * Builds `$match` AND clauses for SAM list / export from a parsed batch query (same semantics as the controller list).
22
+ // */
23
+ // export function buildSamsMatch(parsedQuery: GetSamsBatchQuery, options: { includeApexVersionFilter?: boolean } = {}): Record<string, unknown>[] {
24
+ // const { includeApexVersionFilter = true } = options;
25
+ // const matchAnd: Record<string, unknown>[] = [];
26
+ // const agencyIdsForMatch = parsedQuery.agency_ids.filter(
27
+ // id => id !== PermissionCatalog.ALLOW_ALL_FLAG,
28
+ // );
29
+ // if (agencyIdsForMatch.length > 0) {
30
+ // matchAnd.push({ agency_id: { $in: agencyIdsForMatch } });
31
+ // }
32
+ // const searchRaw = parsedQuery.search?.trim() ?? '';
33
+ // if (searchRaw.length > 0) {
34
+ // const vehicleIds = parseSamsVehicleSearch(searchRaw);
35
+ // if (vehicleIds.length > 0) {
36
+ // matchAnd.push({
37
+ // $or: [
38
+ // { vehicle_id: { $in: vehicleIds } },
39
+ // { 'analysis.vehicle_id': { $in: vehicleIds } },
40
+ // ],
41
+ // });
42
+ // } else {
43
+ // const deviceIds = parseSamsDeviceSearch(searchRaw);
44
+ // if (deviceIds.length > 0) {
45
+ // matchAnd.push({
46
+ // $or: [
47
+ // { device_id: { $in: deviceIds } },
48
+ // { 'analysis.device_id': { $in: deviceIds } },
49
+ // ],
50
+ // });
51
+ // } else {
52
+ // const escaped = escapeRegex(searchRaw);
53
+ // matchAnd.push({
54
+ // $or: [
55
+ // { $expr: { $regexMatch: { input: { $toString: '$_id' }, options: 'i', regex: escaped } } },
56
+ // { agency_id: { $options: 'i', $regex: escaped } },
57
+ // { 'analysis.first_transaction_id': { $options: 'i', $regex: escaped } },
58
+ // { 'analysis.last_transaction_id': { $options: 'i', $regex: escaped } },
59
+ // ],
60
+ // });
61
+ // }
62
+ // }
63
+ // }
64
+ // for (const [key, value] of Object.entries(parsedQuery)) {
65
+ // if (RESERVED_QUERY_FIELDS.has(key) || value === undefined || value === null || value === '')
66
+ // continue;
67
+ // if (!includeApexVersionFilter && key === 'latest_apex_version')
68
+ // continue;
69
+ // if (key in RANGE_QUERY_FIELDS) {
70
+ // const rangeKey = key as keyof typeof RANGE_QUERY_FIELDS;
71
+ // const rangeConfig = RANGE_QUERY_FIELDS[rangeKey];
72
+ // matchAnd.push({ [rangeConfig.field]: { [rangeConfig.operator]: value } });
73
+ // continue;
74
+ // }
75
+ // if (Array.isArray(value)) {
76
+ // if (value.length > 0) {
77
+ // if (key === 'latest_apex_version') {
78
+ // matchAnd.push({ latest_apex_version: { $in: value } });
79
+ // continue;
80
+ // }
81
+ // matchAnd.push({ [key]: { $in: value } });
82
+ // }
83
+ // continue;
84
+ // }
85
+ // matchAnd.push({ [key]: value });
86
+ // }
87
+ // return matchAnd;
88
+ // }
@@ -1,4 +1,3 @@
1
1
  export * from './aggregations/index.js';
2
- export * from './build-sams-match.js';
3
2
  export * from './regex-search.js';
4
3
  export * from './sams.js';
@@ -1,4 +1,3 @@
1
1
  export * from './aggregations/index.js';
2
- export * from './build-sams-match.js';
3
2
  export * from './regex-search.js';
4
3
  export * from './sams.js';
@@ -1,5 +1,5 @@
1
1
  import { MongoCollectionClass } from '../../common/mongo-collection.js';
2
- import { type CreateSamDto, type Sam, type UpdateSamDto } from '@tmlmobilidade/types';
2
+ import { type CreateSamDto, type Sam, type UpdateSamDto } from '@tmlmobilidade/go-types-operation';
3
3
  import { IndexDescription } from 'mongodb';
4
4
  import { z } from 'zod';
5
5
  declare class SamsClass extends MongoCollectionClass<Sam, CreateSamDto, UpdateSamDto> {
@@ -1,6 +1,6 @@
1
1
  /* * */
2
2
  import { MongoCollectionClass } from '../../common/mongo-collection.js';
3
- import { CreateSamSchema, UpdateSamSchema } from '@tmlmobilidade/types';
3
+ import { CreateSamSchema, UpdateSamSchema } from '@tmlmobilidade/go-types-operation';
4
4
  import { asyncSingletonProxy } from '@tmlmobilidade/utils';
5
5
  /* * */
6
6
  class SamsClass extends MongoCollectionClass {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/interfaces",
3
- "version": "20260811.1505.45",
3
+ "version": "20260828.1636.54",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"
@@ -37,14 +37,17 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@tmlmobilidade/consts": "*",
40
- "@tmlmobilidade/dates": "*",
41
40
  "@tmlmobilidade/files": "*",
41
+ "@tmlmobilidade/go-clients-ssh": "*",
42
42
  "@tmlmobilidade/go-interfaces-godb": "*",
43
43
  "@tmlmobilidade/go-types-apex": "*",
44
+ "@tmlmobilidade/go-types-core": "*",
45
+ "@tmlmobilidade/go-types-operation": "*",
46
+ "@tmlmobilidade/go-types-performance": "*",
44
47
  "@tmlmobilidade/go-types-vehicle-events": "*",
48
+ "@tmlmobilidade/go-utils-dates": "*",
45
49
  "@tmlmobilidade/logger": "*",
46
50
  "@tmlmobilidade/mongo": "*",
47
- "@tmlmobilidade/ssh": "*",
48
51
  "@tmlmobilidade/strings": "*",
49
52
  "@tmlmobilidade/utils": "*",
50
53
  "@tmlmobilidade/writers": "*",
@@ -55,8 +58,7 @@
55
58
  "zod": "3.25.76"
56
59
  },
57
60
  "devDependencies": {
58
- "@tmlmobilidade/tsconfig": "*",
59
- "@tmlmobilidade/types": "*",
61
+ "@tmlmobilidade/go-utils-tsconfig": "*",
60
62
  "@types/node": "26.1.2",
61
63
  "resolve-tspaths": "0.8.23",
62
64
  "tsc-watch": "7.2.1",
@@ -1 +0,0 @@
1
- export * from './notifications.js';
@@ -1 +0,0 @@
1
- export * from './notifications.js';
@@ -1,26 +0,0 @@
1
- import { MongoCollectionClass } from '../../common/mongo-collection.js';
2
- import { type CreateNotificationDto, type Notification, UpdateNotificationDto, User } from '@tmlmobilidade/types';
3
- import { IndexDescription } from 'mongodb';
4
- import { z } from 'zod';
5
- declare class NotificationsClass extends MongoCollectionClass<Notification, CreateNotificationDto, UpdateNotificationDto> {
6
- private static _instance;
7
- protected createSchema: z.ZodSchema;
8
- protected updateSchema: z.ZodSchema;
9
- private constructor();
10
- static getInstance(): Promise<NotificationsClass>;
11
- sendNotification(scope: string, topic: string, user: User, id: string, title: string, description: string): Promise<void>;
12
- protected getCollectionIndexes(): IndexDescription[];
13
- protected getCollectionName(): string;
14
- protected getEnvName(): string;
15
- /**
16
- * Collects all effective permissions of a user (direct + via roles),
17
- * merging duplicates by (scope:action).
18
- */
19
- private collectUserPermissions;
20
- /**
21
- * Determines whether a user can receive email notifications for a topic.
22
- */
23
- private getNotificationPermission;
24
- }
25
- export declare const notifications: NotificationsClass;
26
- export {};
@@ -1,107 +0,0 @@
1
- /* * */
2
- import { MongoCollectionClass } from '../../common/mongo-collection.js';
3
- import { getModuleConfig } from '@tmlmobilidade/consts';
4
- import { goDb } from '@tmlmobilidade/go-interfaces-godb';
5
- import { CreateNotificationSchema, UpdateNotificationSchema } from '@tmlmobilidade/types';
6
- import { asyncSingletonProxy, mergeObjects } from '@tmlmobilidade/utils';
7
- /* * */
8
- class NotificationsClass extends MongoCollectionClass {
9
- static _instance;
10
- createSchema = CreateNotificationSchema;
11
- updateSchema = UpdateNotificationSchema;
12
- constructor() {
13
- super();
14
- }
15
- static async getInstance() {
16
- if (!NotificationsClass._instance) {
17
- const instance = new NotificationsClass();
18
- await instance.connect();
19
- NotificationsClass._instance = instance;
20
- }
21
- return NotificationsClass._instance;
22
- }
23
- async sendNotification(scope, topic, user, id, title, description) {
24
- // Fetch roles and users that have access to this topic
25
- const rolesWithTopic = await goDb.core.roles.findMany({ 'permissions.action': topic });
26
- const roleIdsWithTopic = rolesWithTopic.map(r => r._id);
27
- const usersWithTopic = await goDb.core.users.findMany({
28
- $or: [
29
- { 'permissions.action': topic },
30
- { role_ids: { $in: roleIdsWithTopic } },
31
- ],
32
- });
33
- if (usersWithTopic.length === 0)
34
- return;
35
- // Base notification template
36
- const baseNotification = {
37
- created_by: user?._id,
38
- is_read: false,
39
- payload: {
40
- body: description,
41
- href: `${getModuleConfig(scope, 'frontend_url')}/${scope}/${id}`,
42
- icon: scope,
43
- title,
44
- },
45
- priority: 'normal',
46
- scope,
47
- topic,
48
- updated_by: user?._id,
49
- };
50
- // Iterate over eligible users (excluding creator)
51
- for (const recipient of usersWithTopic.filter(u => u._id !== baseNotification.created_by)) {
52
- const permissions = this.collectUserPermissions(recipient, rolesWithTopic);
53
- const canReceiveEmail = this.getNotificationPermission(permissions, topic);
54
- const newNotification = { ...baseNotification, user_id: recipient._id };
55
- const result = await notifications.insertOne(newNotification);
56
- // // Send email if permission allows
57
- // if (canReceiveEmail) {
58
- // await sendGenericNotificationEmail({
59
- // data: {
60
- // body: result.payload.body,
61
- // notificationId: result._id,
62
- // notificationUrl: result.payload.href ?? '',
63
- // title: result.payload.title,
64
- // },
65
- // to: recipient.email,
66
- // });
67
- // }
68
- }
69
- }
70
- getCollectionIndexes() {
71
- return [
72
- { background: true, key: { name: 1 }, unique: true },
73
- ];
74
- }
75
- getCollectionName() {
76
- return 'notifications';
77
- }
78
- getEnvName() {
79
- return 'DATABASE_URI';
80
- }
81
- /**
82
- * Collects all effective permissions of a user (direct + via roles),
83
- * merging duplicates by (scope:action).
84
- */
85
- collectUserPermissions(user, rolesWithTopic) {
86
- const rolePermissions = rolesWithTopic
87
- .filter(role => user.role_ids?.includes(role._id))
88
- .flatMap(role => role.permissions ?? []);
89
- const allPermissions = [...rolePermissions, ...(user.permissions ?? [])];
90
- const map = new Map();
91
- for (const permission of allPermissions) {
92
- const key = `${permission.scope}:${permission.action}`;
93
- const existing = map.get(key);
94
- map.set(key, existing ? mergeObjects(existing, permission) : permission);
95
- }
96
- return map;
97
- }
98
- /**
99
- * Determines whether a user can receive email notifications for a topic.
100
- */
101
- getNotificationPermission(permissions, topic) {
102
- const permission = permissions.get(`notifications:${topic}`);
103
- return permission['resource']?.send_mail ?? false;
104
- }
105
- }
106
- /* * */
107
- export const notifications = asyncSingletonProxy(NotificationsClass);
@@ -1 +0,0 @@
1
- export * from './proposed-changes.js';
@@ -1 +0,0 @@
1
- export * from './proposed-changes.js';
@@ -1,16 +0,0 @@
1
- import { MongoCollectionClass } from '../../common/mongo-collection.js';
2
- import { CreateProposedChangeDto, ProposedChange, UpdateProposedChangeDto } from '@tmlmobilidade/types';
3
- import { IndexDescription } from 'mongodb';
4
- import { z } from 'zod';
5
- declare class ProposedChangesClass extends MongoCollectionClass<ProposedChange<any>, CreateProposedChangeDto<any>, UpdateProposedChangeDto<any>> {
6
- private static _instances;
7
- protected createSchema: z.ZodSchema;
8
- protected updateSchema: z.ZodSchema;
9
- private constructor();
10
- static getInstance(typeName?: string): Promise<ProposedChangesClass>;
11
- protected getCollectionIndexes(): IndexDescription[];
12
- protected getCollectionName(): string;
13
- protected getEnvName(): string;
14
- }
15
- export declare const proposedChanges: ProposedChangesClass;
16
- export {};
@@ -1,34 +0,0 @@
1
- // /* * */
2
- import { MongoCollectionClass } from '../../common/mongo-collection.js';
3
- import { ProposedChangeSchema, UpdateProposedChangeSchema } from '@tmlmobilidade/types';
4
- import { asyncSingletonProxy } from '@tmlmobilidade/utils';
5
- /* * */
6
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- class ProposedChangesClass extends MongoCollectionClass {
8
- static _instances = new Map();
9
- createSchema = ProposedChangeSchema;
10
- updateSchema = UpdateProposedChangeSchema;
11
- constructor() {
12
- super();
13
- }
14
- static async getInstance(typeName) {
15
- const key = typeName ?? 'default';
16
- if (!this._instances.has(key)) {
17
- const instance = new ProposedChangesClass();
18
- await instance.connect();
19
- this._instances.set(key, instance);
20
- }
21
- return this._instances.get(key);
22
- }
23
- getCollectionIndexes() {
24
- return [{ background: true, key: { name: 1 } }];
25
- }
26
- getCollectionName() {
27
- return 'proposed_changes';
28
- }
29
- getEnvName() {
30
- return 'DATABASE_URI';
31
- }
32
- }
33
- /* * */
34
- export const proposedChanges = asyncSingletonProxy(ProposedChangesClass);
@@ -1 +0,0 @@
1
- export * from './pipelines.js';
@@ -1 +0,0 @@
1
- export * from './pipelines.js';
@@ -1,124 +0,0 @@
1
- import { AggregationPipeline } from '../../common/aggregation-pipeline.js';
2
- import { DelayStatus, OneOrTheOther, OperationalDate, OperationalStatus, Ride, RideAcceptanceStatus, RideAnalysisGradeWithNone, SeenStatus, TicketingStatus, UnixTimestamp } from '@tmlmobilidade/types';
3
- /**
4
- * Creates MongoDB aggregation pipeline stages to calculate and categorize delay statuses.
5
- *
6
- * This function generates three aggregation stages:
7
- * 1. Calculates delay differences (in milliseconds) between scheduled and observed times
8
- * 2. Categorizes delays into statuses: 'delayed', 'early', 'ontime', or 'none'
9
- * 3. Removes intermediate calculation fields
10
- *
11
- * Delay thresholds:
12
- * - Delayed: > 5 minutes (300000 ms)
13
- * - Early: < -1 minute (-60000 ms)
14
- * - On-time: between -1 minute and 5 minutes
15
- * - None: missing scheduled or observed time data
16
- *
17
- * @returns {Array} Array of MongoDB aggregation pipeline stages
18
- */
19
- export declare function ridesPipelineDelayStatus({ filter }?: {
20
- filter?: {
21
- end_delay_status?: DelayStatus[];
22
- start_delay_status?: DelayStatus[];
23
- };
24
- }): AggregationPipeline<Ride>;
25
- /**
26
- * Creates MongoDB aggregation pipeline stages to calculate and categorize operational statuses.
27
- *
28
- * This function generates four aggregation stages:
29
- * 1. Adds the current timestamp (now) to each document
30
- * 2. Calculates time differences from last seen and from start time
31
- * 3. Categorizes operational status: 'scheduled', 'missed', 'running', or 'ended'
32
- * 4. Removes intermediate calculation fields
33
- *
34
- * Operational status logic:
35
- * - Scheduled: within 10 minutes of start time and never seen
36
- * - Missed: more than 10 minutes after start time and never seen
37
- * - Running: last seen within 10 minutes
38
- * - Ended: default fallback (last seen more than 10 minutes ago)
39
- *
40
- * Time thresholds:
41
- * - Operational window: 10 minutes (600000 ms)
42
- *
43
- * @param {number} now - Current timestamp in milliseconds
44
- * @returns {Array} Array of MongoDB aggregation pipeline stages
45
- */
46
- export declare function ridesPipelineOperationalStatus({ filter }?: {
47
- filter?: {
48
- operational_status?: OperationalStatus[];
49
- };
50
- }): AggregationPipeline<Ride>;
51
- /**
52
- * Creates MongoDB aggregation pipeline stages to calculate and categorize seen statuses.
53
- *
54
- * This function generates three aggregation stages:
55
- * 1. Adds the current timestamp (now) to each document
56
- * 2. Calculates time difference from last seen to now
57
- * 3. Categorizes seen status: 'gone', 'seen', or 'unseen'
58
- * 4. Removes intermediate calculation fields
59
- *
60
- * Seen status logic:
61
- * - Gone: last seen more than 30 seconds ago
62
- * - Seen: last seen within 30 seconds
63
- * - Unseen: no last seen time
64
- *
65
- * Time thresholds:
66
- * - Seen window: 30 seconds (30000 ms)
67
- *
68
- * @param {number} now - Current timestamp in milliseconds
69
- * @returns {Array} Array of MongoDB aggregation pipeline stages
70
- */
71
- export declare function ridesPipelineSeenStatus({ filter }?: {
72
- filter?: {
73
- seen_status?: SeenStatus[];
74
- };
75
- }): AggregationPipeline<Ride>;
76
- export declare function ridesPipelineTicketingStatus({ filter }?: {
77
- filter?: {
78
- ticketing_status?: TicketingStatus[];
79
- };
80
- }): AggregationPipeline<Ride>;
81
- interface DatesRange {
82
- date_end: UnixTimestamp;
83
- date_start: UnixTimestamp;
84
- }
85
- interface OperationalDateRange {
86
- operational_date_end: OperationalDate;
87
- operational_date_start: OperationalDate;
88
- }
89
- type RidesPipelineFilter = OneOrTheOther<DatesRange, OperationalDateRange> & {
90
- acceptance_status?: ('none' | RideAcceptanceStatus)[];
91
- agency_ids?: string[];
92
- analysis_ended_at_last_stop_grade?: RideAnalysisGradeWithNone[];
93
- analysis_expected_apex_validation_interval?: RideAnalysisGradeWithNone[];
94
- analysis_simple_three_vehicle_events_grade?: RideAnalysisGradeWithNone[];
95
- analysis_transaction_sequentiality?: RideAnalysisGradeWithNone[];
96
- delay_statuses?: DelayStatus[];
97
- line_ids?: string[];
98
- operational_statuses?: OperationalStatus[];
99
- pattern_ids?: string[];
100
- search?: string;
101
- seen_statuses?: SeenStatus[];
102
- stop_ids?: string[];
103
- ticketing_status?: TicketingStatus[];
104
- vehicle_ids?: string[];
105
- };
106
- /**
107
- * Creates MongoDB aggregation pipeline stages to filter and process ride data.
108
- *
109
- * This function generates an aggregation pipeline with the following stages:
110
- * 1. Filters rides by scheduled time range (date_start to date_end)
111
- * 2. Optionally filters by line IDs
112
- * 3. Optionally filters by agency IDs
113
- * 4. Optionally searches rides by ride ID using regex pattern matching and, if present, by vehicle IDs in the search string (with support for "v:1117,1118" etc.)
114
- * 5. Adds acceptance status by joining from the ride_acceptances collection
115
- * 6. Filters by analysis grades (ended_at_last_stop, expected_apex_validation_interval, simple_three_vehicle_events)
116
- * 7. Filters by acceptance status (excluding 'none' if present)
117
- * 8. Applies delay, operational, and seen status filters using dedicated pipeline functions
118
- *
119
- * @param {Object} params - Parameters object
120
- * @param {RidesPipelineFilter} params.filter - Filter criteria for rides
121
- * @returns {AggregationPipeline<Ride>} Array of MongoDB aggregation pipeline stages
122
- */
123
- export declare function ridesBatchAggregationPipeline({ ...filter }: RidesPipelineFilter): AggregationPipeline<Ride>;
124
- export {};
@@ -1,480 +0,0 @@
1
- import { Dates } from '@tmlmobilidade/dates';
2
- //
3
- // Time thresholds
4
- const LAST_SEEN_WINDOW = 30 * 1000; // 30 seconds
5
- const OPERATIONAL_WINDOW = 60 * 1000 * 10; // 10 minutes
6
- const DELAY_THRESHOLDS = {
7
- delayed: 5 * 60 * 1000, // 5 minutes after scheduled time
8
- early: -1 * 60 * 1000, // 1 minute before scheduled time
9
- };
10
- /**
11
- * Creates MongoDB aggregation pipeline stages to calculate and categorize delay statuses.
12
- *
13
- * This function generates three aggregation stages:
14
- * 1. Calculates delay differences (in milliseconds) between scheduled and observed times
15
- * 2. Categorizes delays into statuses: 'delayed', 'early', 'ontime', or 'none'
16
- * 3. Removes intermediate calculation fields
17
- *
18
- * Delay thresholds:
19
- * - Delayed: > 5 minutes (300000 ms)
20
- * - Early: < -1 minute (-60000 ms)
21
- * - On-time: between -1 minute and 5 minutes
22
- * - None: missing scheduled or observed time data
23
- *
24
- * @returns {Array} Array of MongoDB aggregation pipeline stages
25
- */
26
- export function ridesPipelineDelayStatus({ filter } = {}) {
27
- const pipeline = [
28
- // Stage 1: Calculate delay differences in milliseconds
29
- // Only compute if both scheduled and observed times exist
30
- {
31
- $addFields: {
32
- end_delay_diff: {
33
- $cond: {
34
- else: null,
35
- if: { $and: [
36
- { $ifNull: ['$end_time_scheduled', false] },
37
- { $ifNull: ['$end_time_observed', false] },
38
- ] },
39
- then: { $subtract: ['$end_time_observed', '$end_time_scheduled'] },
40
- },
41
- },
42
- start_delay_diff: {
43
- $cond: {
44
- else: null,
45
- if: { $and: [
46
- { $ifNull: ['$start_time_scheduled', false] },
47
- { $ifNull: ['$start_time_observed', false] },
48
- ] },
49
- then: { $subtract: ['$start_time_observed', '$start_time_scheduled'] },
50
- },
51
- },
52
- },
53
- },
54
- // Stage 2: Categorize delays into status strings
55
- // Uses switch statement to classify based on delay thresholds
56
- {
57
- $addFields: {
58
- end_delay_status: {
59
- $switch: {
60
- branches: [
61
- // Delayed: > 5 minutes (300000 ms)
62
- { case: { $gt: ['$end_delay_diff', DELAY_THRESHOLDS.delayed] }, then: 'delayed' },
63
- // Early: < -1 minute (-60000 ms)
64
- { case: { $lt: ['$end_delay_diff', DELAY_THRESHOLDS.early] }, then: 'early' },
65
- // On-time: between -1 minute and 5 minutes
66
- { case: { $and: [
67
- { $gte: ['$end_delay_diff', DELAY_THRESHOLDS.early] },
68
- { $lte: ['$end_delay_diff', DELAY_THRESHOLDS.delayed] },
69
- ] }, then: 'ontime' },
70
- ],
71
- default: 'none',
72
- },
73
- },
74
- start_delay_status: {
75
- $switch: {
76
- branches: [
77
- // Delayed: > 5 minutes (300000 ms)
78
- { case: { $gt: ['$start_delay_diff', DELAY_THRESHOLDS.delayed] }, then: 'delayed' },
79
- // Early: < -1 minute (-60000 ms)
80
- { case: { $lt: ['$start_delay_diff', DELAY_THRESHOLDS.early] }, then: 'early' },
81
- // On-time: between -1 minute and 5 minutes
82
- { case: { $and: [
83
- { $gte: ['$start_delay_diff', DELAY_THRESHOLDS.early] },
84
- { $lte: ['$start_delay_diff', DELAY_THRESHOLDS.delayed] },
85
- ] }, then: 'ontime' },
86
- ],
87
- default: 'none',
88
- },
89
- },
90
- },
91
- },
92
- // Stage 3: Remove intermediate calculation fields
93
- // These fields were only used for status calculation and are not needed in final output
94
- { $project: { end_delay_diff: 0, start_delay_diff: 0 } },
95
- ];
96
- // Stage 5: Filter by delay status if provided
97
- if (filter?.end_delay_status) {
98
- pipeline.push({ $match: { end_delay_status: { $in: filter.end_delay_status } } });
99
- }
100
- if (filter?.start_delay_status) {
101
- pipeline.push({ $match: { start_delay_status: { $in: filter.start_delay_status } } });
102
- }
103
- return pipeline;
104
- }
105
- /**
106
- * Creates MongoDB aggregation pipeline stages to calculate and categorize operational statuses.
107
- *
108
- * This function generates four aggregation stages:
109
- * 1. Adds the current timestamp (now) to each document
110
- * 2. Calculates time differences from last seen and from start time
111
- * 3. Categorizes operational status: 'scheduled', 'missed', 'running', or 'ended'
112
- * 4. Removes intermediate calculation fields
113
- *
114
- * Operational status logic:
115
- * - Scheduled: within 10 minutes of start time and never seen
116
- * - Missed: more than 10 minutes after start time and never seen
117
- * - Running: last seen within 10 minutes
118
- * - Ended: default fallback (last seen more than 10 minutes ago)
119
- *
120
- * Time thresholds:
121
- * - Operational window: 10 minutes (600000 ms)
122
- *
123
- * @param {number} now - Current timestamp in milliseconds
124
- * @returns {Array} Array of MongoDB aggregation pipeline stages
125
- */
126
- export function ridesPipelineOperationalStatus({ filter } = {}) {
127
- const now = Dates.now('Europe/Lisbon').unix_timestamp;
128
- const pipeline = [
129
- // Stage 1: Add current timestamp to each document
130
- { $addFields: { now } },
131
- // Stage 2: Calculate time differences from last seen and from start time
132
- // Only compute if the relevant fields exist
133
- {
134
- $addFields: {
135
- milliseconds_from_last_seen_to_now: {
136
- $cond: {
137
- else: null,
138
- if: { $ifNull: ['$seen_last_at', false] },
139
- then: { $subtract: ['$now', '$seen_last_at'] },
140
- },
141
- },
142
- milliseconds_from_start_to_now: { $subtract: ['$now', '$start_time_scheduled'] },
143
- },
144
- },
145
- // Stage 3: Categorize operational status using switch statement
146
- {
147
- $addFields: {
148
- operational_status: {
149
- $switch: {
150
- branches: [
151
- // Scheduled: within 10 minutes of start time and never seen
152
- {
153
- case: {
154
- $and: [
155
- { $lte: ['$milliseconds_from_start_to_now', OPERATIONAL_WINDOW] },
156
- { $or: [{ $eq: ['$seen_last_at', null] }, { $not: ['$seen_last_at'] }] },
157
- ],
158
- },
159
- then: 'scheduled',
160
- },
161
- // Missed: more than 10 minutes after start time and never seen
162
- {
163
- case: {
164
- $and: [
165
- { $gt: ['$milliseconds_from_start_to_now', OPERATIONAL_WINDOW] },
166
- { $or: [{ $eq: ['$seen_last_at', null] }, { $not: ['$seen_last_at'] }] },
167
- ],
168
- },
169
- then: 'missed',
170
- },
171
- // Running: last seen within 10 minutes
172
- {
173
- case: { $lte: ['$milliseconds_from_last_seen_to_now', OPERATIONAL_WINDOW] },
174
- then: 'running',
175
- },
176
- ],
177
- default: 'ended',
178
- },
179
- },
180
- },
181
- },
182
- // Stage 4: Remove intermediate calculation fields
183
- // These fields were only used for status calculation and are not needed in final output
184
- {
185
- $project: {
186
- milliseconds_from_last_seen_to_now: 0,
187
- milliseconds_from_start_to_now: 0,
188
- now: 0,
189
- },
190
- },
191
- ];
192
- // Stage 5: Filter by operational status if provided
193
- if (filter?.operational_status) {
194
- pipeline.push({ $match: { operational_status: { $in: filter.operational_status } } });
195
- }
196
- return pipeline;
197
- }
198
- /**
199
- * Creates MongoDB aggregation pipeline stages to calculate and categorize seen statuses.
200
- *
201
- * This function generates three aggregation stages:
202
- * 1. Adds the current timestamp (now) to each document
203
- * 2. Calculates time difference from last seen to now
204
- * 3. Categorizes seen status: 'gone', 'seen', or 'unseen'
205
- * 4. Removes intermediate calculation fields
206
- *
207
- * Seen status logic:
208
- * - Gone: last seen more than 30 seconds ago
209
- * - Seen: last seen within 30 seconds
210
- * - Unseen: no last seen time
211
- *
212
- * Time thresholds:
213
- * - Seen window: 30 seconds (30000 ms)
214
- *
215
- * @param {number} now - Current timestamp in milliseconds
216
- * @returns {Array} Array of MongoDB aggregation pipeline stages
217
- */
218
- export function ridesPipelineSeenStatus({ filter } = {}) {
219
- const now = Dates.now('Europe/Lisbon').unix_timestamp;
220
- const pipeline = [
221
- // Stage 1: Add current timestamp to each document
222
- { $addFields: { now } },
223
- // Stage 2: Calculate time difference from last seen to now
224
- // Only compute if the last seen time exists
225
- {
226
- $addFields: {
227
- milliseconds_from_last_seen_to_now: {
228
- $cond: {
229
- else: null,
230
- if: { $ifNull: ['$seen_last_at', false] },
231
- then: { $subtract: ['$now', '$seen_last_at'] },
232
- },
233
- },
234
- },
235
- },
236
- // Stage 3: Categorize seen status using switch statement
237
- {
238
- $addFields: {
239
- seen_status: {
240
- $switch: {
241
- branches: [
242
- { case: { $eq: ['$seen_last_at', null] }, then: 'unseen' },
243
- { case: { $lte: ['$milliseconds_from_last_seen_to_now', LAST_SEEN_WINDOW] }, then: 'seen' },
244
- ],
245
- default: 'gone',
246
- },
247
- },
248
- },
249
- },
250
- // Stage 4: Remove intermediate calculation fields
251
- // These fields were only used for status calculation and are not needed in final output
252
- {
253
- $project: {
254
- milliseconds_from_last_seen_to_now: 0,
255
- now: 0,
256
- },
257
- },
258
- ];
259
- // Stage 5: Filter by seen status if provided
260
- if (filter?.seen_status) {
261
- pipeline.push({ $match: { seen_status: { $in: filter.seen_status } } });
262
- }
263
- return pipeline;
264
- }
265
- export function ridesPipelineTicketingStatus({ filter } = {}) {
266
- const pipeline = [];
267
- if (!filter?.ticketing_status?.length)
268
- return pipeline;
269
- const includesHasTicketing = filter.ticketing_status.includes('has_ticketing');
270
- const includesNoTicketing = filter.ticketing_status.includes('no_ticketing');
271
- // If both are present, match all documents (no filter needed)
272
- if (includesHasTicketing && includesNoTicketing)
273
- return pipeline;
274
- if (includesHasTicketing) {
275
- pipeline.push({ $match: { apex_validations_qty: { $gte: 1 } } });
276
- }
277
- if (includesNoTicketing) {
278
- pipeline.push({ $match: { apex_validations_qty: { $eq: 0 } } });
279
- }
280
- return pipeline;
281
- }
282
- /**
283
- * Attempts to map search term to specific indexed field based on structural
284
- * heuristics. Returns null when term does not match known pattern.
285
- */
286
- function routeTermToField(term) {
287
- // Exact pattern_id match: "1001_0_2"
288
- if (/^\d+_\d+_\d+$/.test(term)) {
289
- return { pattern_id: term };
290
- }
291
- // Wildcard trip_id: any string containing %%; %% → regex .*
292
- // NOTE: case-sensitive (no $options: 'i') so anchored prefix regex can use
293
- // the { trip_id: 1 } / { trip_id: 1, start_time_scheduled: 1 } index.
294
- if (term.includes('%%')) {
295
- let escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
296
- escaped = escaped.replace(/%%/g, '.*');
297
- return { trip_id: { $regex: `^${escaped}$` } };
298
- }
299
- // route_id: "1001_0"
300
- if (/^\d+_\d+$/.test(term)) {
301
- return { route_id: term };
302
- }
303
- // operational_date: "20240101"
304
- if (/^\d{8}$/.test(term)) {
305
- return { operational_date: term };
306
- }
307
- // line_id: pure integer string like "1001"
308
- // agency_id also numeric, so use length heuristic
309
- if (/^\d+$/.test(term)) {
310
- const n = Number(term);
311
- if (term.length >= 3)
312
- return { line_id: n };
313
- }
314
- return null;
315
- }
316
- function buildSearchPipeline(filter) {
317
- const pipeline = [];
318
- if (!filter.search)
319
- return pipeline;
320
- const vehicleMatch = filter.search.match(/v:([\d,]+)/);
321
- const driverMatch = filter.search.match(/d:([\d,]+)/);
322
- const rawSearch = filter.search
323
- .replace(/v:[\d,]+/g, '')
324
- .replace(/d:[\d,]+/g, '')
325
- .trim();
326
- if (rawSearch.length > 0) {
327
- const terms = rawSearch
328
- .split(/\s+/)
329
- .filter(k => k.length > 0);
330
- const conditions = [];
331
- const unroutableTerms = [];
332
- const routedFields = new Set();
333
- for (const term of terms) {
334
- const routed = routeTermToField(term);
335
- if (routed) {
336
- const [field] = Object.keys(routed);
337
- if (routedFields.has(field)) {
338
- unroutableTerms.push(term);
339
- continue;
340
- }
341
- routedFields.add(field);
342
- conditions.push(routed);
343
- }
344
- else {
345
- unroutableTerms.push(term);
346
- }
347
- }
348
- if (unroutableTerms.length > 0) {
349
- // Anchor first term as prefix on trip_id so the query can use the
350
- // { trip_id: 1, start_time_scheduled: 1 } compound index.
351
- // Remaining terms become plain contains-regex (no index, but bounded
352
- // by the prefix scan of the first term).
353
- // NOTE: case-sensitive; adding $options: 'i' disables index usage.
354
- const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
355
- const [firstTerm, ...restTerms] = unroutableTerms;
356
- conditions.push({ trip_id: { $regex: `^${escape(firstTerm)}` } });
357
- for (const rest of restTerms) {
358
- conditions.push({ trip_id: { $regex: escape(rest) } });
359
- }
360
- }
361
- if (conditions.length === 1) {
362
- pipeline.push({ $match: conditions[0] });
363
- }
364
- else if (conditions.length > 1) {
365
- pipeline.push({ $match: { $and: conditions } });
366
- }
367
- }
368
- if (vehicleMatch) {
369
- const vehicleIDs = vehicleMatch[1]
370
- .split(',')
371
- .map(id => id.trim())
372
- .filter(Boolean);
373
- if (vehicleIDs.length > 0) {
374
- pipeline.push({ $match: { vehicle_ids: { $in: vehicleIDs } } });
375
- }
376
- }
377
- if (driverMatch) {
378
- const driverIDs = driverMatch[1]
379
- .split(',')
380
- .map(id => id.trim())
381
- .filter(Boolean);
382
- if (driverIDs.length > 0) {
383
- pipeline.push({ $match: { driver_ids: { $in: driverIDs } } });
384
- }
385
- }
386
- return pipeline;
387
- }
388
- /**
389
- * Creates MongoDB aggregation pipeline stages to filter and process ride data.
390
- *
391
- * This function generates an aggregation pipeline with the following stages:
392
- * 1. Filters rides by scheduled time range (date_start to date_end)
393
- * 2. Optionally filters by line IDs
394
- * 3. Optionally filters by agency IDs
395
- * 4. Optionally searches rides by ride ID using regex pattern matching and, if present, by vehicle IDs in the search string (with support for "v:1117,1118" etc.)
396
- * 5. Adds acceptance status by joining from the ride_acceptances collection
397
- * 6. Filters by analysis grades (ended_at_last_stop, expected_apex_validation_interval, simple_three_vehicle_events)
398
- * 7. Filters by acceptance status (excluding 'none' if present)
399
- * 8. Applies delay, operational, and seen status filters using dedicated pipeline functions
400
- *
401
- * @param {Object} params - Parameters object
402
- * @param {RidesPipelineFilter} params.filter - Filter criteria for rides
403
- * @returns {AggregationPipeline<Ride>} Array of MongoDB aggregation pipeline stages
404
- */
405
- export function ridesBatchAggregationPipeline({ ...filter }) {
406
- const pipeline = [];
407
- // Stage 1: Filter by scheduled time range (required)
408
- if ('date_start' in filter && 'date_end' in filter) {
409
- pipeline.push({ $match: { start_time_scheduled: { $gte: filter.date_start, $lte: filter.date_end } } });
410
- }
411
- else if ('operational_date_start' in filter && 'operational_date_end' in filter) {
412
- pipeline.push({ $match: { operational_date: { $gte: filter.operational_date_start, $lte: filter.operational_date_end } } });
413
- }
414
- else {
415
- throw new Error('Either date_start and date_end or operational_date_start and operational_date_end must be provided');
416
- }
417
- // Stage 2: Filter by agency IDs (required)
418
- pipeline.push({ $match: { agency_id: { $in: filter.agency_ids ?? [] } } });
419
- // Stage 2.1: Sort by start_time_scheduled
420
- pipeline.push({ $sort: { start_time_scheduled: 1 } });
421
- // Stage 3: Filter by line IDs if provided
422
- if (filter.line_ids?.length)
423
- pipeline.push({ $match: { line_id: { $in: filter.line_ids } } });
424
- // Stage 4: Search by term routing and selective fallback regex
425
- pipeline.push(...buildSearchPipeline(filter));
426
- // Stage 5: Search by vehicle IDs from search field if provided (legacy support)
427
- // This allows filtering via search field with "v:1117" or "v:1117,1118" format
428
- // Remove literal 'v:' from the search string since it is breaking stuff
429
- // Stage 6: Add acceptance status from ride_acceptances collection
430
- // Lookup joins acceptance data, unwinds to flatten, adds status field, and removes intermediate data
431
- pipeline.push({ $lookup: { as: 'acceptance', foreignField: 'ride_id', from: 'ride_acceptances', localField: '_id' } }, { $unwind: { path: '$acceptance', preserveNullAndEmptyArrays: true } }, { $addFields: { acceptance_status: '$acceptance.acceptance_status' } }, { $project: { acceptance: 0 } });
432
- // Stage 7: Filter by analysis grades
433
- // Maps filter fields to their corresponding analysis paths in the document
434
- const analysisFilters = [
435
- { field: 'analysis_ended_at_last_stop_grade', path: 'analysis.ENDED_AT_LAST_STOP.grade' },
436
- { field: 'analysis_expected_apex_validation_interval', path: 'analysis.EXPECTED_APEX_VALIDATION_INTERVAL.grade' },
437
- { field: 'analysis_simple_three_vehicle_events_grade', path: 'analysis.SIMPLE_THREE_VEHICLE_EVENTS.grade' },
438
- { field: 'analysis_transaction_sequentiality', path: 'analysis.TRANSACTION_SEQUENTIALITY.grade' },
439
- ];
440
- analysisFilters.forEach(({ field, path }) => {
441
- if (!filter[field])
442
- return;
443
- if (filter[field].includes('none')) {
444
- // When 'none' is included, set the field to 'none'
445
- // if it doesn't exist, then match on the filter array
446
- pipeline.push({
447
- $addFields: {
448
- [path]: { $ifNull: [`$${path}`, 'none'] },
449
- },
450
- });
451
- }
452
- // Match documents where the field value is in the filter array
453
- pipeline.push({
454
- $match: {
455
- [path]: { $in: filter[field] },
456
- },
457
- });
458
- });
459
- // Stage 8: Filter by acceptance status
460
- // Only applies filter if acceptance_status is provided and doesn't include 'none'
461
- if (filter.acceptance_status && filter.acceptance_status) {
462
- if (filter.acceptance_status.includes('none')) {
463
- // When 'none' is included, set the field to 'none'
464
- // if it doesn't exist, then match on the filter array
465
- pipeline.push({
466
- $addFields: {
467
- acceptance_status: { $ifNull: [`$acceptance_status`, 'none'] },
468
- },
469
- });
470
- }
471
- pipeline.push({ $match: { acceptance_status: { $exists: true } } }, { $match: { acceptance_status: { $in: filter.acceptance_status } } });
472
- }
473
- // Stage 9: Apply status filters using dedicated pipeline functions
474
- // These functions add calculated status fields and filter by them
475
- pipeline.push(...ridesPipelineDelayStatus({ filter: { end_delay_status: filter.delay_statuses, start_delay_status: filter.delay_statuses } }));
476
- pipeline.push(...ridesPipelineOperationalStatus({ filter: { operational_status: filter.operational_statuses } }));
477
- pipeline.push(...ridesPipelineSeenStatus({ filter: { seen_status: filter.seen_statuses } }));
478
- pipeline.push(...ridesPipelineTicketingStatus({ filter: { ticketing_status: filter.ticketing_status } }));
479
- return pipeline;
480
- }
@@ -1 +0,0 @@
1
- export * from './simplified-vehicle-events.js';
@@ -1 +0,0 @@
1
- export * from './simplified-vehicle-events.js';
@@ -1,17 +0,0 @@
1
- import { MongoCollectionClass } from '../../common/mongo-collection.js';
2
- import { type SimplifiedVehicleEvent } from '@tmlmobilidade/go-types-vehicle-events';
3
- import { type IndexDescription } from 'mongodb';
4
- declare class SimplifiedVehicleEventsClass extends MongoCollectionClass<SimplifiedVehicleEvent, SimplifiedVehicleEvent, SimplifiedVehicleEvent> {
5
- private static _instance;
6
- private constructor();
7
- static getInstance(): Promise<SimplifiedVehicleEventsClass>;
8
- protected getCollectionIndexes(): IndexDescription[];
9
- protected getCollectionName(): string;
10
- protected getEnvName(): string;
11
- }
12
- /**
13
- * @deprecated This collection is no longer used and will be removed in a future release.
14
- * Use `simplifiedVehicleEventsNew` instead, which is the SQL version of this collection.
15
- */
16
- export declare const simplifiedVehicleEvents: SimplifiedVehicleEventsClass;
17
- export {};
@@ -1,37 +0,0 @@
1
- /* * */
2
- import { MongoCollectionClass } from '../../common/mongo-collection.js';
3
- import { asyncSingletonProxy } from '@tmlmobilidade/utils';
4
- /* * */
5
- class SimplifiedVehicleEventsClass extends MongoCollectionClass {
6
- static _instance;
7
- constructor() {
8
- super();
9
- }
10
- static async getInstance() {
11
- if (!SimplifiedVehicleEventsClass._instance) {
12
- const instance = new SimplifiedVehicleEventsClass();
13
- await instance.connect();
14
- SimplifiedVehicleEventsClass._instance = instance;
15
- }
16
- return SimplifiedVehicleEventsClass._instance;
17
- }
18
- getCollectionIndexes() {
19
- return [
20
- { background: true, key: { received_at: 1 } },
21
- // eslint-disable-next-line perfectionist/sort-objects
22
- { background: true, key: { trip_id: 1, created_at: 1 } },
23
- ];
24
- }
25
- getCollectionName() {
26
- return 'simplified_vehicle_events';
27
- }
28
- getEnvName() {
29
- return 'DATABASE_URI';
30
- }
31
- }
32
- /* * */
33
- /**
34
- * @deprecated This collection is no longer used and will be removed in a future release.
35
- * Use `simplifiedVehicleEventsNew` instead, which is the SQL version of this collection.
36
- */
37
- export const simplifiedVehicleEvents = asyncSingletonProxy(SimplifiedVehicleEventsClass);