@tmlmobilidade/interfaces 20260817.1945.33 → 20260828.1700.4
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/dist/common/aggregation-pipeline.d.ts +1 -1
- package/dist/common/enrich-user-refs.js +1 -1
- package/dist/common/mongo-collection.d.ts +1 -1
- package/dist/common/mongo-collection.js +1 -1
- package/dist/interfaces/index.d.ts +0 -4
- package/dist/interfaces/index.js +0 -4
- package/dist/interfaces/metrics/metrics.d.ts +1 -1
- package/dist/interfaces/metrics/metrics.js +1 -1
- package/dist/interfaces/sams/aggregations/apex-versions.d.ts +1 -1
- package/dist/interfaces/sams/aggregations/batch-list.d.ts +1 -1
- package/dist/interfaces/sams/aggregations/export-analysis.d.ts +1 -1
- package/dist/interfaces/sams/build-sams-match.d.ts +1 -7
- package/dist/interfaces/sams/build-sams-match.js +88 -87
- package/dist/interfaces/sams/index.d.ts +0 -1
- package/dist/interfaces/sams/index.js +0 -1
- package/dist/interfaces/sams/sams.d.ts +1 -1
- package/dist/interfaces/sams/sams.js +1 -1
- package/package.json +7 -5
- package/dist/interfaces/notifications/index.d.ts +0 -1
- package/dist/interfaces/notifications/index.js +0 -1
- package/dist/interfaces/notifications/notifications.d.ts +0 -26
- package/dist/interfaces/notifications/notifications.js +0 -107
- package/dist/interfaces/proposed-changes/index.d.ts +0 -1
- package/dist/interfaces/proposed-changes/index.js +0 -1
- package/dist/interfaces/proposed-changes/proposed-changes.d.ts +0 -16
- package/dist/interfaces/proposed-changes/proposed-changes.js +0 -34
- package/dist/interfaces/rides/index.d.ts +0 -1
- package/dist/interfaces/rides/index.js +0 -1
- package/dist/interfaces/rides/pipelines.d.ts +0 -124
- package/dist/interfaces/rides/pipelines.js +0 -480
- package/dist/interfaces/vehicle-events/index.d.ts +0 -1
- package/dist/interfaces/vehicle-events/index.js +0 -1
- package/dist/interfaces/vehicle-events/simplified-vehicle-events.d.ts +0 -17
- package/dist/interfaces/vehicle-events/simplified-vehicle-events.js +0 -37
|
@@ -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';
|
package/dist/interfaces/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,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": "
|
|
3
|
+
"version": "20260828.1700.4",
|
|
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);
|