@govish/shared-services 1.0.0 → 1.1.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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 1.1.0
4
+
5
+ ### Added
6
+ - **StationCacheService**: Redis-based caching service for police stations
7
+ - Full list as JSON key (`stations`), set of IDs (`stations:all`), per-station hashes (`station:${id}`)
8
+ - `cacheAllPoliceStations(stations)`, `getStationById`, `getAllStations`, `getStationsList`, `getStationsJson`, `clearCache`, `storePoliceStation`
9
+
10
+ ---
11
+
3
12
  ## Version 1.0.0
4
13
 
5
14
  ### Added
package/README.md CHANGED
@@ -28,6 +28,7 @@ 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)
31
32
  - **ApiKeyService**: Service for managing and validating API keys with Redis caching
32
33
  - **AuditService**: Service for logging audit events to Kafka with comprehensive event tracking
33
34
  - **authenticateDeviceOrOfficer**: Express middleware for authenticating devices, officers, or microservices via API keys
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ 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';
6
7
  export { ApiKeyService } from './services/apiKeyService';
7
8
  export type { ApiKeyData } from './services/apiKeyService';
8
9
  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.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,8 @@ 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; } });
15
17
  var apiKeyService_1 = require("./services/apiKeyService");
16
18
  Object.defineProperty(exports, "ApiKeyService", { enumerable: true, get: function () { return apiKeyService_1.ApiKeyService; } });
17
19
  var auditService_1 = require("./services/auditService");
@@ -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;
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.1.0",
4
+ "description": "Govish shared services package - includes officer, device, station, and penal code cache services, API key service, audit logging, and authentication middleware",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [