@plusscommunities/pluss-maintenance-aws-forms 2.1.43 → 2.1.45
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/feature.config.js +8 -0
- package/package.json +1 -1
- package/requests/getRequests.js +217 -142
package/feature.config.js
CHANGED
|
@@ -212,6 +212,7 @@ exports.serverless = {
|
|
|
212
212
|
{ name: "jobNo", type: "N" },
|
|
213
213
|
{ name: "userID", type: "S" },
|
|
214
214
|
{ name: "AssigneeId", type: "S" },
|
|
215
|
+
{ name: "status", type: "S" },
|
|
215
216
|
],
|
|
216
217
|
id: "id",
|
|
217
218
|
indexes: [
|
|
@@ -247,6 +248,13 @@ exports.serverless = {
|
|
|
247
248
|
{ name: "AssigneeId", type: "RANGE" },
|
|
248
249
|
],
|
|
249
250
|
},
|
|
251
|
+
{
|
|
252
|
+
name: "MaintenanceSiteStatusIndex",
|
|
253
|
+
keys: [
|
|
254
|
+
{ name: "site", type: "HASH" },
|
|
255
|
+
{ name: "status", type: "RANGE" },
|
|
256
|
+
],
|
|
257
|
+
},
|
|
250
258
|
],
|
|
251
259
|
},
|
|
252
260
|
{
|
package/package.json
CHANGED
package/requests/getRequests.js
CHANGED
|
@@ -3,169 +3,244 @@ const getSessionUserFromReqAuthKey = require("@plusscommunities/pluss-core-aws/h
|
|
|
3
3
|
const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
|
|
4
4
|
const validateSiteAccess = require("@plusscommunities/pluss-core-aws/helper/auth/validateSiteAccess");
|
|
5
5
|
const indexQuery = require("@plusscommunities/pluss-core-aws/db/common/indexQuery");
|
|
6
|
+
const indexQueryRecursive = require("@plusscommunities/pluss-core-aws/db/common/indexQueryRecursive");
|
|
6
7
|
const { values } = require("../values.config");
|
|
7
8
|
|
|
8
9
|
// Normalise legacy/null status to "Open" (null = very old jobs, "Unassigned" = pre-rename)
|
|
9
10
|
const STATUS_OPEN = "Open";
|
|
10
11
|
const normalizeStatus = (status) =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// HACK: Lax "complete" match handles minor label variations across sites
|
|
14
|
-
const isCompleted = (status) =>
|
|
15
|
-
status && status.toLowerCase().includes("complete");
|
|
12
|
+
!status || status === "Unassigned" ? STATUS_OPEN : status;
|
|
16
13
|
|
|
17
14
|
// Priority defaults to "Low" when not set (not set on creation)
|
|
18
15
|
const DEFAULT_PRIORITY = "Low";
|
|
19
16
|
const normalizePriority = (priority) =>
|
|
20
|
-
|
|
17
|
+
priority == null ? DEFAULT_PRIORITY : priority;
|
|
21
18
|
|
|
22
19
|
// Minimum number of filtered results to return before stopping.
|
|
23
20
|
// Because DynamoDB pages are unfiltered and we filter after query,
|
|
24
21
|
// a single page may yield very few matching results. We keep
|
|
25
22
|
// fetching pages until we have this many items to return.
|
|
26
|
-
const MIN_RESULT_COUNT =
|
|
23
|
+
const MIN_RESULT_COUNT = 250;
|
|
24
|
+
|
|
25
|
+
// When filters are active, fetch all matching results so the
|
|
26
|
+
// client receives a complete set rather than a truncated page.
|
|
27
|
+
const MIN_RESULT_COUNT_FILTERED = Infinity;
|
|
28
|
+
|
|
29
|
+
const hasActiveFilters = (qParams) =>
|
|
30
|
+
qParams.status ||
|
|
31
|
+
qParams.priority ||
|
|
32
|
+
qParams.type ||
|
|
33
|
+
qParams.search ||
|
|
34
|
+
qParams.startTime ||
|
|
35
|
+
qParams.endTime;
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* Apply all post-query filters to a set of jobs.
|
|
30
39
|
* Extracted so the same logic runs on every auto-fill page.
|
|
31
40
|
*/
|
|
32
41
|
const filterJobs = (jobs, qParams, authorised, assigneeTracking, userId) => {
|
|
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
|
-
|
|
42
|
+
let filtered = jobs;
|
|
43
|
+
|
|
44
|
+
if (qParams.status) {
|
|
45
|
+
filtered = filtered.filter((j) =>
|
|
46
|
+
qParams.status.includes(normalizeStatus(j.status)),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (qParams.priority) {
|
|
51
|
+
filtered = filtered.filter((j) =>
|
|
52
|
+
qParams.priority.includes(normalizePriority(j.priority)),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (qParams.type) {
|
|
57
|
+
filtered = filtered.filter((j) => qParams.type.includes(j.type));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!authorised && assigneeTracking) {
|
|
61
|
+
filtered = filtered.filter(
|
|
62
|
+
(j) => j.AssigneeId === userId || j.userID === userId,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (qParams.startTime) {
|
|
67
|
+
const startTime = parseInt(qParams.startTime, 10);
|
|
68
|
+
filtered = filtered.filter((j) => j.createdUnix >= startTime);
|
|
69
|
+
}
|
|
70
|
+
if (qParams.endTime) {
|
|
71
|
+
const endTime = parseInt(qParams.endTime, 10);
|
|
72
|
+
filtered = filtered.filter((j) => j.createdUnix <= endTime);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (qParams.search) {
|
|
76
|
+
const searchLower = qParams.search.toLowerCase();
|
|
77
|
+
filtered = filtered.filter((j) => {
|
|
78
|
+
if (j.jobId && j.jobId === qParams.search) return true;
|
|
79
|
+
if (j.room && j.room.toLowerCase().indexOf(searchLower) > -1) return true;
|
|
80
|
+
if (j.title && j.title.toLowerCase().indexOf(searchLower) > -1)
|
|
81
|
+
return true;
|
|
82
|
+
return false;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return filtered;
|
|
75
87
|
};
|
|
76
88
|
|
|
77
89
|
module.exports = async (event) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
90
|
+
const qParams = event.queryStringParameters;
|
|
91
|
+
const logId = log("getRequests", "Params", qParams);
|
|
92
|
+
|
|
93
|
+
// insufficient input
|
|
94
|
+
if (!qParams.site) {
|
|
95
|
+
return { status: 422, data: { error: "Insufficient input" } };
|
|
96
|
+
}
|
|
97
|
+
log("getRequests", "SufficientInput", true, logId);
|
|
98
|
+
|
|
99
|
+
// no access to site
|
|
100
|
+
const valid = await validateSiteAccess(event, qParams.site);
|
|
101
|
+
log("getRequests", "valid", valid, logId);
|
|
102
|
+
if (!valid) {
|
|
103
|
+
return { status: 403, data: { error: "Not authorised" } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// check auth level to determine whether to fetch all requests or only matching requests
|
|
107
|
+
const authorised = await validateMasterAuth(
|
|
108
|
+
event,
|
|
109
|
+
values.permissionMaintenanceTracking,
|
|
110
|
+
qParams.site,
|
|
111
|
+
);
|
|
112
|
+
let assigneeTracking = false;
|
|
113
|
+
if (!authorised) {
|
|
114
|
+
assigneeTracking = await validateMasterAuth(
|
|
115
|
+
event,
|
|
116
|
+
values.permissionMaintenanceAssignment,
|
|
117
|
+
qParams.site,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
log("getRequests", "authorised", authorised, logId);
|
|
122
|
+
const userId = authorised ? null : await getSessionUserFromReqAuthKey(event);
|
|
123
|
+
|
|
124
|
+
log("getRequests", "userId", userId, logId);
|
|
125
|
+
|
|
126
|
+
// Queries each status value in parallel for speed.
|
|
127
|
+
// Each job has one status, so no dedup needed.
|
|
128
|
+
const useStatusGsi =
|
|
129
|
+
qParams.status && !qParams.assignee && (authorised || assigneeTracking);
|
|
130
|
+
|
|
131
|
+
if (useStatusGsi) {
|
|
132
|
+
const statusValues = Array.isArray(qParams.status)
|
|
133
|
+
? qParams.status
|
|
134
|
+
: qParams.status.split(",");
|
|
135
|
+
|
|
136
|
+
// "Open" matches both "Open" and legacy "Unassigned" in the DB
|
|
137
|
+
const dbStatuses = statusValues.flatMap((s) =>
|
|
138
|
+
s === STATUS_OPEN ? [STATUS_OPEN, "Unassigned"] : [s],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
log("getRequests", "StatusGsi", dbStatuses, logId);
|
|
142
|
+
|
|
143
|
+
const allResults = await Promise.all(
|
|
144
|
+
dbStatuses.map((s) =>
|
|
145
|
+
indexQueryRecursive(values.tableNameMaintenance, {
|
|
146
|
+
IndexName: "MaintenanceSiteStatusIndex",
|
|
147
|
+
KeyConditionExpression: "site = :site AND #status = :status",
|
|
148
|
+
ExpressionAttributeNames: { "#status": "status" },
|
|
149
|
+
ExpressionAttributeValues: {
|
|
150
|
+
":site": qParams.site,
|
|
151
|
+
":status": s,
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const allJobs = filterJobs(
|
|
158
|
+
allResults.flat(),
|
|
159
|
+
qParams,
|
|
160
|
+
authorised,
|
|
161
|
+
assigneeTracking,
|
|
162
|
+
userId,
|
|
163
|
+
);
|
|
164
|
+
log("getRequests", "StatusGsiResult", allJobs.length, logId);
|
|
165
|
+
return { status: 200, data: { Items: allJobs } };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const query =
|
|
169
|
+
authorised || assigneeTracking
|
|
170
|
+
? {
|
|
171
|
+
IndexName: "MaintenanceSiteJobNoIndex",
|
|
172
|
+
KeyConditionExpression: "site = :site",
|
|
173
|
+
ExpressionAttributeValues: {
|
|
174
|
+
":site": qParams.site,
|
|
175
|
+
},
|
|
176
|
+
ScanIndexForward: false,
|
|
177
|
+
}
|
|
178
|
+
: {
|
|
179
|
+
IndexName: "MaintenanceSiteUserIdIndex",
|
|
180
|
+
KeyConditionExpression: "site = :site AND userID = :userId",
|
|
181
|
+
ExpressionAttributeValues: {
|
|
182
|
+
":site": qParams.site,
|
|
183
|
+
":userId": userId,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Use the assignee GSI when filtering by assignee (avoids fetching the entire site's jobs)
|
|
188
|
+
if (qParams.assignee && (authorised || assigneeTracking)) {
|
|
189
|
+
query.IndexName = "MaintenanceSiteAssigneeIndex";
|
|
190
|
+
query.KeyConditionExpression = "site = :site AND AssigneeId = :assigneeId";
|
|
191
|
+
query.ExpressionAttributeValues[":assigneeId"] = qParams.assignee;
|
|
192
|
+
}
|
|
193
|
+
log("getRequests", "query", query, logId);
|
|
194
|
+
|
|
195
|
+
// check whether pagination is applied
|
|
196
|
+
if (qParams.lastKey) {
|
|
197
|
+
try {
|
|
198
|
+
query.ExclusiveStartKey = JSON.parse(qParams.lastKey);
|
|
199
|
+
} catch (e) {}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// get first page of jobs
|
|
203
|
+
let result = await indexQuery(values.tableNameMaintenance, query);
|
|
204
|
+
let allJobs = filterJobs(
|
|
205
|
+
result.Items,
|
|
206
|
+
qParams,
|
|
207
|
+
authorised,
|
|
208
|
+
assigneeTracking,
|
|
209
|
+
userId,
|
|
210
|
+
);
|
|
211
|
+
let lastKey = result.LastEvaluatedKey;
|
|
212
|
+
|
|
213
|
+
log("getRequests", "LastEvaluatedKey", lastKey, logId);
|
|
214
|
+
log("getRequests", "FirstPageFiltered", allJobs.length, logId);
|
|
215
|
+
|
|
216
|
+
const minResults = hasActiveFilters(qParams)
|
|
217
|
+
? MIN_RESULT_COUNT_FILTERED
|
|
218
|
+
: MIN_RESULT_COUNT;
|
|
219
|
+
|
|
220
|
+
// auto-fill: keep fetching pages until we have enough filtered results
|
|
221
|
+
while (lastKey && allJobs.length < minResults) {
|
|
222
|
+
const nextQuery = { ...query, ExclusiveStartKey: lastKey };
|
|
223
|
+
result = await indexQuery(values.tableNameMaintenance, nextQuery);
|
|
224
|
+
const filtered = filterJobs(
|
|
225
|
+
result.Items,
|
|
226
|
+
qParams,
|
|
227
|
+
authorised,
|
|
228
|
+
assigneeTracking,
|
|
229
|
+
userId,
|
|
230
|
+
);
|
|
231
|
+
allJobs = [...allJobs, ...filtered];
|
|
232
|
+
lastKey = result.LastEvaluatedKey;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
log("getRequests", "TotalFiltered", allJobs.length, logId);
|
|
236
|
+
log(
|
|
237
|
+
"getRequests",
|
|
238
|
+
"PagesFetched",
|
|
239
|
+
lastKey ? "more available" : "exhausted",
|
|
240
|
+
logId,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const results = { Items: allJobs, LastKey: lastKey };
|
|
244
|
+
log("getRequests", "Done", true, logId);
|
|
245
|
+
return { status: 200, data: results };
|
|
171
246
|
};
|