@govish/shared-services 1.0.0 → 1.2.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 1.2.0
4
+
5
+ ### Added
6
+ - **StationDutyBlockCacheService**: Read-only cache for station-duty blocks and sectors
7
+ - Keys: `station_duty_blocks:${stationDutyId}`, `station_duty_block:${blockId}`, `block_sectors:${blockId}`, `block_sector:${sectorId}`
8
+ - `getBlocks(stationDutyId)`, `getBlock(blockId, stationDutyId?)`, `getSectors(blockId)`, `getSector(sectorId, blockId?)`
9
+ - **RankLeaveDaysCacheService**: Read-only cache for rank leave days
10
+ - Keys: `rank_leave_days:${rankId}`, `rank_leave_day:${leaveDayId}`
11
+ - `getLeaveDays(rankId)`, `getLeaveDay(leaveDayId, rankId?)`
12
+
13
+ ---
14
+
15
+ ## Version 1.1.0
16
+
17
+ ### Added
18
+ - **StationCacheService**: Redis-based caching service for police stations
19
+ - Full list as JSON key (`stations`), set of IDs (`stations:all`), per-station hashes (`station:${id}`)
20
+ - `cacheAllPoliceStations(stations)`, `getStationById`, `getAllStations`, `getStationsList`, `getStationsJson`, `clearCache`, `storePoliceStation`
21
+
22
+ ---
23
+
3
24
  ## Version 1.0.0
4
25
 
5
26
  ### Added
