@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/Api.ts +363 -239
- package/CacheClient.ts +251 -0
- package/CacheTypes.ts +35 -0
- package/MemoryCache.ts +29 -0
- package/dist/Api.d.ts +361 -239
- package/dist/Api.js +1 -1
- package/dist/Api.js.map +1 -1
- package/dist/CacheClient.d.ts +56 -0
- package/dist/CacheClient.js +260 -0
- package/dist/CacheClient.js.map +1 -0
- package/dist/CacheTypes.d.ts +31 -0
- package/dist/CacheTypes.js +17 -0
- package/dist/CacheTypes.js.map +1 -0
- package/dist/MemoryCache.d.ts +10 -0
- package/dist/MemoryCache.js +33 -0
- package/dist/MemoryCache.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/index.ts +15 -0
- package/package.json +4 -3
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
|
+
}
|