@tmlmobilidade/interfaces 20260513.1602.22 → 20260514.1347.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interfaces/auth/roles.d.ts +6 -6
- package/dist/interfaces/auth/users.d.ts +8 -8
- package/dist/interfaces/sams/aggregations/apex-versions.d.ts +12 -0
- package/dist/interfaces/sams/aggregations/apex-versions.js +16 -0
- package/dist/interfaces/sams/aggregations/batch-list.d.ts +44 -0
- package/dist/interfaces/sams/aggregations/batch-list.js +217 -0
- package/dist/interfaces/sams/aggregations/batch-timeline-from-analysis.d.ts +7 -0
- package/dist/interfaces/sams/aggregations/batch-timeline-from-analysis.js +217 -0
- package/dist/interfaces/sams/aggregations/export-analysis.d.ts +28 -0
- package/dist/interfaces/sams/aggregations/export-analysis.js +57 -0
- package/dist/interfaces/sams/aggregations/index.d.ts +3 -0
- package/dist/interfaces/sams/aggregations/index.js +3 -0
- package/dist/interfaces/sams/build-sams-match.d.ts +7 -0
- package/dist/interfaces/sams/build-sams-match.js +87 -0
- package/dist/interfaces/sams/index.d.ts +3 -0
- package/dist/interfaces/sams/index.js +3 -0
- package/dist/interfaces/sams/regex-search.d.ts +18 -0
- package/dist/interfaces/sams/regex-search.js +53 -0
- package/dist/interfaces/sams/sams.js +5 -0
- package/package.json +1 -1
|
@@ -33,7 +33,7 @@ declare class RolesClass extends MongoCollectionClass<Role, CreateRoleDto, Updat
|
|
|
33
33
|
};
|
|
34
34
|
} | {
|
|
35
35
|
scope: "sams";
|
|
36
|
-
action: "read";
|
|
36
|
+
action: "read" | "export";
|
|
37
37
|
} | {
|
|
38
38
|
scope: "gtfs_validations";
|
|
39
39
|
action: "create" | "read" | "lock" | "request_approval" | "update_processing_status";
|
|
@@ -60,7 +60,7 @@ declare class RolesClass extends MongoCollectionClass<Role, CreateRoleDto, Updat
|
|
|
60
60
|
action: "create" | "update" | "delete" | "read" | "lock";
|
|
61
61
|
} | {
|
|
62
62
|
scope: "stops";
|
|
63
|
-
action: "create" | "update" | "delete" | "read" | "lock";
|
|
63
|
+
action: "create" | "update" | "delete" | "read" | "lock" | "edit_coordinates";
|
|
64
64
|
resources: {
|
|
65
65
|
municipality_ids: string[];
|
|
66
66
|
agency_ids: string[];
|
|
@@ -163,7 +163,7 @@ declare class RolesClass extends MongoCollectionClass<Role, CreateRoleDto, Updat
|
|
|
163
163
|
};
|
|
164
164
|
} | {
|
|
165
165
|
scope: "sams";
|
|
166
|
-
action: "read";
|
|
166
|
+
action: "read" | "export";
|
|
167
167
|
} | {
|
|
168
168
|
scope: "gtfs_validations";
|
|
169
169
|
action: "create" | "read" | "lock" | "request_approval" | "update_processing_status";
|
|
@@ -190,7 +190,7 @@ declare class RolesClass extends MongoCollectionClass<Role, CreateRoleDto, Updat
|
|
|
190
190
|
action: "create" | "update" | "delete" | "read" | "lock";
|
|
191
191
|
} | {
|
|
192
192
|
scope: "stops";
|
|
193
|
-
action: "create" | "update" | "delete" | "read" | "lock";
|
|
193
|
+
action: "create" | "update" | "delete" | "read" | "lock" | "edit_coordinates";
|
|
194
194
|
resources: {
|
|
195
195
|
municipality_ids: string[];
|
|
196
196
|
agency_ids: string[];
|
|
@@ -289,7 +289,7 @@ declare class RolesClass extends MongoCollectionClass<Role, CreateRoleDto, Updat
|
|
|
289
289
|
};
|
|
290
290
|
} | {
|
|
291
291
|
scope: "sams";
|
|
292
|
-
action: "read";
|
|
292
|
+
action: "read" | "export";
|
|
293
293
|
} | {
|
|
294
294
|
scope: "gtfs_validations";
|
|
295
295
|
action: "create" | "read" | "lock" | "request_approval" | "update_processing_status";
|
|
@@ -316,7 +316,7 @@ declare class RolesClass extends MongoCollectionClass<Role, CreateRoleDto, Updat
|
|
|
316
316
|
action: "create" | "update" | "delete" | "read" | "lock";
|
|
317
317
|
} | {
|
|
318
318
|
scope: "stops";
|
|
319
|
-
action: "create" | "update" | "delete" | "read" | "lock";
|
|
319
|
+
action: "create" | "update" | "delete" | "read" | "lock" | "edit_coordinates";
|
|
320
320
|
resources: {
|
|
321
321
|
municipality_ids: string[];
|
|
322
322
|
agency_ids: string[];
|
|
@@ -57,7 +57,7 @@ declare class UsersClass extends MongoCollectionClass<User_UNSAFE, CreateUserDto
|
|
|
57
57
|
};
|
|
58
58
|
} | {
|
|
59
59
|
scope: "sams";
|
|
60
|
-
action: "read";
|
|
60
|
+
action: "read" | "export";
|
|
61
61
|
} | {
|
|
62
62
|
scope: "gtfs_validations";
|
|
63
63
|
action: "create" | "read" | "lock" | "request_approval" | "update_processing_status";
|
|
@@ -84,7 +84,7 @@ declare class UsersClass extends MongoCollectionClass<User_UNSAFE, CreateUserDto
|
|
|
84
84
|
action: "create" | "update" | "delete" | "read" | "lock";
|
|
85
85
|
} | {
|
|
86
86
|
scope: "stops";
|
|
87
|
-
action: "create" | "update" | "delete" | "read" | "lock";
|
|
87
|
+
action: "create" | "update" | "delete" | "read" | "lock" | "edit_coordinates";
|
|
88
88
|
resources: {
|
|
89
89
|
municipality_ids: string[];
|
|
90
90
|
agency_ids: string[];
|
|
@@ -194,7 +194,7 @@ declare class UsersClass extends MongoCollectionClass<User_UNSAFE, CreateUserDto
|
|
|
194
194
|
};
|
|
195
195
|
} | {
|
|
196
196
|
scope: "sams";
|
|
197
|
-
action: "read";
|
|
197
|
+
action: "read" | "export";
|
|
198
198
|
} | {
|
|
199
199
|
scope: "gtfs_validations";
|
|
200
200
|
action: "create" | "read" | "lock" | "request_approval" | "update_processing_status";
|
|
@@ -221,7 +221,7 @@ declare class UsersClass extends MongoCollectionClass<User_UNSAFE, CreateUserDto
|
|
|
221
221
|
action: "create" | "update" | "delete" | "read" | "lock";
|
|
222
222
|
} | {
|
|
223
223
|
scope: "stops";
|
|
224
|
-
action: "create" | "update" | "delete" | "read" | "lock";
|
|
224
|
+
action: "create" | "update" | "delete" | "read" | "lock" | "edit_coordinates";
|
|
225
225
|
resources: {
|
|
226
226
|
municipality_ids: string[];
|
|
227
227
|
agency_ids: string[];
|
|
@@ -335,7 +335,7 @@ declare class UsersClass extends MongoCollectionClass<User_UNSAFE, CreateUserDto
|
|
|
335
335
|
};
|
|
336
336
|
} | {
|
|
337
337
|
scope: "sams";
|
|
338
|
-
action: "read";
|
|
338
|
+
action: "read" | "export";
|
|
339
339
|
} | {
|
|
340
340
|
scope: "gtfs_validations";
|
|
341
341
|
action: "create" | "read" | "lock" | "request_approval" | "update_processing_status";
|
|
@@ -362,7 +362,7 @@ declare class UsersClass extends MongoCollectionClass<User_UNSAFE, CreateUserDto
|
|
|
362
362
|
action: "create" | "update" | "delete" | "read" | "lock";
|
|
363
363
|
} | {
|
|
364
364
|
scope: "stops";
|
|
365
|
-
action: "create" | "update" | "delete" | "read" | "lock";
|
|
365
|
+
action: "create" | "update" | "delete" | "read" | "lock" | "edit_coordinates";
|
|
366
366
|
resources: {
|
|
367
367
|
municipality_ids: string[];
|
|
368
368
|
agency_ids: string[];
|
|
@@ -472,7 +472,7 @@ declare class UsersClass extends MongoCollectionClass<User_UNSAFE, CreateUserDto
|
|
|
472
472
|
};
|
|
473
473
|
} | {
|
|
474
474
|
scope: "sams";
|
|
475
|
-
action: "read";
|
|
475
|
+
action: "read" | "export";
|
|
476
476
|
} | {
|
|
477
477
|
scope: "gtfs_validations";
|
|
478
478
|
action: "create" | "read" | "lock" | "request_approval" | "update_processing_status";
|
|
@@ -499,7 +499,7 @@ declare class UsersClass extends MongoCollectionClass<User_UNSAFE, CreateUserDto
|
|
|
499
499
|
action: "create" | "update" | "delete" | "read" | "lock";
|
|
500
500
|
} | {
|
|
501
501
|
scope: "stops";
|
|
502
|
-
action: "create" | "update" | "delete" | "read" | "lock";
|
|
502
|
+
action: "create" | "update" | "delete" | "read" | "lock" | "edit_coordinates";
|
|
503
503
|
resources: {
|
|
504
504
|
municipality_ids: string[];
|
|
505
505
|
agency_ids: string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AggregationPipeline } from '../../../common/aggregation-pipeline.js';
|
|
2
|
+
import { type Sam } from '@tmlmobilidade/types';
|
|
3
|
+
/**
|
|
4
|
+
* Builds a pipeline that enumerates all distinct `latest_apex_version`
|
|
5
|
+
* values present in SAM documents (ignores per-analysis versions).
|
|
6
|
+
*
|
|
7
|
+
* @param matchAnd - Optional array of $match conditions (ANDed).
|
|
8
|
+
* @returns Aggregation pipeline for MongoDB.
|
|
9
|
+
*/
|
|
10
|
+
export declare function samsApexVersionsAggregationPipeline({ matchAnd, }: {
|
|
11
|
+
matchAnd: Record<string, unknown>[];
|
|
12
|
+
}): AggregationPipeline<Sam>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
/**
|
|
3
|
+
* Builds a pipeline that enumerates all distinct `latest_apex_version`
|
|
4
|
+
* values present in SAM documents (ignores per-analysis versions).
|
|
5
|
+
*
|
|
6
|
+
* @param matchAnd - Optional array of $match conditions (ANDed).
|
|
7
|
+
* @returns Aggregation pipeline for MongoDB.
|
|
8
|
+
*/
|
|
9
|
+
export function samsApexVersionsAggregationPipeline({ matchAnd, }) {
|
|
10
|
+
return [
|
|
11
|
+
...(matchAnd.length > 0 ? [{ $match: { $and: matchAnd } }] : []),
|
|
12
|
+
{ $group: { _id: '$latest_apex_version' } },
|
|
13
|
+
{ $match: { _id: { $nin: [null, ''] } } },
|
|
14
|
+
{ $sort: { _id: -1 } },
|
|
15
|
+
];
|
|
16
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type AggregationPipeline } from '../../../common/aggregation-pipeline.js';
|
|
2
|
+
import { type Sam, type SamTimelineSummary } from '@tmlmobilidade/types';
|
|
3
|
+
/**
|
|
4
|
+
* `timeline_summary` on SAM list responses: months with `key`, `month`, `count`,
|
|
5
|
+
* `successful_count`, `failed_count`, plus optional `undated` (computed from `analysis`).
|
|
6
|
+
*/
|
|
7
|
+
export type SamsBatchListTimelineSummary = SamTimelineSummary;
|
|
8
|
+
/**
|
|
9
|
+
* Builds a pipeline to return all matching SAMs for list view (no skip/limit).
|
|
10
|
+
* Excludes `analysis` from list fields; `timeline_summary` is computed from `analysis`.
|
|
11
|
+
*
|
|
12
|
+
* @param matchAnd - An array of $match conditions (ANDed).
|
|
13
|
+
* @returns Aggregation pipeline for MongoDB.
|
|
14
|
+
*/
|
|
15
|
+
export declare function samsBatchAggregationPipeline({ matchAnd, }: {
|
|
16
|
+
matchAnd: Record<string, unknown>[];
|
|
17
|
+
}): AggregationPipeline<Sam>;
|
|
18
|
+
/**
|
|
19
|
+
* Same list row shape as {@link samsBatchAggregationPipeline} for explicit `_id`s (e.g. favorites).
|
|
20
|
+
*/
|
|
21
|
+
export declare function samsByIdsListViewAggregationPipeline({ agencyIds, ids, restrictByAgency, }: {
|
|
22
|
+
agencyIds?: string[];
|
|
23
|
+
ids: number[];
|
|
24
|
+
restrictByAgency: boolean;
|
|
25
|
+
}): AggregationPipeline<Sam>;
|
|
26
|
+
/**
|
|
27
|
+
* Full SAM by id: all fields preserved; `timeline_summary` is recomputed from `analysis`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function samsByIdAggregationPipeline(id: number): AggregationPipeline<Sam>;
|
|
30
|
+
/**
|
|
31
|
+
* Returns stored timeline summary rows for explicit SAM `_id`s.
|
|
32
|
+
* Used for fast SAM list timeline hydration.
|
|
33
|
+
*/
|
|
34
|
+
export declare function samsByIdsTimelineSummaryAggregationPipeline({ agencyIds, ids, restrictByAgency, }: {
|
|
35
|
+
agencyIds?: string[];
|
|
36
|
+
ids: number[];
|
|
37
|
+
restrictByAgency: boolean;
|
|
38
|
+
}): AggregationPipeline<Sam>;
|
|
39
|
+
/**
|
|
40
|
+
* Lightweight SAM list rows without timeline data (used for fast initial list loading).
|
|
41
|
+
*/
|
|
42
|
+
export declare function samsBatchBaseAggregationPipeline({ matchAnd, }: {
|
|
43
|
+
matchAnd: Record<string, unknown>[];
|
|
44
|
+
}): AggregationPipeline<Sam>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { samsAnalysisTimelineRowsExpr, samsDetailSnapExpr, samsListViewCarryFieldsExpr, samsTimelineSummaryFromBucketsExpr, } from './batch-timeline-from-analysis.js';
|
|
3
|
+
/**
|
|
4
|
+
* List view: derive `timeline_summary` from embedded `analysis` (same shape as
|
|
5
|
+
* {@link samsTimelineSummaryFromBucketsExpr}: month + counts per bucket).
|
|
6
|
+
*/
|
|
7
|
+
function samsListFacetMergeTimelineStages() {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
$addFields: {
|
|
11
|
+
__carry: samsListViewCarryFieldsExpr(),
|
|
12
|
+
__rowCount: { $size: { $ifNull: ['$analysis', []] } },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
$facet: {
|
|
17
|
+
emptyAnalysis: [
|
|
18
|
+
{ $match: { __rowCount: 0 } },
|
|
19
|
+
{
|
|
20
|
+
$replaceRoot: {
|
|
21
|
+
newRoot: {
|
|
22
|
+
$mergeObjects: [
|
|
23
|
+
'$__carry',
|
|
24
|
+
{ timeline_summary: { months: [], undated: null } },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
fromAnalysis: [
|
|
31
|
+
{ $match: { __rowCount: { $gt: 0 } } },
|
|
32
|
+
{ $addFields: { __rows: samsAnalysisTimelineRowsExpr() } },
|
|
33
|
+
{ $unwind: '$__rows' },
|
|
34
|
+
{
|
|
35
|
+
$group: {
|
|
36
|
+
_id: { m: '$__rows.monthKey', sam: '$__carry._id' },
|
|
37
|
+
__carry: { $first: '$__carry' },
|
|
38
|
+
f: { $sum: '$__rows.failed' },
|
|
39
|
+
s: { $sum: '$__rows.successful' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
$group: {
|
|
44
|
+
_id: '$_id.sam',
|
|
45
|
+
__carry: { $first: '$__carry' },
|
|
46
|
+
buckets: { $push: { f: '$f', m: '$_id.m', s: '$s' } },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ $addFields: { timeline_summary: samsTimelineSummaryFromBucketsExpr() } },
|
|
50
|
+
{
|
|
51
|
+
$replaceRoot: {
|
|
52
|
+
newRoot: {
|
|
53
|
+
$mergeObjects: [
|
|
54
|
+
'$__carry',
|
|
55
|
+
{ timeline_summary: '$timeline_summary' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
$project: {
|
|
65
|
+
merged: { $concatArrays: ['$emptyAnalysis', '$fromAnalysis'] },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{ $unwind: '$merged' },
|
|
69
|
+
{ $replaceRoot: { newRoot: '$merged' } },
|
|
70
|
+
{ $sort: { created_at: -1 } },
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
function samsDetailFacetMergeTimelineStages() {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
$addFields: {
|
|
77
|
+
__rowCount: { $size: { $ifNull: ['$analysis', []] } },
|
|
78
|
+
__snap: samsDetailSnapExpr(),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
$facet: {
|
|
83
|
+
emptyAnalysis: [
|
|
84
|
+
{ $match: { __rowCount: 0 } },
|
|
85
|
+
{
|
|
86
|
+
$replaceRoot: {
|
|
87
|
+
newRoot: {
|
|
88
|
+
$mergeObjects: [
|
|
89
|
+
'$__snap',
|
|
90
|
+
{ timeline_summary: { months: [], undated: null } },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
fromAnalysis: [
|
|
97
|
+
{ $match: { __rowCount: { $gt: 0 } } },
|
|
98
|
+
{ $addFields: { __rows: samsAnalysisTimelineRowsExpr() } },
|
|
99
|
+
{ $unwind: '$__rows' },
|
|
100
|
+
{
|
|
101
|
+
$group: {
|
|
102
|
+
_id: { m: '$__rows.monthKey', sam: '$__snap._id' },
|
|
103
|
+
__snap: { $first: '$__snap' },
|
|
104
|
+
analysis: { $first: '$analysis' },
|
|
105
|
+
f: { $sum: '$__rows.failed' },
|
|
106
|
+
s: { $sum: '$__rows.successful' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
$group: {
|
|
111
|
+
_id: '$_id.sam',
|
|
112
|
+
__snap: { $first: '$__snap' },
|
|
113
|
+
analysis: { $first: '$analysis' },
|
|
114
|
+
buckets: { $push: { f: '$f', m: '$_id.m', s: '$s' } },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{ $addFields: { timeline_summary: samsTimelineSummaryFromBucketsExpr() } },
|
|
118
|
+
{
|
|
119
|
+
$replaceRoot: {
|
|
120
|
+
newRoot: {
|
|
121
|
+
$mergeObjects: [
|
|
122
|
+
'$__snap',
|
|
123
|
+
{ timeline_summary: '$timeline_summary' },
|
|
124
|
+
{ __analysis: '$analysis' },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
$project: {
|
|
134
|
+
merged: { $concatArrays: ['$emptyAnalysis', '$fromAnalysis'] },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{ $unwind: '$merged' },
|
|
138
|
+
{ $replaceRoot: { newRoot: '$merged' } },
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Builds a pipeline to return all matching SAMs for list view (no skip/limit).
|
|
143
|
+
* Excludes `analysis` from list fields; `timeline_summary` is computed from `analysis`.
|
|
144
|
+
*
|
|
145
|
+
* @param matchAnd - An array of $match conditions (ANDed).
|
|
146
|
+
* @returns Aggregation pipeline for MongoDB.
|
|
147
|
+
*/
|
|
148
|
+
export function samsBatchAggregationPipeline({ matchAnd, }) {
|
|
149
|
+
return [
|
|
150
|
+
...(matchAnd.length > 0 ? [{ $match: { $and: matchAnd } }] : []),
|
|
151
|
+
{ $sort: { created_at: -1 } },
|
|
152
|
+
...samsListFacetMergeTimelineStages(),
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Same list row shape as {@link samsBatchAggregationPipeline} for explicit `_id`s (e.g. favorites).
|
|
157
|
+
*/
|
|
158
|
+
export function samsByIdsListViewAggregationPipeline({ agencyIds, ids, restrictByAgency, }) {
|
|
159
|
+
const matchAnd = [{ _id: { $in: ids } }];
|
|
160
|
+
if (restrictByAgency && agencyIds?.length)
|
|
161
|
+
matchAnd.push({ agency_id: { $in: agencyIds } });
|
|
162
|
+
return [
|
|
163
|
+
{ $match: { $and: matchAnd } },
|
|
164
|
+
{ $sort: { created_at: -1 } },
|
|
165
|
+
...samsListFacetMergeTimelineStages(),
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Full SAM by id: all fields preserved; `timeline_summary` is recomputed from `analysis`.
|
|
170
|
+
*/
|
|
171
|
+
export function samsByIdAggregationPipeline(id) {
|
|
172
|
+
return [
|
|
173
|
+
{ $match: { _id: id } },
|
|
174
|
+
...samsDetailFacetMergeTimelineStages(),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Returns stored timeline summary rows for explicit SAM `_id`s.
|
|
179
|
+
* Used for fast SAM list timeline hydration.
|
|
180
|
+
*/
|
|
181
|
+
export function samsByIdsTimelineSummaryAggregationPipeline({ agencyIds, ids, restrictByAgency, }) {
|
|
182
|
+
const matchAnd = [{ _id: { $in: ids } }];
|
|
183
|
+
if (restrictByAgency && agencyIds?.length)
|
|
184
|
+
matchAnd.push({ agency_id: { $in: agencyIds } });
|
|
185
|
+
return [
|
|
186
|
+
{ $match: { $and: matchAnd } },
|
|
187
|
+
{
|
|
188
|
+
$project: {
|
|
189
|
+
_id: 1,
|
|
190
|
+
timeline_summary: { $ifNull: ['$timeline_summary', { months: [] }] },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Lightweight SAM list rows without timeline data (used for fast initial list loading).
|
|
197
|
+
*/
|
|
198
|
+
function samsListBaseProjectionStages() {
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
$project: {
|
|
202
|
+
analysis: 0,
|
|
203
|
+
timeline_summary: 0,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{ $sort: { created_at: -1 } },
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Lightweight SAM list rows without timeline data (used for fast initial list loading).
|
|
211
|
+
*/
|
|
212
|
+
export function samsBatchBaseAggregationPipeline({ matchAnd, }) {
|
|
213
|
+
return [
|
|
214
|
+
...(matchAnd.length > 0 ? [{ $match: { $and: matchAnd } }] : []),
|
|
215
|
+
...samsListBaseProjectionStages(),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** One row per analysis × month (or one undated row). Success = both transaction ids non-null. */
|
|
2
|
+
export declare function samsAnalysisTimelineRowsExpr(): Record<string, unknown>;
|
|
3
|
+
/** Build `timeline_summary` from grouped `buckets: { m, s, f }[]`. */
|
|
4
|
+
export declare function samsTimelineSummaryFromBucketsExpr(): Record<string, unknown>;
|
|
5
|
+
export declare function samsListViewCarryFieldsExpr(): Record<string, unknown>;
|
|
6
|
+
/** Full SAM snapshot for detail merge (excludes temp fields added later in the same stage). */
|
|
7
|
+
export declare function samsDetailSnapExpr(): Record<string, unknown>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
/* eslint-disable perfectionist/sort-objects -- MongoDB aggregation readability */
|
|
3
|
+
const LISBON_TZ = 'Europe/Lisbon';
|
|
4
|
+
/** One row per analysis × month (or one undated row). Success = both transaction ids non-null. */
|
|
5
|
+
export function samsAnalysisTimelineRowsExpr() {
|
|
6
|
+
return {
|
|
7
|
+
$reduce: {
|
|
8
|
+
initialValue: [],
|
|
9
|
+
in: {
|
|
10
|
+
$let: {
|
|
11
|
+
vars: {
|
|
12
|
+
acc: '$$value',
|
|
13
|
+
success: {
|
|
14
|
+
$and: [
|
|
15
|
+
{ $ne: ['$$this.first_transaction_id', null] },
|
|
16
|
+
{ $ne: ['$$this.last_transaction_id', null] },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
in: {
|
|
21
|
+
// One binding per $let: MongoDB may evaluate same `vars` object in an order
|
|
22
|
+
// where `d0`/`d1` run before `startMs`/`endMs`, causing "undefined variable".
|
|
23
|
+
$let: {
|
|
24
|
+
vars: {
|
|
25
|
+
startMs: { $ifNull: ['$$this.start_time', '$$this.end_time'] },
|
|
26
|
+
},
|
|
27
|
+
in: {
|
|
28
|
+
$let: {
|
|
29
|
+
vars: {
|
|
30
|
+
endMs: { $ifNull: ['$$this.end_time', '$$this.start_time'] },
|
|
31
|
+
},
|
|
32
|
+
in: {
|
|
33
|
+
$let: {
|
|
34
|
+
vars: {
|
|
35
|
+
failN: { $cond: ['$$success', 0, 1] },
|
|
36
|
+
succN: { $cond: ['$$success', 1, 0] },
|
|
37
|
+
},
|
|
38
|
+
in: {
|
|
39
|
+
$cond: [
|
|
40
|
+
{
|
|
41
|
+
$or: [
|
|
42
|
+
{ $eq: ['$$startMs', null] },
|
|
43
|
+
{ $eq: ['$$endMs', null] },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
$concatArrays: [
|
|
48
|
+
'$$acc',
|
|
49
|
+
[{
|
|
50
|
+
failed: '$$failN',
|
|
51
|
+
monthKey: null,
|
|
52
|
+
successful: '$$succN',
|
|
53
|
+
}],
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
$let: {
|
|
58
|
+
vars: {
|
|
59
|
+
month0: {
|
|
60
|
+
$dateTrunc: {
|
|
61
|
+
date: {
|
|
62
|
+
$toDate: {
|
|
63
|
+
$min: ['$$startMs', '$$endMs'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
unit: 'month',
|
|
67
|
+
timezone: LISBON_TZ,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
monthLast: {
|
|
71
|
+
$dateTrunc: {
|
|
72
|
+
date: {
|
|
73
|
+
$toDate: {
|
|
74
|
+
$max: ['$$startMs', '$$endMs'],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
unit: 'month',
|
|
78
|
+
timezone: LISBON_TZ,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
in: {
|
|
83
|
+
$concatArrays: [
|
|
84
|
+
'$$acc',
|
|
85
|
+
{
|
|
86
|
+
$map: {
|
|
87
|
+
as: 'idx',
|
|
88
|
+
in: {
|
|
89
|
+
$let: {
|
|
90
|
+
vars: {
|
|
91
|
+
monthStart: {
|
|
92
|
+
$dateAdd: {
|
|
93
|
+
amount: '$$idx',
|
|
94
|
+
startDate: '$$month0',
|
|
95
|
+
timezone: LISBON_TZ,
|
|
96
|
+
unit: 'month',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
in: {
|
|
101
|
+
failed: '$$failN',
|
|
102
|
+
monthKey: {
|
|
103
|
+
$dateToString: {
|
|
104
|
+
date: '$$monthStart',
|
|
105
|
+
format: '%Y-%m',
|
|
106
|
+
timezone: LISBON_TZ,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
successful: '$$succN',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
input: {
|
|
114
|
+
$range: [
|
|
115
|
+
0,
|
|
116
|
+
{
|
|
117
|
+
$max: [
|
|
118
|
+
1,
|
|
119
|
+
{
|
|
120
|
+
$add: [
|
|
121
|
+
{
|
|
122
|
+
$dateDiff: {
|
|
123
|
+
endDate: '$$monthLast',
|
|
124
|
+
startDate: '$$month0',
|
|
125
|
+
timezone: LISBON_TZ,
|
|
126
|
+
unit: 'month',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
1,
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
input: { $ifNull: ['$analysis', []] },
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Build `timeline_summary` from grouped `buckets: { m, s, f }[]`. */
|
|
157
|
+
export function samsTimelineSummaryFromBucketsExpr() {
|
|
158
|
+
return {
|
|
159
|
+
months: {
|
|
160
|
+
$map: {
|
|
161
|
+
as: 'x',
|
|
162
|
+
in: {
|
|
163
|
+
failed_count: '$$x.f',
|
|
164
|
+
month: '$$x.m',
|
|
165
|
+
successful_count: '$$x.s',
|
|
166
|
+
},
|
|
167
|
+
input: {
|
|
168
|
+
$sortArray: {
|
|
169
|
+
input: {
|
|
170
|
+
$filter: {
|
|
171
|
+
as: 'b',
|
|
172
|
+
cond: { $ne: ['$$b.m', null] },
|
|
173
|
+
input: '$buckets',
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
sortBy: { m: 1 },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function samsListViewCarryFieldsExpr() {
|
|
184
|
+
return {
|
|
185
|
+
_id: '$_id',
|
|
186
|
+
agency_id: '$agency_id',
|
|
187
|
+
created_at: '$created_at',
|
|
188
|
+
latest_apex_version: '$latest_apex_version',
|
|
189
|
+
remarks: '$remarks',
|
|
190
|
+
seen_first_at: { $ifNull: ['$seen_first_at', null] },
|
|
191
|
+
seen_last_at: { $ifNull: ['$seen_last_at', null] },
|
|
192
|
+
system_status: '$system_status',
|
|
193
|
+
transactions_expected: '$transactions_expected',
|
|
194
|
+
transactions_found: '$transactions_found',
|
|
195
|
+
transactions_missing: '$transactions_missing',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Full SAM snapshot for detail merge (excludes temp fields added later in the same stage). */
|
|
199
|
+
export function samsDetailSnapExpr() {
|
|
200
|
+
return {
|
|
201
|
+
_id: '$_id',
|
|
202
|
+
agency_id: '$agency_id',
|
|
203
|
+
analysis: '$analysis',
|
|
204
|
+
created_at: '$created_at',
|
|
205
|
+
latest_apex_version: '$latest_apex_version',
|
|
206
|
+
remarks: '$remarks',
|
|
207
|
+
seen_first_at: '$seen_first_at',
|
|
208
|
+
seen_last_at: '$seen_last_at',
|
|
209
|
+
system_status: '$system_status',
|
|
210
|
+
timeline_summary: '$timeline_summary',
|
|
211
|
+
transactions_expected: '$transactions_expected',
|
|
212
|
+
transactions_found: '$transactions_found',
|
|
213
|
+
transactions_missing: '$transactions_missing',
|
|
214
|
+
updated_at: '$updated_at',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/* eslint-enable perfectionist/sort-objects */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type AggregationPipeline } from '../../../common/aggregation-pipeline.js';
|
|
2
|
+
import { type Sam } from '@tmlmobilidade/types';
|
|
3
|
+
/**
|
|
4
|
+
* Optional per-analysis filters after `$unwind` (e.g. export file `start_time` / `end_time`).
|
|
5
|
+
*/
|
|
6
|
+
export interface SamsAnalysisExportAnalysisFilter {
|
|
7
|
+
/** If set, keep rows where `analysis.end_time` is less than or equal to this timestamp. */
|
|
8
|
+
end_time?: number;
|
|
9
|
+
/** If set, keep rows where `analysis.start_time` is greater than or equal to this timestamp. */
|
|
10
|
+
start_time?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds a pipeline that returns one document per analysis record (no SAM pagination),
|
|
14
|
+
* for CSV / file export. SAM documents are restricted by optional {@link samIds} and/or
|
|
15
|
+
* {@link matchAnd} (ANDed together). Callers must pass at least one non-empty constraint;
|
|
16
|
+
* otherwise the pipeline would not filter SAMs before unwind.
|
|
17
|
+
* Then `analysis` is unwound and optionally narrowed by {@link SamsAnalysisExportAnalysisFilter}.
|
|
18
|
+
*
|
|
19
|
+
* @param samIds - Optional SAM `_id` values to include. When omitted or empty, no `_id` filter is applied.
|
|
20
|
+
* @param matchAnd - Optional list conditions ANDed with each other (and with `samIds` when set).
|
|
21
|
+
* @param analysisFilter - Optional filters on the unwound `analysis` subdocument.
|
|
22
|
+
* @returns Aggregation pipeline for MongoDB.
|
|
23
|
+
*/
|
|
24
|
+
export declare function samsAnalysisExportAggregationPipeline({ analysisFilter, matchAnd, samIds }: {
|
|
25
|
+
analysisFilter?: SamsAnalysisExportAnalysisFilter;
|
|
26
|
+
matchAnd?: Record<string, unknown>[];
|
|
27
|
+
samIds?: number[];
|
|
28
|
+
}): AggregationPipeline<Sam>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
/**
|
|
3
|
+
* Builds a pipeline that returns one document per analysis record (no SAM pagination),
|
|
4
|
+
* for CSV / file export. SAM documents are restricted by optional {@link samIds} and/or
|
|
5
|
+
* {@link matchAnd} (ANDed together). Callers must pass at least one non-empty constraint;
|
|
6
|
+
* otherwise the pipeline would not filter SAMs before unwind.
|
|
7
|
+
* Then `analysis` is unwound and optionally narrowed by {@link SamsAnalysisExportAnalysisFilter}.
|
|
8
|
+
*
|
|
9
|
+
* @param samIds - Optional SAM `_id` values to include. When omitted or empty, no `_id` filter is applied.
|
|
10
|
+
* @param matchAnd - Optional list conditions ANDed with each other (and with `samIds` when set).
|
|
11
|
+
* @param analysisFilter - Optional filters on the unwound `analysis` subdocument.
|
|
12
|
+
* @returns Aggregation pipeline for MongoDB.
|
|
13
|
+
*/
|
|
14
|
+
export function samsAnalysisExportAggregationPipeline({ analysisFilter, matchAnd, samIds }) {
|
|
15
|
+
const analysisConditions = [];
|
|
16
|
+
if (analysisFilter?.end_time != null) {
|
|
17
|
+
analysisConditions.push({ 'analysis.end_time': { $lte: analysisFilter.end_time } });
|
|
18
|
+
}
|
|
19
|
+
if (analysisFilter?.start_time != null) {
|
|
20
|
+
analysisConditions.push({ 'analysis.start_time': { $gte: analysisFilter.start_time } });
|
|
21
|
+
}
|
|
22
|
+
const docMatchConditions = [];
|
|
23
|
+
if (samIds != null && samIds.length > 0) {
|
|
24
|
+
docMatchConditions.push({ _id: { $in: samIds } });
|
|
25
|
+
}
|
|
26
|
+
if (matchAnd != null && matchAnd.length > 0) {
|
|
27
|
+
docMatchConditions.push(...matchAnd);
|
|
28
|
+
}
|
|
29
|
+
const docMatchStage = docMatchConditions.length === 0
|
|
30
|
+
? undefined
|
|
31
|
+
: docMatchConditions.length === 1
|
|
32
|
+
? { $match: docMatchConditions[0] }
|
|
33
|
+
: { $match: { $and: docMatchConditions } };
|
|
34
|
+
return [
|
|
35
|
+
...(docMatchStage ? [docMatchStage] : []),
|
|
36
|
+
{ $sort: { created_at: -1 } },
|
|
37
|
+
{ $unwind: { path: '$analysis', preserveNullAndEmptyArrays: false } },
|
|
38
|
+
...(analysisConditions.length > 0 ? [{ $match: { $and: analysisConditions } }] : []),
|
|
39
|
+
{ $sort: { '_id': 1, 'analysis.start_time': -1 } },
|
|
40
|
+
{
|
|
41
|
+
$project: {
|
|
42
|
+
_id: 0,
|
|
43
|
+
agency_id: 1,
|
|
44
|
+
analysis: 1,
|
|
45
|
+
created_at: 1,
|
|
46
|
+
latest_apex_version: 1,
|
|
47
|
+
remarks: 1,
|
|
48
|
+
seen_first_at: 1,
|
|
49
|
+
seen_last_at: 1,
|
|
50
|
+
system_status: 1,
|
|
51
|
+
transactions_expected: 1,
|
|
52
|
+
transactions_found: 1,
|
|
53
|
+
transactions_missing: 1,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
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>[];
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const SAMS_VEHICLE_SEARCH_REGEX: RegExp;
|
|
2
|
+
export declare const SAMS_DEVICE_SEARCH_REGEX: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* Parses a sams vehicle search string (e.g., "v:123,456").
|
|
5
|
+
* Returns a deduplicated array of vehicle IDs as numbers.
|
|
6
|
+
* Returns an empty array if the pattern does not match.
|
|
7
|
+
*
|
|
8
|
+
* @param searchRaw - The raw vehicle search string
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseSamsVehicleSearch(searchRaw: string): number[];
|
|
11
|
+
/**
|
|
12
|
+
* Parses a sams device search string (e.g., "d:DEVICE1,DEVICE2").
|
|
13
|
+
* Returns a deduplicated array of device IDs as strings.
|
|
14
|
+
* Returns an empty array if the pattern does not match.
|
|
15
|
+
*
|
|
16
|
+
* @param searchRaw - The raw device search string
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseSamsDeviceSearch(searchRaw: string): string[];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
// Regex to match vehicle searches in the format: "v:123,456,789"
|
|
3
|
+
export const SAMS_VEHICLE_SEARCH_REGEX = /^v:(?<vehicleIds>\d+(?:\s*,\s*\d+)*)$/i;
|
|
4
|
+
// Regex to match device searches in the format: "d:A1,B2,C3"
|
|
5
|
+
export const SAMS_DEVICE_SEARCH_REGEX = /^d:(?<deviceIds>[^,\s]+(?:\s*,\s*[^,\s]+)*)$/i;
|
|
6
|
+
/**
|
|
7
|
+
* Parses a sams vehicle search string (e.g., "v:123,456").
|
|
8
|
+
* Returns a deduplicated array of vehicle IDs as numbers.
|
|
9
|
+
* Returns an empty array if the pattern does not match.
|
|
10
|
+
*
|
|
11
|
+
* @param searchRaw - The raw vehicle search string
|
|
12
|
+
*/
|
|
13
|
+
export function parseSamsVehicleSearch(searchRaw) {
|
|
14
|
+
// Remove any leading/trailing whitespace
|
|
15
|
+
const normalizedSearch = searchRaw.trim();
|
|
16
|
+
// Execute regex to extract vehicleIds group
|
|
17
|
+
const regexMatch = SAMS_VEHICLE_SEARCH_REGEX.exec(normalizedSearch);
|
|
18
|
+
// If there is no match or vehicleIds group, return []
|
|
19
|
+
if (!regexMatch?.groups?.vehicleIds)
|
|
20
|
+
return [];
|
|
21
|
+
// Split by comma, trim whitespace, convert to numbers, and filter invalid numbers
|
|
22
|
+
// Use Set to remove duplicates
|
|
23
|
+
return [
|
|
24
|
+
...new Set(regexMatch.groups.vehicleIds
|
|
25
|
+
.split(',')
|
|
26
|
+
.map(item => Number(item.trim()))
|
|
27
|
+
.filter(item => Number.isInteger(item))),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parses a sams device search string (e.g., "d:DEVICE1,DEVICE2").
|
|
32
|
+
* Returns a deduplicated array of device IDs as strings.
|
|
33
|
+
* Returns an empty array if the pattern does not match.
|
|
34
|
+
*
|
|
35
|
+
* @param searchRaw - The raw device search string
|
|
36
|
+
*/
|
|
37
|
+
export function parseSamsDeviceSearch(searchRaw) {
|
|
38
|
+
// Remove any leading/trailing whitespace
|
|
39
|
+
const normalizedSearch = searchRaw.trim();
|
|
40
|
+
// Execute regex to extract deviceIds group
|
|
41
|
+
const regexMatch = SAMS_DEVICE_SEARCH_REGEX.exec(normalizedSearch);
|
|
42
|
+
// If there is no match or deviceIds group, return []
|
|
43
|
+
if (!regexMatch?.groups?.deviceIds)
|
|
44
|
+
return [];
|
|
45
|
+
// Split by comma, trim whitespace, filter out empty values
|
|
46
|
+
// Use Set to remove duplicates
|
|
47
|
+
return [
|
|
48
|
+
...new Set(regexMatch.groups.deviceIds
|
|
49
|
+
.split(',')
|
|
50
|
+
.map(item => item.trim())
|
|
51
|
+
.filter(Boolean)),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
@@ -22,7 +22,12 @@ class SamsClass extends MongoCollectionClass {
|
|
|
22
22
|
return [
|
|
23
23
|
{ background: true, key: { created_at: 1 } },
|
|
24
24
|
{ background: true, key: { agency_id: 1 } },
|
|
25
|
+
{ background: true, key: { agency_id: 1, created_at: -1 } },
|
|
26
|
+
{ background: true, key: { latest_apex_version: 1 } },
|
|
27
|
+
{ background: true, key: { created_at: -1, latest_apex_version: 1 } },
|
|
25
28
|
{ background: true, key: { mac_sam_serial_number: 1 } },
|
|
29
|
+
{ background: true, key: { seen_first_at: 1 } },
|
|
30
|
+
{ background: true, key: { seen_last_at: 1 } },
|
|
26
31
|
];
|
|
27
32
|
}
|
|
28
33
|
getCollectionName() {
|