@ikonintegration/module-config-api 2.0.16 → 2.0.18

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/CacheClient.ts ADDED
@@ -0,0 +1,251 @@
1
+ // @ts-nocheck — This file is compiled in CI alongside generated sources (Api.js, data-contracts.js)
2
+ import { Api } from './Api.js'
3
+ import { CustomerResponse, TenantResponse } from './data-contracts.js'
4
+ import { MemoryCache } from './MemoryCache.js'
5
+ import { RedisLike, CacheConfig, CacheConstants } from './CacheTypes.js'
6
+
7
+ /**
8
+ * Extended client that wraps the auto-generated Api with Redis cache support.
9
+ * Provides direct Redis-backed lookups for customers, tenants, certifications, and courses.
10
+ *
11
+ * Usage:
12
+ * ```ts
13
+ * import { CacheClient } from "@ikonintegration/module-config-api/cache";
14
+ *
15
+ * const client = new CacheClient({
16
+ * // API layer (passed to the generated Api client)
17
+ * baseUrl: "https://api.example.com",
18
+ * // Cache layer (optional — enables Redis-backed lookups)
19
+ * cache: {
20
+ * endpoint: "redis.example.com",
21
+ * username: "default",
22
+ * password: "secret",
23
+ * enableTLS: true,
24
+ * version: 5
25
+ * }
26
+ * });
27
+ * ```
28
+ */
29
+ export class CacheClient {
30
+ public api: Api<unknown>
31
+ private _localCache: MemoryCache
32
+ private _remoteCache: RedisLike | null = null
33
+ private _cacheConfig: CacheConfig | null
34
+
35
+ constructor(options: {
36
+ baseUrl?: string
37
+ baseApiParams?: ConstructorParameters<typeof Api>[0]
38
+ cache?: CacheConfig
39
+ }) {
40
+ this.api = new Api({
41
+ baseURL: options.baseUrl,
42
+ ...options.baseApiParams,
43
+ })
44
+ this._localCache = new MemoryCache()
45
+ this._cacheConfig = options.cache || null
46
+ }
47
+
48
+ /********* CACHE LAYER — Customers *********/
49
+
50
+ async getAllCustomers(): Promise<CustomerResponse[] | null> {
51
+ let allCustomers = this._localCache.getValue<CustomerResponse[]>(CacheConstants.CustomerPrefix)
52
+ if (!allCustomers) {
53
+ console.debug('ConfigClient - Customers Fetch All')
54
+ allCustomers = await this._getAllRemoteCustomers()
55
+ if (!allCustomers) return null
56
+ this._localCache.setValue(CacheConstants.CustomerPrefix, allCustomers, CacheConstants.AllCustomersTTL)
57
+ }
58
+ return allCustomers
59
+ }
60
+
61
+ async getCustomerByID(customerID: string): Promise<CustomerResponse | null> {
62
+ // Attempt local cache
63
+ let customer = this._localCache.getValue<CustomerResponse>(`${CacheConstants.CustomerPrefix}${customerID}`)
64
+ // Attempt from allCustomers local cache
65
+ if (!customer) {
66
+ const allCustomers = this._localCache.getValue<CustomerResponse[]>(CacheConstants.CustomerPrefix)
67
+ customer = allCustomers?.find(c => c.id === customerID) || null
68
+ }
69
+ // Attempt remote cache
70
+ if (!customer) {
71
+ console.debug('ConfigClient - Customer Fetch:', customerID)
72
+ await this._initCacheClient()
73
+ const raw = await this._remoteCache?.get(`${CacheConstants.CustomerPrefix}${customerID}`)
74
+ if (!raw) return null
75
+ customer = JSON.parse(raw)
76
+ this._localCache.setValue(`${CacheConstants.CustomerPrefix}${customerID}`, customer, CacheConstants.CustomerTTL)
77
+ }
78
+ return customer
79
+ }
80
+
81
+ /********* CACHE LAYER — Tenants *********/
82
+
83
+ async getAllTenants(): Promise<TenantResponse[] | null> {
84
+ let allTenants = this._localCache.getValue<TenantResponse[]>(CacheConstants.TenantPrefix)
85
+ if (!allTenants) {
86
+ console.debug('ConfigClient - Tenants Fetch All')
87
+ allTenants = await this._getAllRemoteTenants()
88
+ if (!allTenants) return null
89
+ this._localCache.setValue(CacheConstants.TenantPrefix, allTenants, CacheConstants.AllTenantsTTL)
90
+ }
91
+ return allTenants
92
+ }
93
+
94
+ async getTenantByID(tenantID: string): Promise<TenantResponse | null> {
95
+ let tenant = this._localCache.getValue<TenantResponse>(`${CacheConstants.TenantPrefix}${tenantID}`)
96
+ if (!tenant) {
97
+ console.debug('ConfigClient - Tenant Fetch:', tenantID)
98
+ await this._initCacheClient()
99
+ const raw = await this._remoteCache?.get(`${CacheConstants.TenantPrefix}${tenantID}`)
100
+ if (!raw) return null
101
+ tenant = JSON.parse(raw)
102
+ this._localCache.setValue(`${CacheConstants.TenantPrefix}${tenantID}`, tenant, CacheConstants.TenantTTL)
103
+ }
104
+ return tenant
105
+ }
106
+
107
+ /********* CACHE LAYER — Certifications & Courses *********/
108
+
109
+ async getCertificationByID(certID: string, tenantID: string) {
110
+ const tenant = await this.getTenantByID(tenantID)
111
+ if (!tenant || !Array.isArray((tenant as any).certifications)) return null
112
+ return (tenant as any).certifications.find((c: any) => c.id === certID) || null
113
+ }
114
+
115
+ async getCourseByID(courseID: string, tenantID: string) {
116
+ const tenant = await this.getTenantByID(tenantID)
117
+ if (!tenant || !Array.isArray((tenant as any).certifications)) return null
118
+ for (const cert of (tenant as any).certifications) {
119
+ for (const req of cert.requirements || []) {
120
+ if (Array.isArray(req)) {
121
+ const found = req.find((r: any) => r.id === courseID)
122
+ if (found) return found
123
+ } else if (req.id === courseID) return req
124
+ }
125
+ }
126
+ return null
127
+ }
128
+
129
+ async getCourseInCertificationByID(certificationID: string, courseID: string, tenantID: string) {
130
+ const tenant = await this.getTenantByID(tenantID)
131
+ if (!tenant || !Array.isArray((tenant as any).certifications)) return null
132
+ const cert = (tenant as any).certifications.find((c: any) => c.id === certificationID)
133
+ if (!cert) return null
134
+ for (const req of cert.requirements || []) {
135
+ if (Array.isArray(req)) {
136
+ const found = req.find((r: any) => r.id === courseID)
137
+ if (found) return found
138
+ } else if (req.id === courseID) return req
139
+ }
140
+ return null
141
+ }
142
+
143
+ async getAllCoursesByTenantID(tenantID: string) {
144
+ const tenant = await this.getTenantByID(tenantID)
145
+ if (!tenant || !Array.isArray((tenant as any).certifications)) return null
146
+ const courses: any[] = []
147
+ for (const cert of (tenant as any).certifications) {
148
+ for (const req of cert.requirements || []) {
149
+ if (Array.isArray(req)) {
150
+ for (const sub of req) courses.push(sub)
151
+ } else courses.push(req)
152
+ }
153
+ }
154
+ return courses
155
+ }
156
+
157
+ async getAllCoursesByCertificationID(certID: string, tenantID: string) {
158
+ const tenant = await this.getTenantByID(tenantID)
159
+ if (!tenant || !Array.isArray((tenant as any).certifications)) return null
160
+ const courses: any[] = []
161
+ for (const cert of (tenant as any).certifications) {
162
+ if (cert.id !== certID) continue
163
+ for (const req of cert.requirements || []) {
164
+ if (Array.isArray(req)) {
165
+ for (const sub of req) courses.push(sub)
166
+ } else courses.push(req)
167
+ }
168
+ }
169
+ return courses
170
+ }
171
+
172
+ async getAllCourses() {
173
+ return this.getAllCoursesByCustomerID(null)
174
+ }
175
+
176
+ async getAllCoursesByCustomerID(customerID: string | null) {
177
+ const allTenants = await this.getAllTenants()
178
+ if (!allTenants) return []
179
+ const filtered = allTenants.filter(t => !customerID || (t as any).customerID === customerID)
180
+ const courses: any[] = []
181
+ for (const tenant of filtered) {
182
+ if (!Array.isArray((tenant as any).certifications)) continue
183
+ for (const cert of (tenant as any).certifications) {
184
+ for (const req of cert.requirements || []) {
185
+ if (Array.isArray(req)) {
186
+ for (const sub of req) {
187
+ if (!courses.find(c => c.id === sub.id)) courses.push(sub)
188
+ }
189
+ } else {
190
+ if (!courses.find(c => c.id === req.id)) courses.push(req)
191
+ }
192
+ }
193
+ }
194
+ }
195
+ return courses
196
+ }
197
+
198
+ /********* CACHE LAYER — Course Providers *********/
199
+
200
+ async getCourseProviderByIDs(courseProviderID: string, customerID: string) {
201
+ const customer = await this.getCustomerByID(customerID)
202
+ if (!customer || !Array.isArray((customer as any).courseProviders)) return null
203
+ return (customer as any).courseProviders.find((p: any) => p === courseProviderID) || null
204
+ }
205
+
206
+ /********* Private *********/
207
+
208
+ private async _getAllRemoteCustomers(): Promise<CustomerResponse[] | null> {
209
+ await this._initCacheClient()
210
+ if (!this._remoteCache) return null
211
+ const allKeys = await this._remoteCache.keys(`${CacheConstants.CustomerPrefix}*`)
212
+ if (!allKeys || allKeys.length === 0) return null
213
+ const raw = await this._remoteCache.mGet(allKeys)
214
+ return Array.isArray(raw) ? raw.map(r => JSON.parse(r)) : null
215
+ }
216
+
217
+ private async _getAllRemoteTenants(): Promise<TenantResponse[] | null> {
218
+ await this._initCacheClient()
219
+ if (!this._remoteCache) return null
220
+ const allKeys = await this._remoteCache.keys(`${CacheConstants.TenantPrefix}*`)
221
+ if (!allKeys || allKeys.length === 0) return null
222
+ const raw = await this._remoteCache.mGet(allKeys)
223
+ return Array.isArray(raw) ? raw.map(r => JSON.parse(r)) : null
224
+ }
225
+
226
+ private async _initCacheClient() {
227
+ if (this._remoteCache || !this._cacheConfig) return
228
+ const config = this._cacheConfig
229
+
230
+ if (config.version === 5) {
231
+ const dynamicImport = ['@ikonintegration/ikapi']
232
+ const { Redis } = await import(dynamicImport[0])
233
+ this._remoteCache = await Redis.connection({
234
+ hostname: config.endpoint,
235
+ username: config.username,
236
+ password: config.password,
237
+ enableTLS: config.enableTLS ?? true,
238
+ })
239
+ } else {
240
+ const dynamicImport = ['redis', '@ikonintegration/ikapi']
241
+ await import(dynamicImport[0])
242
+ const IKAPI = (await import(dynamicImport[1])).default
243
+ this._remoteCache = new IKAPI.IKRedis({
244
+ host: config.endpoint,
245
+ user: config.username,
246
+ password: config.password,
247
+ enableTLS: config.enableTLS ?? true,
248
+ })
249
+ }
250
+ }
251
+ }
package/CacheTypes.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Minimal interface for Redis-like clients used by the cache layer.
3
+ * Supports both @ikonintegration/ikapi Redis and the legacy IKRedis.
4
+ */
5
+ export interface RedisLike {
6
+ get(key: string): Promise<string | null>
7
+ keys(pattern: string): Promise<string[]>
8
+ mGet(keys: string[]): Promise<string[]>
9
+ }
10
+
11
+ /**
12
+ * Configuration for the Redis cache connection.
13
+ */
14
+ export interface CacheConfig {
15
+ endpoint: string
16
+ username: string
17
+ password: string
18
+ enableTLS?: boolean
19
+ /** Set to 5 to use the new @ikonintegration/ikapi Redis client */
20
+ version?: number
21
+ }
22
+
23
+ /**
24
+ * Cache key prefixes and TTL constants (matching the legacy client).
25
+ */
26
+ export const CacheConstants = {
27
+ // Customer
28
+ CustomerPrefix: 'CO::',
29
+ AllCustomersTTL: 60 * 60, // 1 hour
30
+ CustomerTTL: 60, // 1 minute
31
+ // Tenant
32
+ TenantPrefix: 'TN::',
33
+ AllTenantsTTL: 60 * 60, // 1 hour
34
+ TenantTTL: 60, // 1 minute
35
+ } as const
package/MemoryCache.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Simple in-memory TTL cache used as a local layer before hitting Redis.
3
+ */
4
+ export class MemoryCache {
5
+ private store: Map<string, { value: any; expiresAt: number }> = new Map()
6
+
7
+ getValue<T = any>(key: string): T | null {
8
+ const entry = this.store.get(key)
9
+ if (!entry) return null
10
+ if (entry.expiresAt > 0 && Date.now() > entry.expiresAt) {
11
+ this.store.delete(key)
12
+ return null
13
+ }
14
+ return entry.value as T
15
+ }
16
+
17
+ setValue(key: string, value: any, ttlSeconds: number = 0): void {
18
+ const expiresAt = ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : 0
19
+ this.store.set(key, { value, expiresAt })
20
+ }
21
+
22
+ cleanKey(key: string): void {
23
+ this.store.delete(key)
24
+ }
25
+
26
+ clear(): void {
27
+ this.store.clear()
28
+ }
29
+ }