package/README.md CHANGED
@@ -28,6 +28,9 @@ const authenticateDeviceOrOfficer = createAuthenticateDeviceOrOfficer(dependenci
28
28
  - **OfficerCacheService**: Redis-based caching service for officers with indexing and efficient queries
29
29
  - **PenalCodeCacheService**: Redis-based caching service for penal codes
30
30
  - **DeviceCacheService**: Redis-based caching service for devices with indexing
31
+ - **StationCacheService**: Redis-based caching service for police stations (JSON list, set of IDs, and per-station hashes)
32
+ - **StationDutyBlockCacheService**: Read-only cache for station-duty blocks and sectors (`getBlocks`, `getBlock`, `getSectors`, `getSector`)
33
+ - **RankLeaveDaysCacheService**: Read-only cache for rank leave days (`getLeaveDays`, `getLeaveDay`)
31
34
  - **ApiKeyService**: Service for managing and validating API keys with Redis caching
32
35
  - **AuditService**: Service for logging audit events to Kafka with comprehensive event tracking
33
36
  - **authenticateDeviceOrOfficer**: Express middleware for authenticating devices, officers, or microservices via API keys
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ export { OfficerService } from './services/officerService';
3
3
  export { PenalCodeCacheService } from './services/penalCodeCacheService';
4
4
  export { DeviceCacheService } from './services/deviceCacheService';
5
5
  export { DeviceService } from './services/deviceService';
6
+ export { StationCacheService } from './services/stationCacheService';
7
+ export { StationDutyBlockCacheService } from './services/stationDutyBlockCacheService';
8
+ export { RankLeaveDaysCacheService } from './services/rankLeaveDaysCacheService';
6
9
  export { ApiKeyService } from './services/apiKeyService';
7
10
  export type { ApiKeyData } from './services/apiKeyService';
8
11
  export { AuditService } from './services/auditService';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.devDebug = exports.devWarn = exports.devError = exports.devLog = exports.isProductionMode = exports.isDevelopmentMode = exports.getEnvironmentMode = exports.createSafeRedisUtils = exports.createSafeRedisSet = exports.createSafeRedisGet = exports.createAuthenticateDeviceOrOfficer = exports.AuditEventType = exports.AuditService = exports.ApiKeyService = exports.DeviceService = exports.DeviceCacheService = exports.PenalCodeCacheService = exports.OfficerService = exports.OfficerCacheService = void 0;
3
+ exports.devDebug = exports.devWarn = exports.devError = exports.devLog = exports.isProductionMode = exports.isDevelopmentMode = exports.getEnvironmentMode = exports.createSafeRedisUtils = exports.createSafeRedisSet = exports.createSafeRedisGet = exports.createAuthenticateDeviceOrOfficer = exports.AuditEventType = exports.AuditService = exports.ApiKeyService = exports.RankLeaveDaysCacheService = exports.StationDutyBlockCacheService = exports.StationCacheService = exports.DeviceService = exports.DeviceCacheService = exports.PenalCodeCacheService = exports.OfficerService = exports.OfficerCacheService = void 0;
4
4
  // Export services
5
5
  var officerCacheService_1 = require("./services/officerCacheService");
6
6
  Object.defineProperty(exports, "OfficerCacheService", { enumerable: true, get: function () { return officerCacheService_1.OfficerCacheService; } });
@@ -12,6 +12,12 @@ var deviceCacheService_1 = require("./services/deviceCacheService");
12
12
  Object.defineProperty(exports, "DeviceCacheService", { enumerable: true, get: function () { return deviceCacheService_1.DeviceCacheService; } });
13
13
  var deviceService_1 = require("./services/deviceService");
14
14
  Object.defineProperty(exports, "DeviceService", { enumerable: true, get: function () { return deviceService_1.DeviceService; } });
15
+ var stationCacheService_1 = require("./services/stationCacheService");
16
+ Object.defineProperty(exports, "StationCacheService", { enumerable: true, get: function () { return stationCacheService_1.StationCacheService; } });
17
+ var stationDutyBlockCacheService_1 = require("./services/stationDutyBlockCacheService");
18
+ Object.defineProperty(exports, "StationDutyBlockCacheService", { enumerable: true, get: function () { return stationDutyBlockCacheService_1.StationDutyBlockCacheService; } });
19
+ var rankLeaveDaysCacheService_1 = require("./services/rankLeaveDaysCacheService");
20
+ Object.defineProperty(exports, "RankLeaveDaysCacheService", { enumerable: true, get: function () { return rankLeaveDaysCacheService_1.RankLeaveDaysCacheService; } });
15
21
  var apiKeyService_1 = require("./services/apiKeyService");
16
22
  Object.defineProperty(exports, "ApiKeyService", { enumerable: true, get: function () { return apiKeyService_1.ApiKeyService; } });
17
23
  var auditService_1 = require("./services/auditService");
@@ -0,0 +1,19 @@
1
+ import { SharedServicesDependencies } from '../types/dependencies';
2
+ /**
3
+ * Read-only cache service for rank leave days.
4
+ * Only provides get-from-cache; the app is responsible for storing and invalidating.
5
+ */
6
+ export declare class RankLeaveDaysCacheService {
7
+ private redisClient;
8
+ private logger;
9
+ constructor(deps: SharedServicesDependencies);
10
+ /**
11
+ * Get leave days for a rank from cache. Returns empty array on miss.
12
+ */
13
+ getLeaveDays(rankId: number): Promise<Array<Record<string, unknown>>>;
14
+ /**
15
+ * Get one leave day by ID from cache. Returns null on miss.
16
+ * If rankId is provided, returns null when cached leave day's rank_id does not match.
17
+ */
18
+ getLeaveDay(leaveDayId: number, rankId?: number): Promise<Record<string, unknown> | null>;
19
+ }
@@ -0,0 +1,89 @@
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.RankLeaveDaysCacheService = void 0;
13
+ /** Redis keys (must match app that writes: rank_leave_days, rank_leave_day) */
14
+ const RANK_LEAVE_DAYS_KEY = (rankId) => `rank_leave_days:${rankId}`;
15
+ const RANK_LEAVE_DAY_KEY = (leaveDayId) => `rank_leave_day:${leaveDayId}`;
16
+ function safeJsonParse(value, fallback, logger) {
17
+ if (!value || value === '' || value === 'null')
18
+ return fallback;
19
+ try {
20
+ return JSON.parse(value);
21
+ }
22
+ catch (e) {
23
+ logger.warn('Failed to parse rank leave days cache JSON', {
24
+ error: e instanceof Error ? e.message : String(e),
25
+ });
26
+ return fallback;
27
+ }
28
+ }
29
+ /**
30
+ * Read-only cache service for rank leave days.
31
+ * Only provides get-from-cache; the app is responsible for storing and invalidating.
32
+ */
33
+ class RankLeaveDaysCacheService {
34
+ constructor(deps) {
35
+ this.redisClient = deps.redisClient;
36
+ this.logger = deps.logger;
37
+ }
38
+ /**
39
+ * Get leave days for a rank from cache. Returns empty array on miss.
40
+ */
41
+ getLeaveDays(rankId) {
42
+ return __awaiter(this, void 0, void 0, function* () {
43
+ var _a;
44
+ try {
45
+ const key = RANK_LEAVE_DAYS_KEY(rankId);
46
+ const cached = yield this.redisClient.get(key);
47
+ if (!cached)
48
+ return [];
49
+ return (_a = safeJsonParse(cached, [], this.logger)) !== null && _a !== void 0 ? _a : [];
50
+ }
51
+ catch (error) {
52
+ this.logger.error('Error getting rank leave days from cache', {
53
+ error: error instanceof Error ? error.message : String(error),
54
+ rankId,
55
+ });
56
+ return [];
57
+ }
58
+ });
59
+ }
60
+ /**
61
+ * Get one leave day by ID from cache. Returns null on miss.
62
+ * If rankId is provided, returns null when cached leave day's rank_id does not match.
63
+ */
64
+ getLeaveDay(leaveDayId, rankId) {
65
+ return __awaiter(this, void 0, void 0, function* () {
66
+ try {
67
+ const key = RANK_LEAVE_DAY_KEY(leaveDayId);
68
+ const cached = yield this.redisClient.get(key);
69
+ if (!cached)
70
+ return null;
71
+ const parsed = safeJsonParse(cached, null, this.logger);
72
+ if (!parsed)
73
+ return null;
74
+ if (rankId !== undefined && Number(parsed.rank_id) !== rankId)
75
+ return null;
76
+ return parsed;
77
+ }
78
+ catch (error) {
79
+ this.logger.error('Error getting rank leave day from cache', {
80
+ error: error instanceof Error ? error.message : String(error),
81
+ leaveDayId,
82
+ rankId,
83
+ });
84
+ return null;
85
+ }
86
+ });
87
+ }
88
+ }
89
+ exports.RankLeaveDaysCacheService = RankLeaveDaysCacheService;
@@ -0,0 +1,41 @@
1
+ import { SharedServicesDependencies } from '../types/dependencies';
2
+ export declare class StationCacheService {
3
+ private redisClient;
4
+ private logger;
5
+ constructor(deps: SharedServicesDependencies);
6
+ /**
7
+ * Store individual station as Redis hash (for hGetAll lookup).
8
+ * Nested objects (subCounty, county, region, Officer) are JSON-stringified.
9
+ */
10
+ storePoliceStation(station: any): Promise<void>;
11
+ /**
12
+ * Get station by ID from cache (O(1) hash lookup).
13
+ */
14
+ getStationById(id: number): Promise<any | null>;
15
+ /**
16
+ * Get all station IDs from the "stations:all" set.
17
+ */
18
+ getAllStationIds(): Promise<number[]>;
19
+ /**
20
+ * Get all stations from cache (hashes), sorted by id desc.
21
+ */
22
+ getAllStations(): Promise<any[]>;
23
+ /**
24
+ * Get the full stations list as JSON string (same as redisClient.get('stations')).
25
+ * Use when you need the exact payload that was stored by cacheAllPoliceStations.
26
+ */
27
+ getStationsJson(): Promise<string | null>;
28
+ /**
29
+ * Get parsed stations array from the JSON key (convenience for list API).
30
+ */
31
+ getStationsList(): Promise<any[]>;
32
+ /**
33
+ * Clear all station-related keys: stations, stations:all, and every station:${id} hash.
34
+ */
35
+ clearCache(): Promise<void>;
36
+ /**
37
+ * Cache all police stations: clear cache, store full list as JSON, store IDs in set, store each station as hash.
38
+ * Caller must provide the stations array (e.g. from Prisma or auth API).
39
+ */
40
+ cacheAllPoliceStations(stations: any[]): Promise<any[]>;
41
+ }
@@ -0,0 +1,280 @@
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.StationCacheService = void 0;
13
+ /** Redis keys used by this service */
14
+ const KEY_JSON = 'stations';
15
+ const KEY_ALL_IDS = 'stations:all';
16
+ const KEY_STATION_HASH = (id) => `station:${id}`;
17
+ class StationCacheService {
18
+ constructor(deps) {
19
+ this.redisClient = deps.redisClient;
20
+ this.logger = deps.logger;
21
+ }
22
+ /**
23
+ * Store individual station as Redis hash (for hGetAll lookup).
24
+ * Nested objects (subCounty, county, region, Officer) are JSON-stringified.
25
+ */
26
+ storePoliceStation(station) {
27
+ return __awaiter(this, void 0, void 0, function* () {
28
+ var _a, _b, _c, _d, _e, _f, _g, _h;
29
+ try {
30
+ const key = KEY_STATION_HASH(station.id);
31
+ const safeDate = (date) => {
32
+ if (!date)
33
+ return '';
34
+ if (date instanceof Date)
35
+ return isNaN(date.getTime()) ? '' : date.toISOString();
36
+ if (typeof date === 'string')
37
+ return date;
38
+ return '';
39
+ };
40
+ const hashData = {
41
+ id: station.id.toString(),
42
+ name: (station.name || '').toString(),
43
+ station_code: (station.station_code || '').toString(),
44
+ subCountyId: ((_b = (_a = station.subCountyId) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : '').toString(),
45
+ countyId: ((_d = (_c = station.countyId) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : '').toString(),
46
+ regionId: ((_f = (_e = station.regionId) === null || _e === void 0 ? void 0 : _e.toString()) !== null && _f !== void 0 ? _f : '').toString(),
47
+ created_at: safeDate(station.created_at),
48
+ updated_at: safeDate(station.updated_at),
49
+ };
50
+ try {
51
+ hashData.subCounty = JSON.stringify((_g = station.subCounty) !== null && _g !== void 0 ? _g : null);
52
+ }
53
+ catch (_j) {
54
+ hashData.subCounty = 'null';
55
+ }
56
+ try {
57
+ hashData.Officer = JSON.stringify((_h = station.Officer) !== null && _h !== void 0 ? _h : null);
58
+ }
59
+ catch (_k) {
60
+ hashData.Officer = 'null';
61
+ }
62
+ yield this.redisClient.hSet(key, hashData);
63
+ }
64
+ catch (error) {
65
+ this.logger.error('Error storing station in cache', {
66
+ error: error instanceof Error ? error.message : String(error),
67
+ stationId: station === null || station === void 0 ? void 0 : station.id,
68
+ });
69
+ throw error;
70
+ }
71
+ });
72
+ }
73
+ /**
74
+ * Get station by ID from cache (O(1) hash lookup).
75
+ */
76
+ getStationById(id) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ try {
79
+ const raw = yield this.redisClient.hGetAll(KEY_STATION_HASH(id));
80
+ const data = raw instanceof Map ? Object.fromEntries(raw) : raw;
81
+ if (!(data === null || data === void 0 ? void 0 : data.id))
82
+ return null;
83
+ const parseJson = (s) => {
84
+ if (!s || s === 'null' || s === '"null"')
85
+ return null;
86
+ try {
87
+ return JSON.parse(s);
88
+ }
89
+ catch (_a) {
90
+ return null;
91
+ }
92
+ };
93
+ return Object.assign(Object.assign({}, data), { id: parseInt(data.id, 10), subCountyId: data.subCountyId ? parseInt(data.subCountyId, 10) : null, countyId: data.countyId ? parseInt(data.countyId, 10) : null, regionId: data.regionId ? parseInt(data.regionId, 10) : null, created_at: data.created_at ? new Date(data.created_at) : null, updated_at: data.updated_at ? new Date(data.updated_at) : null, subCounty: parseJson(data.subCounty), Officer: parseJson(data.Officer) });
94
+ }
95
+ catch (error) {
96
+ this.logger.error('Error getting station from cache', {
97
+ error: error instanceof Error ? error.message : String(error),
98
+ stationId: id,
99
+ });
100
+ return null;
101
+ }
102
+ });
103
+ }
104
+ /**
105
+ * Get all station IDs from the "stations:all" set.
106
+ */
107
+ getAllStationIds() {
108
+ return __awaiter(this, void 0, void 0, function* () {
109
+ try {
110
+ const ids = yield this.redisClient.sMembers(KEY_ALL_IDS);
111
+ return ids.map((id) => parseInt(id, 10)).filter((n) => !Number.isNaN(n));
112
+ }
113
+ catch (error) {
114
+ this.logger.error('Error getting station IDs from cache', {
115
+ error: error instanceof Error ? error.message : String(error),
116
+ });
117
+ return [];
118
+ }
119
+ });
120
+ }
121
+ /**
122
+ * Get all stations from cache (hashes), sorted by id desc.
123
+ */
124
+ getAllStations() {
125
+ return __awaiter(this, void 0, void 0, function* () {
126
+ try {
127
+ const ids = yield this.getAllStationIds();
128
+ if (ids.length === 0)
129
+ return [];
130
+ const pipeline = this.redisClient.multi();
131
+ ids.forEach((id) => pipeline.hGetAll(KEY_STATION_HASH(id)));
132
+ const results = yield pipeline.exec();
133
+ if (!(results === null || results === void 0 ? void 0 : results.length))
134
+ return [];
135
+ const parseJson = (s) => {
136
+ if (!s || s === 'null' || s === '"null"')
137
+ return null;
138
+ try {
139
+ return JSON.parse(s);
140
+ }
141
+ catch (_a) {
142
+ return null;
143
+ }
144
+ };
145
+ const stations = results
146
+ .map((result, index) => {
147
+ let raw;
148
+ if (Array.isArray(result) && result.length === 2) {
149
+ const [err, data] = result;
150
+ if (err)
151
+ return null;
152
+ raw = data;
153
+ }
154
+ else {
155
+ raw = result;
156
+ }
157
+ const data = raw instanceof Map ? Object.fromEntries(raw) : raw;
158
+ if (!(data === null || data === void 0 ? void 0 : data.id))
159
+ return null;
160
+ return Object.assign(Object.assign({}, data), { id: parseInt(data.id, 10), subCountyId: data.subCountyId ? parseInt(data.subCountyId, 10) : null, countyId: data.countyId ? parseInt(data.countyId, 10) : null, regionId: data.regionId ? parseInt(data.regionId, 10) : null, created_at: data.created_at ? new Date(data.created_at) : null, updated_at: data.updated_at ? new Date(data.updated_at) : null, subCounty: parseJson(data.subCounty), Officer: parseJson(data.Officer) });
161
+ })
162
+ .filter(Boolean);
163
+ return stations.sort((a, b) => b.id - a.id);
164
+ }
165
+ catch (error) {
166
+ this.logger.error('Error getting all stations from cache', {
167
+ error: error instanceof Error ? error.message : String(error),
168
+ });
169
+ return [];
170
+ }
171
+ });
172
+ }
173
+ /**
174
+ * Get the full stations list as JSON string (same as redisClient.get('stations')).
175
+ * Use when you need the exact payload that was stored by cacheAllPoliceStations.
176
+ */
177
+ getStationsJson() {
178
+ return __awaiter(this, void 0, void 0, function* () {
179
+ try {
180
+ return yield this.redisClient.get(KEY_JSON);
181
+ }
182
+ catch (error) {
183
+ this.logger.error('Error getting stations JSON from cache', {
184
+ error: error instanceof Error ? error.message : String(error),
185
+ });
186
+ return null;
187
+ }
188
+ });
189
+ }
190
+ /**
191
+ * Get parsed stations array from the JSON key (convenience for list API).
192
+ */
193
+ getStationsList() {
194
+ return __awaiter(this, void 0, void 0, function* () {
195
+ const json = yield this.getStationsJson();
196
+ if (!json)
197
+ return [];
198
+ try {
199
+ const parsed = JSON.parse(json);
200
+ return Array.isArray(parsed) ? parsed : [];
201
+ }
202
+ catch (_a) {
203
+ return [];
204
+ }
205
+ });
206
+ }
207
+ /**
208
+ * Clear all station-related keys: stations, stations:all, and every station:${id} hash.
209
+ */
210
+ clearCache() {
211
+ return __awaiter(this, void 0, void 0, function* () {
212
+ try {
213
+ const ids = yield this.redisClient.sMembers(KEY_ALL_IDS);
214
+ if (ids.length > 0) {
215
+ const pipeline = this.redisClient.multi();
216
+ ids.forEach((id) => pipeline.del(KEY_STATION_HASH(parseInt(id, 10))));
217
+ yield pipeline.exec();
218
+ }
219
+ yield this.redisClient.del(KEY_ALL_IDS);
220
+ yield this.redisClient.del(KEY_JSON);
221
+ }
222
+ catch (error) {
223
+ this.logger.error('Error clearing station cache', {
224
+ error: error instanceof Error ? error.message : String(error),
225
+ });
226
+ }
227
+ });
228
+ }
229
+ /**
230
+ * Cache all police stations: clear cache, store full list as JSON, store IDs in set, store each station as hash.
231
+ * Caller must provide the stations array (e.g. from Prisma or auth API).
232
+ */
233
+ cacheAllPoliceStations(stations) {
234
+ return __awaiter(this, void 0, void 0, function* () {
235
+ try {
236
+ yield this.clearCache();
237
+ yield this.redisClient.set(KEY_JSON, JSON.stringify(stations));
238
+ const stationIds = stations.map((s) => s.id.toString());
239
+ if (stationIds.length > 0) {
240
+ try {
241
+ const keyType = yield this.redisClient.type(KEY_ALL_IDS);
242
+ if (keyType !== 'set' && keyType !== 'none') {
243
+ yield this.redisClient.del(KEY_ALL_IDS);
244
+ }
245
+ }
246
+ catch (_a) {
247
+ // ignore
248
+ }
249
+ yield this.redisClient.sAdd(KEY_ALL_IDS, stationIds);
250
+ }
251
+ for (const station of stations) {
252
+ yield this.storePoliceStation(station);
253
+ }
254
+ if (stations.length > 0) {
255
+ const sampleId = stations[0].id;
256
+ const sampleHash = yield this.redisClient.hGetAll(KEY_STATION_HASH(sampleId));
257
+ if (!sampleHash || !sampleHash.id) {
258
+ this.logger.warn('Hash storage verification failed for sample station', { stationId: sampleId });
259
+ }
260
+ }
261
+ this.logger.info('Police stations cached successfully', {
262
+ stationCount: stations.length,
263
+ storageTypes: {
264
+ json: `${KEY_JSON} (JSON string)`,
265
+ hash: `station:\${id} (Redis hash - hGetAll)`,
266
+ set: `${KEY_ALL_IDS} (Redis set)`,
267
+ },
268
+ });
269
+ return stations;
270
+ }
271
+ catch (error) {
272
+ this.logger.error('Error caching police stations', {
273
+ error: error instanceof Error ? error.message : String(error),
274
+ });
275
+ throw error;
276
+ }
277
+ });
278
+ }
279
+ }
280
+ exports.StationCacheService = StationCacheService;
@@ -0,0 +1,28 @@
1
+ import { SharedServicesDependencies } from '../types/dependencies';
2
+ /**
3
+ * Read-only cache service for station-duty blocks and sectors.
4
+ * Only provides get-from-cache; the app is responsible for storing and invalidating.
5
+ */
6
+ export declare class StationDutyBlockCacheService {
7
+ private redisClient;
8
+ private logger;
9
+ constructor(deps: SharedServicesDependencies);
10
+ /**
11
+ * Get blocks for a station-duty from cache. Returns null on miss.
12
+ */
13
+ getBlocks(stationDutyId: number): Promise<Array<Record<string, unknown>> | null>;
14
+ /**
15
+ * Get one block by ID from cache. Returns null on miss.
16
+ * If stationDutyId is provided, returns null when cached block's station_duty_id does not match.
17
+ */
18
+ getBlock(blockId: number, stationDutyId?: number): Promise<Record<string, unknown> | null>;
19
+ /**
20
+ * Get sectors for a block from cache. Returns empty array on miss.
21
+ */
22
+ getSectors(blockId: number): Promise<Array<Record<string, unknown>>>;
23
+ /**
24
+ * Get one sector by ID from cache. Returns null on miss.
25
+ * If blockId is provided, returns null when cached sector's block_id does not match.
26
+ */
27
+ getSector(sectorId: number, blockId?: number): Promise<Record<string, unknown> | null>;
28
+ }
@@ -0,0 +1,138 @@
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.StationDutyBlockCacheService = void 0;
13
+ /** Redis keys (must match app that writes: station_duty_blocks, station_duty_block, block_sectors, block_sector) */
14
+ const BLOCKS_KEY = (stationDutyId) => `station_duty_blocks:${stationDutyId}`;
15
+ const BLOCK_KEY = (blockId) => `station_duty_block:${blockId}`;
16
+ const SECTORS_KEY = (blockId) => `block_sectors:${blockId}`;
17
+ const SECTOR_KEY = (sectorId) => `block_sector:${sectorId}`;
18
+ function safeJsonParse(value, fallback, logger) {
19
+ if (!value || value === '' || value === 'null')
20
+ return fallback;
21
+ try {
22
+ return JSON.parse(value);
23
+ }
24
+ catch (e) {
25
+ logger.warn('Failed to parse cache JSON', { error: e instanceof Error ? e.message : String(e) });
26
+ return fallback;
27
+ }
28
+ }
29
+ /**
30
+ * Read-only cache service for station-duty blocks and sectors.
31
+ * Only provides get-from-cache; the app is responsible for storing and invalidating.
32
+ */
33
+ class StationDutyBlockCacheService {
34
+ constructor(deps) {
35
+ this.redisClient = deps.redisClient;
36
+ this.logger = deps.logger;
37
+ }
38
+ /**
39
+ * Get blocks for a station-duty from cache. Returns null on miss.
40
+ */
41
+ getBlocks(stationDutyId) {
42
+ return __awaiter(this, void 0, void 0, function* () {
43
+ try {
44
+ const key = BLOCKS_KEY(stationDutyId);
45
+ const cached = yield this.redisClient.get(key);
46
+ if (!cached)
47
+ return null;
48
+ return safeJsonParse(cached, null, this.logger);
49
+ }
50
+ catch (error) {
51
+ this.logger.error('Error getting blocks from cache', {
52
+ error: error instanceof Error ? error.message : String(error),
53
+ stationDutyId,
54
+ });
55
+ return null;
56
+ }
57
+ });
58
+ }
59
+ /**
60
+ * Get one block by ID from cache. Returns null on miss.
61
+ * If stationDutyId is provided, returns null when cached block's station_duty_id does not match.
62
+ */
63
+ getBlock(blockId, stationDutyId) {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ try {
66
+ const key = BLOCK_KEY(blockId);
67
+ const cached = yield this.redisClient.get(key);
68
+ if (!cached)
69
+ return null;
70
+ const parsed = safeJsonParse(cached, null, this.logger);
71
+ if (!parsed)
72
+ return null;
73
+ if (stationDutyId !== undefined && Number(parsed.station_duty_id) !== stationDutyId)
74
+ return null;
75
+ return parsed;
76
+ }
77
+ catch (error) {
78
+ this.logger.error('Error getting block from cache', {
79
+ error: error instanceof Error ? error.message : String(error),
80
+ blockId,
81
+ stationDutyId,
82
+ });
83
+ return null;
84
+ }
85
+ });
86
+ }
87
+ /**
88
+ * Get sectors for a block from cache. Returns empty array on miss.
89
+ */
90
+ getSectors(blockId) {
91
+ return __awaiter(this, void 0, void 0, function* () {
92
+ var _a;
93
+ try {
94
+ const key = SECTORS_KEY(blockId);
95
+ const cached = yield this.redisClient.get(key);
96
+ if (!cached)
97
+ return [];
98
+ return (_a = safeJsonParse(cached, [], this.logger)) !== null && _a !== void 0 ? _a : [];
99
+ }
100
+ catch (error) {
101
+ this.logger.error('Error getting sectors from cache', {
102
+ error: error instanceof Error ? error.message : String(error),
103
+ blockId,
104
+ });
105
+ return [];
106
+ }
107
+ });
108
+ }
109
+ /**
110
+ * Get one sector by ID from cache. Returns null on miss.
111
+ * If blockId is provided, returns null when cached sector's block_id does not match.
112
+ */
113
+ getSector(sectorId, blockId) {
114
+ return __awaiter(this, void 0, void 0, function* () {
115
+ try {
116
+ const key = SECTOR_KEY(sectorId);
117
+ const cached = yield this.redisClient.get(key);
118
+ if (!cached)
119
+ return null;
120
+ const parsed = safeJsonParse(cached, null, this.logger);
121
+ if (!parsed)
122
+ return null;
123
+ if (blockId !== undefined && Number(parsed.block_id) !== blockId)
124
+ return null;
125
+ return parsed;
126
+ }
127
+ catch (error) {
128
+ this.logger.error('Error getting sector from cache', {
129
+ error: error instanceof Error ? error.message : String(error),
130
+ sectorId,
131
+ blockId,
132
+ });
133
+ return null;
134
+ }
135
+ });
136
+ }
137
+ }
138
+ exports.StationDutyBlockCacheService = StationDutyBlockCacheService;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@govish/shared-services",
3
- "version": "1.0.0",
4
- "description": "Govish shared services package - includes cache services, API key service, audit logging, and authentication middleware",
3
+ "version": "1.2.0",
4
+ "description": "Govish shared services package - officer, device, station, penal code, station-duty block, and rank leave days cache services; API key service; audit logging; authentication middleware",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [