@govish/shared-services 1.0.0
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/CHANGELOG.md +38 -0
- package/README.md +376 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +36 -0
- package/dist/middleware/authenticateDevice.d.ts +15 -0
- package/dist/middleware/authenticateDevice.js +310 -0
- package/dist/services/apiKeyService.d.ts +53 -0
- package/dist/services/apiKeyService.js +293 -0
- package/dist/services/auditService.d.ts +109 -0
- package/dist/services/auditService.js +785 -0
- package/dist/services/deviceCacheService.d.ts +46 -0
- package/dist/services/deviceCacheService.js +432 -0
- package/dist/services/deviceService.d.ts +21 -0
- package/dist/services/deviceService.js +103 -0
- package/dist/services/officerCacheService.d.ts +50 -0
- package/dist/services/officerCacheService.js +434 -0
- package/dist/services/officerService.d.ts +25 -0
- package/dist/services/officerService.js +177 -0
- package/dist/services/penalCodeCacheService.d.ts +20 -0
- package/dist/services/penalCodeCacheService.js +109 -0
- package/dist/types/dependencies.d.ts +23 -0
- package/dist/types/dependencies.js +2 -0
- package/dist/utils/logMode.d.ts +33 -0
- package/dist/utils/logMode.js +90 -0
- package/dist/utils/redis.d.ts +24 -0
- package/dist/utils/redis.js +122 -0
- package/package.json +52 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.OfficerCacheService = void 0;
|
|
13
|
+
class OfficerCacheService {
|
|
14
|
+
constructor(deps) {
|
|
15
|
+
this.redisClient = deps.redisClient;
|
|
16
|
+
this.logger = deps.logger;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Store individual officer as hash with indexes
|
|
20
|
+
*/
|
|
21
|
+
storeOfficer(officer) {
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
24
|
+
try {
|
|
25
|
+
const key = `officer:${officer.id}`;
|
|
26
|
+
// Store as hash with JSON-stringified complex fields
|
|
27
|
+
yield this.redisClient.hSet(key, {
|
|
28
|
+
id: officer.id.toString(),
|
|
29
|
+
name: officer.name || '',
|
|
30
|
+
service_number: officer.service_number || '',
|
|
31
|
+
email: officer.email || '',
|
|
32
|
+
badge_number: officer.badge_number || '',
|
|
33
|
+
phone_number: officer.phone_number || '',
|
|
34
|
+
stationId: ((_a = officer.stationId) === null || _a === void 0 ? void 0 : _a.toString()) || '',
|
|
35
|
+
rankId: ((_b = officer.rankId) === null || _b === void 0 ? void 0 : _b.toString()) || '',
|
|
36
|
+
roleId: ((_c = officer.roleId) === null || _c === void 0 ? void 0 : _c.toString()) || '',
|
|
37
|
+
subCountyId: ((_d = officer.subCountyId) === null || _d === void 0 ? void 0 : _d.toString()) || '',
|
|
38
|
+
countyId: ((_e = officer.countyId) === null || _e === void 0 ? void 0 : _e.toString()) || '',
|
|
39
|
+
regionId: ((_f = officer.regionId) === null || _f === void 0 ? void 0 : _f.toString()) || '',
|
|
40
|
+
iPRS_PersonId: ((_g = officer.iPRS_PersonId) === null || _g === void 0 ? void 0 : _g.toString()) || '',
|
|
41
|
+
is_temporary_password: ((_h = officer.is_temporary_password) === null || _h === void 0 ? void 0 : _h.toString()) || 'false',
|
|
42
|
+
created_at: ((_j = officer.created_at) === null || _j === void 0 ? void 0 : _j.toISOString()) || '',
|
|
43
|
+
updated_at: ((_k = officer.updated_at) === null || _k === void 0 ? void 0 : _k.toISOString()) || '',
|
|
44
|
+
// Stringify nested objects
|
|
45
|
+
iprs: JSON.stringify(officer.iprs || null),
|
|
46
|
+
station: JSON.stringify(officer.station || null),
|
|
47
|
+
subCounty: JSON.stringify(officer.subCounty || null),
|
|
48
|
+
county: JSON.stringify(officer.county || null),
|
|
49
|
+
region: JSON.stringify(officer.region || null),
|
|
50
|
+
rank: JSON.stringify(officer.rank || null),
|
|
51
|
+
role: JSON.stringify(officer.role || null),
|
|
52
|
+
Next_Of_Kin: JSON.stringify(officer.Next_Of_Kin || null)
|
|
53
|
+
});
|
|
54
|
+
// Create indexes for common queries
|
|
55
|
+
if (officer.stationId) {
|
|
56
|
+
yield this.redisClient.sAdd(`officers:station:${officer.stationId}`, officer.id.toString());
|
|
57
|
+
}
|
|
58
|
+
if (officer.rankId) {
|
|
59
|
+
yield this.redisClient.sAdd(`officers:rank:${officer.rankId}`, officer.id.toString());
|
|
60
|
+
}
|
|
61
|
+
if (officer.roleId) {
|
|
62
|
+
yield this.redisClient.sAdd(`officers:role:${officer.roleId}`, officer.id.toString());
|
|
63
|
+
}
|
|
64
|
+
if (officer.subCountyId) {
|
|
65
|
+
yield this.redisClient.sAdd(`officers:subCounty:${officer.subCountyId}`, officer.id.toString());
|
|
66
|
+
}
|
|
67
|
+
if (officer.countyId) {
|
|
68
|
+
yield this.redisClient.sAdd(`officers:county:${officer.countyId}`, officer.id.toString());
|
|
69
|
+
}
|
|
70
|
+
if (officer.regionId) {
|
|
71
|
+
yield this.redisClient.sAdd(`officers:region:${officer.regionId}`, officer.id.toString());
|
|
72
|
+
}
|
|
73
|
+
// Maintain list of all officer IDs
|
|
74
|
+
yield this.redisClient.sAdd('officers:all', officer.id.toString());
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
this.logger.error('Error storing officer in cache', {
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
officerId: officer.id
|
|
80
|
+
});
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get officer by ID - O(1) lookup
|
|
87
|
+
*/
|
|
88
|
+
getOfficerById(id) {
|
|
89
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
90
|
+
try {
|
|
91
|
+
const rawData = yield this.redisClient.hGetAll(`officer:${id}`);
|
|
92
|
+
// Convert Map to Record if needed, or use as-is if already Record
|
|
93
|
+
let data;
|
|
94
|
+
if (rawData instanceof Map) {
|
|
95
|
+
data = Object.fromEntries(rawData);
|
|
96
|
+
}
|
|
97
|
+
else if (Array.isArray(rawData)) {
|
|
98
|
+
// Handle array case (shouldn't happen with hGetAll, but TypeScript sees it as possible)
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
data = rawData;
|
|
103
|
+
}
|
|
104
|
+
if (!data || !data.id)
|
|
105
|
+
return null;
|
|
106
|
+
// Parse JSON fields
|
|
107
|
+
return Object.assign(Object.assign({}, data), { id: parseInt(data.id), stationId: data.stationId ? parseInt(data.stationId) : null, rankId: data.rankId ? parseInt(data.rankId) : null, roleId: data.roleId ? parseInt(data.roleId) : null, subCountyId: data.subCountyId ? parseInt(data.subCountyId) : null, countyId: data.countyId ? parseInt(data.countyId) : null, regionId: data.regionId ? parseInt(data.regionId) : null, iPRS_PersonId: data.iPRS_PersonId ? parseInt(data.iPRS_PersonId) : null, is_temporary_password: data.is_temporary_password === 'true', created_at: data.created_at ? new Date(data.created_at) : null, updated_at: data.updated_at ? new Date(data.updated_at) : null, iprs: data.iprs && data.iprs !== 'null' && data.iprs !== '"null"' ? JSON.parse(data.iprs) : null, station: data.station && data.station !== 'null' && data.station !== '"null"' ? JSON.parse(data.station) : null, subCounty: data.subCounty && data.subCounty !== 'null' && data.subCounty !== '"null"' ? JSON.parse(data.subCounty) : null, county: data.county && data.county !== 'null' && data.county !== '"null"' ? JSON.parse(data.county) : null, region: data.region && data.region !== 'null' && data.region !== '"null"' ? JSON.parse(data.region) : null, rank: data.rank && data.rank !== 'null' && data.rank !== '"null"' ? JSON.parse(data.rank) : null, role: data.role && data.role !== 'null' && data.role !== '"null"' ? JSON.parse(data.role) : null, Next_Of_Kin: data.Next_Of_Kin && data.Next_Of_Kin !== 'null' && data.Next_Of_Kin !== '"null"' ? JSON.parse(data.Next_Of_Kin) : null });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
this.logger.error('Error getting officer from cache', {
|
|
111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
112
|
+
officerId: id
|
|
113
|
+
});
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get multiple officers efficiently using pipeline
|
|
120
|
+
*/
|
|
121
|
+
getOfficersByIds(ids) {
|
|
122
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
if (ids.length === 0)
|
|
124
|
+
return [];
|
|
125
|
+
try {
|
|
126
|
+
const pipeline = this.redisClient.multi();
|
|
127
|
+
ids.forEach(id => pipeline.hGetAll(`officer:${id}`));
|
|
128
|
+
const results = yield pipeline.exec();
|
|
129
|
+
if (!results) {
|
|
130
|
+
this.logger.warn('Pipeline exec returned null/undefined', { idsCount: ids.length, ids });
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
if (results.length === 0) {
|
|
134
|
+
this.logger.warn('Pipeline exec returned empty results array', { idsCount: ids.length, ids });
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
if (results.length !== ids.length) {
|
|
138
|
+
this.logger.warn('Pipeline results count mismatch', {
|
|
139
|
+
expectedCount: ids.length,
|
|
140
|
+
actualCount: results.length,
|
|
141
|
+
ids
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return results
|
|
145
|
+
.map((result, index) => {
|
|
146
|
+
let rawData;
|
|
147
|
+
// In Redis v4, pipeline.exec() returns results as [error, result] tuples
|
|
148
|
+
// Check if result is an error tuple
|
|
149
|
+
if (Array.isArray(result) && result.length === 2) {
|
|
150
|
+
const [error, data] = result;
|
|
151
|
+
if (error) {
|
|
152
|
+
this.logger.warn('Pipeline command error', {
|
|
153
|
+
error,
|
|
154
|
+
index,
|
|
155
|
+
officerId: ids[index],
|
|
156
|
+
key: `officer:${ids[index]}`
|
|
157
|
+
});
|
|
158
|
+
return null; // Skip errors
|
|
159
|
+
}
|
|
160
|
+
rawData = data;
|
|
161
|
+
}
|
|
162
|
+
else if (result && typeof result === 'object') {
|
|
163
|
+
// Handle case where result might be returned directly (some Redis client versions)
|
|
164
|
+
rawData = result;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.logger.warn('Unexpected pipeline result format', {
|
|
168
|
+
result,
|
|
169
|
+
index,
|
|
170
|
+
officerId: ids[index],
|
|
171
|
+
resultType: typeof result
|
|
172
|
+
});
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
// Process the rawData
|
|
176
|
+
let data;
|
|
177
|
+
if (rawData instanceof Map) {
|
|
178
|
+
data = Object.fromEntries(rawData);
|
|
179
|
+
}
|
|
180
|
+
else if (Array.isArray(rawData)) {
|
|
181
|
+
// Handle array case (shouldn't happen with hGetAll)
|
|
182
|
+
this.logger.warn('Unexpected array result from hGetAll', { index, officerId: ids[index] });
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
else if (rawData && typeof rawData === 'object') {
|
|
186
|
+
data = rawData;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this.logger.warn('Invalid rawData type', {
|
|
190
|
+
index,
|
|
191
|
+
officerId: ids[index],
|
|
192
|
+
rawDataType: typeof rawData
|
|
193
|
+
});
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
// Check if data is empty or missing id
|
|
197
|
+
if (!data || Object.keys(data).length === 0 || !data.id) {
|
|
198
|
+
this.logger.debug('Empty or invalid officer data from cache', {
|
|
199
|
+
index,
|
|
200
|
+
officerId: ids[index],
|
|
201
|
+
hasData: !!data,
|
|
202
|
+
dataKeys: data ? Object.keys(data) : []
|
|
203
|
+
});
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return Object.assign(Object.assign({}, data), { id: parseInt(data.id), stationId: data.stationId ? parseInt(data.stationId) : null, rankId: data.rankId ? parseInt(data.rankId) : null, roleId: data.roleId ? parseInt(data.roleId) : null, subCountyId: data.subCountyId ? parseInt(data.subCountyId) : null, countyId: data.countyId ? parseInt(data.countyId) : null, regionId: data.regionId ? parseInt(data.regionId) : null, iPRS_PersonId: data.iPRS_PersonId ? parseInt(data.iPRS_PersonId) : null, is_temporary_password: data.is_temporary_password === 'true', created_at: data.created_at ? new Date(data.created_at) : null, updated_at: data.updated_at ? new Date(data.updated_at) : null, iprs: data.iprs && data.iprs !== 'null' && data.iprs !== '"null"' ? JSON.parse(data.iprs) : null, station: data.station && data.station !== 'null' && data.station !== '"null"' ? JSON.parse(data.station) : null, subCounty: data.subCounty && data.subCounty !== 'null' && data.subCounty !== '"null"' ? JSON.parse(data.subCounty) : null, county: data.county && data.county !== 'null' && data.county !== '"null"' ? JSON.parse(data.county) : null, region: data.region && data.region !== 'null' && data.region !== '"null"' ? JSON.parse(data.region) : null, rank: data.rank && data.rank !== 'null' && data.rank !== '"null"' ? JSON.parse(data.rank) : null, role: data.role && data.role !== 'null' && data.role !== '"null"' ? JSON.parse(data.role) : null, Next_Of_Kin: data.Next_Of_Kin && data.Next_Of_Kin !== 'null' && data.Next_Of_Kin !== '"null"' ? JSON.parse(data.Next_Of_Kin) : null });
|
|
208
|
+
}
|
|
209
|
+
catch (parseError) {
|
|
210
|
+
this.logger.warn('Error parsing officer data from cache', {
|
|
211
|
+
error: parseError instanceof Error ? parseError.message : String(parseError),
|
|
212
|
+
officerId: data.id,
|
|
213
|
+
index
|
|
214
|
+
});
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.filter(Boolean);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
this.logger.error('Error getting officers from cache', {
|
|
222
|
+
error: error instanceof Error ? error.message : String(error),
|
|
223
|
+
idsCount: ids.length
|
|
224
|
+
});
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Query by station
|
|
231
|
+
*/
|
|
232
|
+
getOfficersByStation(stationId) {
|
|
233
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
234
|
+
try {
|
|
235
|
+
const officerIds = yield this.redisClient.sMembers(`officers:station:${stationId}`);
|
|
236
|
+
this.logger.debug('Getting officers by station from cache', {
|
|
237
|
+
stationId,
|
|
238
|
+
officerIdsCount: officerIds.length
|
|
239
|
+
});
|
|
240
|
+
return this.getOfficersByIds(officerIds.map(id => parseInt(id)));
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
this.logger.error('Error getting officers by station from cache', {
|
|
244
|
+
error: error instanceof Error ? error.message : String(error),
|
|
245
|
+
stationId
|
|
246
|
+
});
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Query by rank
|
|
253
|
+
*/
|
|
254
|
+
getOfficersByRank(rankId) {
|
|
255
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
+
try {
|
|
257
|
+
const officerIds = yield this.redisClient.sMembers(`officers:rank:${rankId}`);
|
|
258
|
+
return this.getOfficersByIds(officerIds.map(id => parseInt(id)));
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
this.logger.error('Error getting officers by rank from cache', {
|
|
262
|
+
error: error instanceof Error ? error.message : String(error),
|
|
263
|
+
rankId
|
|
264
|
+
});
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Query by role
|
|
271
|
+
*/
|
|
272
|
+
getOfficersByRole(roleId) {
|
|
273
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
274
|
+
try {
|
|
275
|
+
const officerIds = yield this.redisClient.sMembers(`officers:role:${roleId}`);
|
|
276
|
+
return this.getOfficersByIds(officerIds.map(id => parseInt(id)));
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
this.logger.error('Error getting officers by role from cache', {
|
|
280
|
+
error: error instanceof Error ? error.message : String(error),
|
|
281
|
+
roleId
|
|
282
|
+
});
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get all officers from cache
|
|
289
|
+
*/
|
|
290
|
+
getAllOfficers() {
|
|
291
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
292
|
+
try {
|
|
293
|
+
const officerIds = yield this.redisClient.sMembers('officers:all');
|
|
294
|
+
if (officerIds.length === 0)
|
|
295
|
+
return [];
|
|
296
|
+
const ids = officerIds.map(id => parseInt(id));
|
|
297
|
+
const officers = yield this.getOfficersByIds(ids);
|
|
298
|
+
// Sort by ID descending to match original behavior
|
|
299
|
+
return officers.sort((a, b) => b.id - a.id);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
this.logger.error('Error getting all officers from cache', {
|
|
303
|
+
error: error instanceof Error ? error.message : String(error)
|
|
304
|
+
});
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Get officer by service_number
|
|
311
|
+
*/
|
|
312
|
+
getOfficerByServiceNumber(service_number) {
|
|
313
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
314
|
+
try {
|
|
315
|
+
// Get all officers and filter by service_number
|
|
316
|
+
// This is not ideal but we don't have a service_number index in the updated implementation
|
|
317
|
+
const allOfficers = yield this.getAllOfficers();
|
|
318
|
+
const officer = allOfficers.find((o) => o.service_number === service_number);
|
|
319
|
+
return officer || null;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
this.logger.error('Error getting officer by service_number from cache', {
|
|
323
|
+
error: error instanceof Error ? error.message : String(error),
|
|
324
|
+
service_number
|
|
325
|
+
});
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Cache all officers from auth service
|
|
332
|
+
*/
|
|
333
|
+
cacheAllOfficers() {
|
|
334
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
335
|
+
try {
|
|
336
|
+
// Fetch officers from auth microservice
|
|
337
|
+
const authUrl = process.env.AUTH_PORT
|
|
338
|
+
? `http://${process.env.AUTH_HOST}:${process.env.AUTH_PORT}/api/v1/officer`
|
|
339
|
+
: `http://${process.env.AUTH_HOST}/api/v1/officer`;
|
|
340
|
+
const response = yield fetch(authUrl);
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
throw new Error(`Failed to fetch officers from auth service: ${response.statusText}`);
|
|
343
|
+
}
|
|
344
|
+
const result = yield response.json();
|
|
345
|
+
const officers = result.data || result;
|
|
346
|
+
if (!Array.isArray(officers)) {
|
|
347
|
+
throw new Error('Invalid response format from auth service');
|
|
348
|
+
}
|
|
349
|
+
// Clear existing cache and store all officers
|
|
350
|
+
yield this.clearCache();
|
|
351
|
+
for (const officer of officers) {
|
|
352
|
+
yield this.storeOfficer(officer);
|
|
353
|
+
}
|
|
354
|
+
this.logger.info('Officers cached successfully', {
|
|
355
|
+
officerCount: officers.length
|
|
356
|
+
});
|
|
357
|
+
return officers;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
this.logger.error('Error caching officers', {
|
|
361
|
+
error: error instanceof Error ? error.message : String(error)
|
|
362
|
+
});
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Clear all officer cache
|
|
369
|
+
*/
|
|
370
|
+
clearCache() {
|
|
371
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
372
|
+
try {
|
|
373
|
+
const officerIds = yield this.redisClient.sMembers('officers:all');
|
|
374
|
+
if (officerIds.length > 0) {
|
|
375
|
+
const pipeline = this.redisClient.multi();
|
|
376
|
+
officerIds.forEach(id => pipeline.del(`officer:${id}`));
|
|
377
|
+
yield pipeline.exec();
|
|
378
|
+
}
|
|
379
|
+
// Clear indexes
|
|
380
|
+
const keys = yield this.redisClient.keys('officers:*');
|
|
381
|
+
if (keys.length > 0) {
|
|
382
|
+
yield this.redisClient.del(keys);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
this.logger.error('Error clearing officer cache', {
|
|
387
|
+
error: error instanceof Error ? error.message : String(error)
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Remove single officer from cache
|
|
394
|
+
*/
|
|
395
|
+
removeOfficer(id) {
|
|
396
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
397
|
+
try {
|
|
398
|
+
const officer = yield this.getOfficerById(id);
|
|
399
|
+
if (officer) {
|
|
400
|
+
// Remove from indexes
|
|
401
|
+
if (officer.stationId) {
|
|
402
|
+
yield this.redisClient.sRem(`officers:station:${officer.stationId}`, id.toString());
|
|
403
|
+
}
|
|
404
|
+
if (officer.rankId) {
|
|
405
|
+
yield this.redisClient.sRem(`officers:rank:${officer.rankId}`, id.toString());
|
|
406
|
+
}
|
|
407
|
+
if (officer.roleId) {
|
|
408
|
+
yield this.redisClient.sRem(`officers:role:${officer.roleId}`, id.toString());
|
|
409
|
+
}
|
|
410
|
+
if (officer.subCountyId) {
|
|
411
|
+
yield this.redisClient.sRem(`officers:subCounty:${officer.subCountyId}`, id.toString());
|
|
412
|
+
}
|
|
413
|
+
if (officer.countyId) {
|
|
414
|
+
yield this.redisClient.sRem(`officers:county:${officer.countyId}`, id.toString());
|
|
415
|
+
}
|
|
416
|
+
if (officer.regionId) {
|
|
417
|
+
yield this.redisClient.sRem(`officers:region:${officer.regionId}`, id.toString());
|
|
418
|
+
}
|
|
419
|
+
// Remove from all list
|
|
420
|
+
yield this.redisClient.sRem('officers:all', id.toString());
|
|
421
|
+
// Remove hash
|
|
422
|
+
yield this.redisClient.del(`officer:${id}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
this.logger.error('Error removing officer from cache', {
|
|
427
|
+
error: error instanceof Error ? error.message : String(error),
|
|
428
|
+
officerId: id
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
exports.OfficerCacheService = OfficerCacheService;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SharedServicesDependencies } from '../types/dependencies';
|
|
2
|
+
/**
|
|
3
|
+
* Officer Service - Handles officer retrieval with cache and auth service fallback
|
|
4
|
+
*
|
|
5
|
+
* This service:
|
|
6
|
+
* 1. First tries Redis cache (via OfficerCacheService)
|
|
7
|
+
* 2. Falls back to auth service API if cache miss
|
|
8
|
+
* 3. Stores fetched officers in cache for future use
|
|
9
|
+
*/
|
|
10
|
+
export declare class OfficerService {
|
|
11
|
+
private officerCacheService;
|
|
12
|
+
private logger;
|
|
13
|
+
private authHost?;
|
|
14
|
+
private authPort?;
|
|
15
|
+
constructor(deps: SharedServicesDependencies);
|
|
16
|
+
/**
|
|
17
|
+
* Get officer by ID from Redis cache, fallback to auth service
|
|
18
|
+
* First tries Redis cache, then falls back to auth service API
|
|
19
|
+
*/
|
|
20
|
+
getOfficerById(officerId: string | number): Promise<any | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Get officer by service number from Redis cache, fallback to auth service
|
|
23
|
+
*/
|
|
24
|
+
getOfficerByServiceNumber(serviceNumber: string): Promise<any | null>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.OfficerService = void 0;
|
|
13
|
+
const officerCacheService_1 = require("./officerCacheService");
|
|
14
|
+
/**
|
|
15
|
+
* Officer Service - Handles officer retrieval with cache and auth service fallback
|
|
16
|
+
*
|
|
17
|
+
* This service:
|
|
18
|
+
* 1. First tries Redis cache (via OfficerCacheService)
|
|
19
|
+
* 2. Falls back to auth service API if cache miss
|
|
20
|
+
* 3. Stores fetched officers in cache for future use
|
|
21
|
+
*/
|
|
22
|
+
class OfficerService {
|
|
23
|
+
constructor(deps) {
|
|
24
|
+
this.officerCacheService = new officerCacheService_1.OfficerCacheService(deps);
|
|
25
|
+
this.logger = deps.logger;
|
|
26
|
+
this.authHost = deps.authHost || process.env.AUTH_HOST;
|
|
27
|
+
this.authPort = deps.authPort || process.env.AUTH_PORT;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get officer by ID from Redis cache, fallback to auth service
|
|
31
|
+
* First tries Redis cache, then falls back to auth service API
|
|
32
|
+
*/
|
|
33
|
+
getOfficerById(officerId) {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
try {
|
|
36
|
+
const officerIdNum = typeof officerId === 'string' ? Number(officerId) : officerId;
|
|
37
|
+
if (isNaN(officerIdNum)) {
|
|
38
|
+
this.logger.warn('Invalid officer ID', { officerId });
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// Try cache first
|
|
42
|
+
try {
|
|
43
|
+
const cachedOfficer = yield this.officerCacheService.getOfficerById(officerIdNum);
|
|
44
|
+
if (cachedOfficer) {
|
|
45
|
+
this.logger.debug(`Officer ${officerIdNum} retrieved from Redis cache`);
|
|
46
|
+
return cachedOfficer;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (cacheError) {
|
|
50
|
+
this.logger.warn('Cache read error, falling back to auth service', {
|
|
51
|
+
error: cacheError instanceof Error ? cacheError.message : String(cacheError),
|
|
52
|
+
officerId: officerIdNum
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Fallback to auth service if host is configured
|
|
56
|
+
if (!this.authHost) {
|
|
57
|
+
this.logger.warn('Auth host not configured, cannot fetch officer from auth service', {
|
|
58
|
+
officerId: officerIdNum
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const authUrl = this.authPort
|
|
63
|
+
? `http://${this.authHost}:${this.authPort}/api/v1/officer/${officerIdNum}`
|
|
64
|
+
: `http://${this.authHost}/api/v1/officer/${officerIdNum}`;
|
|
65
|
+
this.logger.debug(`Cache miss for officer ${officerIdNum}, fetching from auth service`);
|
|
66
|
+
const response = yield fetch(authUrl);
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
if (response.status === 404) {
|
|
69
|
+
this.logger.warn(`Officer ${officerIdNum} not found in auth service`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Failed to fetch officer: ${response.status} ${response.statusText}`);
|
|
73
|
+
}
|
|
74
|
+
const result = yield response.json();
|
|
75
|
+
const officer = result.data || result;
|
|
76
|
+
// Store in cache for future use
|
|
77
|
+
if (officer && officer.id) {
|
|
78
|
+
try {
|
|
79
|
+
yield this.officerCacheService.storeOfficer(officer);
|
|
80
|
+
this.logger.debug(`Officer ${officerIdNum} stored in cache after fetch from auth service`);
|
|
81
|
+
}
|
|
82
|
+
catch (cacheStoreError) {
|
|
83
|
+
// Don't fail if cache store fails, just log
|
|
84
|
+
this.logger.warn('Failed to store officer in cache after fetch', {
|
|
85
|
+
error: cacheStoreError instanceof Error ? cacheStoreError.message : String(cacheStoreError),
|
|
86
|
+
officerId: officerIdNum
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.logger.debug(`Officer ${officerIdNum} fetched from auth service`);
|
|
91
|
+
return officer;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.logger.error('Error fetching officer by ID', {
|
|
95
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
96
|
+
officerId: officerId.toString()
|
|
97
|
+
});
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get officer by service number from Redis cache, fallback to auth service
|
|
104
|
+
*/
|
|
105
|
+
getOfficerByServiceNumber(serviceNumber) {
|
|
106
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
try {
|
|
108
|
+
if (!serviceNumber) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// Try cache first
|
|
112
|
+
try {
|
|
113
|
+
const cachedOfficer = yield this.officerCacheService.getOfficerByServiceNumber(serviceNumber);
|
|
114
|
+
if (cachedOfficer) {
|
|
115
|
+
this.logger.debug(`Officer with service number ${serviceNumber} retrieved from Redis cache`);
|
|
116
|
+
return cachedOfficer;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (cacheError) {
|
|
120
|
+
this.logger.warn('Cache read error for service number lookup, falling back to API', {
|
|
121
|
+
error: cacheError instanceof Error ? cacheError.message : String(cacheError),
|
|
122
|
+
serviceNumber
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Fallback to auth service if host is configured
|
|
126
|
+
if (!this.authHost) {
|
|
127
|
+
this.logger.warn('Auth host not configured, cannot fetch officer from auth service', {
|
|
128
|
+
serviceNumber
|
|
129
|
+
});
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const authUrl = this.authPort
|
|
133
|
+
? `http://${this.authHost}:${this.authPort}/api/v1/officer?service_number=${serviceNumber}`
|
|
134
|
+
: `http://${this.authHost}/api/v1/officer?service_number=${serviceNumber}`;
|
|
135
|
+
this.logger.debug(`Cache miss for officer with service number ${serviceNumber}, fetching from auth service`);
|
|
136
|
+
const response = yield fetch(authUrl);
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
if (response.status === 404) {
|
|
139
|
+
this.logger.warn(`Officer with service number ${serviceNumber} not found in auth service`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Failed to fetch officer: ${response.statusText}`);
|
|
143
|
+
}
|
|
144
|
+
const result = yield response.json();
|
|
145
|
+
const officers = result.data || result;
|
|
146
|
+
const officer = Array.isArray(officers) ? officers[0] : officers;
|
|
147
|
+
if (!officer) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// Store in cache for future use
|
|
151
|
+
if (officer && officer.id) {
|
|
152
|
+
try {
|
|
153
|
+
yield this.officerCacheService.storeOfficer(officer);
|
|
154
|
+
this.logger.debug(`Officer with service number ${serviceNumber} stored in cache after fetch from auth service`);
|
|
155
|
+
}
|
|
156
|
+
catch (cacheStoreError) {
|
|
157
|
+
// Don't fail if cache store fails, just log
|
|
158
|
+
this.logger.warn('Failed to store officer in cache after fetch', {
|
|
159
|
+
error: cacheStoreError instanceof Error ? cacheStoreError.message : String(cacheStoreError),
|
|
160
|
+
serviceNumber
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.logger.debug(`Officer with service number ${serviceNumber} fetched from auth service`);
|
|
165
|
+
return officer;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
this.logger.error('Error fetching officer by service number', {
|
|
169
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
170
|
+
serviceNumber
|
|
171
|
+
});
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
exports.OfficerService = OfficerService;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SharedServicesDependencies } from '../types/dependencies';
|
|
2
|
+
export declare class PenalCodeCacheService {
|
|
3
|
+
private redisClient;
|
|
4
|
+
private logger;
|
|
5
|
+
constructor(deps: SharedServicesDependencies);
|
|
6
|
+
/**
|
|
7
|
+
* Get penal codes from Redis cache
|
|
8
|
+
* @returns Array of penal codes or null if not found/error
|
|
9
|
+
*/
|
|
10
|
+
getPenalCodes(): Promise<any[] | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Store penal codes in Redis cache
|
|
13
|
+
* @param penalCodes - Array of penal codes to store
|
|
14
|
+
*/
|
|
15
|
+
storePenalCodes(penalCodes: any[]): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Clear penal codes from cache
|
|
18
|
+
*/
|
|
19
|
+
clearCache(): Promise<void>;
|
|
20
|
+
}
|