@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.
@@ -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
+ }