@openmrs/esm-implementer-tools-app 8.0.1-pre.3406 → 8.0.1-pre.3415

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
+ import { openmrsFetch, isVersionSatisfied } from '@openmrs/esm-framework';
2
+ import {
3
+ checkModules,
4
+ hasInvalidDependencies,
5
+ clearCache,
6
+ type ResolvedBackendModuleType,
7
+ } from './openmrs-backend-dependencies';
8
+
9
+ jest.mock('@openmrs/esm-framework', () => ({
10
+ openmrsFetch: jest.fn(),
11
+ isVersionSatisfied: jest.fn(),
12
+ restBaseUrl: '/ws/rest/v1',
13
+ }));
14
+
15
+ const mockOpenmrsFetch = jest.mocked(openmrsFetch);
16
+ const mockIsVersionSatisfied = jest.mocked(isVersionSatisfied);
17
+
18
+ describe('openmrs-backend-dependencies', () => {
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ clearCache();
22
+ window.installedModules = [];
23
+ });
24
+
25
+ describe('checkModules', () => {
26
+ it('should return empty array when no modules have backend dependencies', async () => {
27
+ mockOpenmrsFetch.mockResolvedValue({
28
+ data: {
29
+ results: [],
30
+ links: [],
31
+ },
32
+ } as any);
33
+
34
+ window.installedModules = [
35
+ ['@openmrs/esm-app-1', {}],
36
+ ['@openmrs/esm-app-2', {} as any],
37
+ ];
38
+
39
+ const result = await checkModules();
40
+
41
+ expect(result).toEqual([]);
42
+ });
43
+
44
+ it('should identify missing backend modules', async () => {
45
+ mockOpenmrsFetch.mockResolvedValue({
46
+ data: {
47
+ results: [{ uuid: 'webservices.rest', version: '2.24.0' }],
48
+ links: [],
49
+ },
50
+ } as any);
51
+
52
+ window.installedModules = [['@openmrs/esm-test-app', { backendDependencies: { 'missing-module': '1.0.0' } }]];
53
+
54
+ const result = await checkModules();
55
+
56
+ expect(result).toHaveLength(1);
57
+ expect(result[0].name).toBe('@openmrs/esm-test-app');
58
+ expect(result[0].dependencies).toHaveLength(1);
59
+ expect(result[0].dependencies[0]).toMatchObject({
60
+ name: 'missing-module',
61
+ requiredVersion: '1.0.0',
62
+ type: 'missing',
63
+ });
64
+ expect(result[0].dependencies[0].installedVersion).toBeUndefined();
65
+ });
66
+
67
+ it('should identify version mismatches', async () => {
68
+ mockOpenmrsFetch.mockResolvedValue({
69
+ data: {
70
+ results: [{ uuid: 'webservices.rest', version: '2.24.0' }],
71
+ links: [],
72
+ },
73
+ } as any);
74
+
75
+ mockIsVersionSatisfied.mockReturnValue(false);
76
+
77
+ window.installedModules = [['@openmrs/esm-test-app', { backendDependencies: { 'webservices.rest': '^3.0.0' } }]];
78
+
79
+ const result = await checkModules();
80
+
81
+ expect(result[0].dependencies[0]).toMatchObject({
82
+ name: 'webservices.rest',
83
+ requiredVersion: '^3.0.0',
84
+ installedVersion: '2.24.0',
85
+ type: 'version-mismatch',
86
+ });
87
+ });
88
+
89
+ it('should mark satisfied dependencies as okay', async () => {
90
+ mockOpenmrsFetch.mockResolvedValue({
91
+ data: {
92
+ results: [{ uuid: 'webservices.rest', version: '2.24.0' }],
93
+ links: [],
94
+ },
95
+ } as any);
96
+
97
+ mockIsVersionSatisfied.mockReturnValue(true);
98
+
99
+ window.installedModules = [['@openmrs/esm-test-app', { backendDependencies: { 'webservices.rest': '^2.0.0' } }]];
100
+
101
+ const result = await checkModules();
102
+
103
+ expect(result[0].dependencies[0]).toMatchObject({
104
+ name: 'webservices.rest',
105
+ requiredVersion: '^2.0.0',
106
+ installedVersion: '2.24.0',
107
+ type: 'okay',
108
+ });
109
+ });
110
+
111
+ it('should handle multiple modules with mixed dependency states', async () => {
112
+ mockOpenmrsFetch.mockResolvedValue({
113
+ data: {
114
+ results: [
115
+ { uuid: 'webservices.rest', version: '2.24.0' },
116
+ { uuid: 'fhir2', version: '1.5.0' },
117
+ ],
118
+ links: [],
119
+ },
120
+ } as any);
121
+
122
+ mockIsVersionSatisfied.mockImplementation((required, installed) => {
123
+ if (required === '^2.0.0' && installed === '2.24.0') return true;
124
+ if (required === '^1.0.0' && installed === '1.5.0') return true;
125
+ return false;
126
+ });
127
+
128
+ window.installedModules = [
129
+ [
130
+ '@openmrs/esm-app-1',
131
+ {
132
+ backendDependencies: {
133
+ 'webservices.rest': '^2.0.0',
134
+ 'missing-module': '1.0.0',
135
+ },
136
+ },
137
+ ],
138
+ ['@openmrs/esm-app-2', { backendDependencies: { fhir2: '^1.0.0' } }],
139
+ ];
140
+
141
+ const result = await checkModules();
142
+
143
+ expect(result).toHaveLength(2);
144
+ expect(result[0].dependencies).toHaveLength(2);
145
+ expect(result[0].dependencies.find((d) => d.type === 'okay')).toBeDefined();
146
+ expect(result[0].dependencies.find((d) => d.type === 'missing')).toBeDefined();
147
+ expect(result[1].dependencies).toHaveLength(1);
148
+ expect(result[1].dependencies[0].type).toBe('okay');
149
+ });
150
+
151
+ it('should include installed optional dependencies with required ones', async () => {
152
+ mockOpenmrsFetch.mockResolvedValue({
153
+ data: {
154
+ results: [
155
+ { uuid: 'webservices.rest', version: '2.24.0' },
156
+ { uuid: 'fhir2', version: '1.5.0' },
157
+ ],
158
+ links: [],
159
+ },
160
+ } as any);
161
+
162
+ mockIsVersionSatisfied.mockReturnValue(true);
163
+
164
+ window.installedModules = [
165
+ [
166
+ '@openmrs/esm-test-app',
167
+ {
168
+ backendDependencies: { 'webservices.rest': '^2.0.0' },
169
+ optionalBackendDependencies: {
170
+ fhir2: '^1.0.0',
171
+ 'optional-missing': '^1.0.0',
172
+ },
173
+ },
174
+ ],
175
+ ];
176
+
177
+ const result = await checkModules();
178
+
179
+ expect(result[0].dependencies).toHaveLength(2);
180
+ // Should include both required and installed optional
181
+ expect(result[0].dependencies.find((d) => d.name === 'webservices.rest')).toBeDefined();
182
+ expect(result[0].dependencies.find((d) => d.name === 'fhir2')).toBeDefined();
183
+ // Should not include missing optional dependencies
184
+ expect(result[0].dependencies.find((d) => d.name === 'optional-missing')).toBeUndefined();
185
+ });
186
+
187
+ it('should extract version from optional dependencies with feature flags', async () => {
188
+ mockOpenmrsFetch.mockResolvedValue({
189
+ data: {
190
+ results: [
191
+ { uuid: 'webservices.rest', version: '2.24.0' },
192
+ { uuid: 'fhir2', version: '1.5.0' },
193
+ ],
194
+ links: [],
195
+ },
196
+ } as any);
197
+
198
+ mockIsVersionSatisfied.mockReturnValue(true);
199
+
200
+ window.installedModules = [
201
+ [
202
+ '@openmrs/esm-test-app',
203
+ {
204
+ backendDependencies: { 'webservices.rest': '^2.0.0' },
205
+ optionalBackendDependencies: {
206
+ fhir2: {
207
+ version: '^1.0.0',
208
+ feature: 'fhir-support' as any,
209
+ },
210
+ },
211
+ },
212
+ ],
213
+ ];
214
+
215
+ const result = await checkModules();
216
+
217
+ const fhir2Dep = result[0].dependencies.find((d) => d.name === 'fhir2');
218
+ expect(fhir2Dep).toBeDefined();
219
+ expect(fhir2Dep?.requiredVersion).toBe('^1.0.0');
220
+ });
221
+
222
+ it('should cache results across multiple calls', async () => {
223
+ mockOpenmrsFetch.mockResolvedValue({
224
+ data: {
225
+ results: [{ uuid: 'webservices.rest', version: '2.24.0' }],
226
+ links: [],
227
+ },
228
+ } as any);
229
+
230
+ mockIsVersionSatisfied.mockReturnValue(true);
231
+
232
+ window.installedModules = [['@openmrs/esm-test-app', { backendDependencies: { 'webservices.rest': '^2.0.0' } }]];
233
+
234
+ const result1 = await checkModules();
235
+ const result2 = await checkModules();
236
+
237
+ // Should only fetch once due to caching
238
+ expect(mockOpenmrsFetch).toHaveBeenCalledTimes(1);
239
+ expect(result1).toBe(result2); // Same reference
240
+ });
241
+
242
+ it('should handle paginated backend module responses', async () => {
243
+ const page1Modules = Array.from({ length: 50 }, (_, i) => ({
244
+ uuid: `module-${i}`,
245
+ version: '1.0.0',
246
+ }));
247
+
248
+ const page2Modules = [{ uuid: 'module-50', version: '1.0.0' }];
249
+
250
+ mockOpenmrsFetch
251
+ .mockResolvedValueOnce({
252
+ data: {
253
+ results: page1Modules,
254
+ links: [{ rel: 'next', uri: '/ws/rest/v1/module?startIndex=50' }],
255
+ },
256
+ } as any)
257
+ .mockResolvedValueOnce({
258
+ data: {
259
+ results: page2Modules,
260
+ links: [],
261
+ },
262
+ } as any);
263
+
264
+ mockIsVersionSatisfied.mockReturnValue(true);
265
+
266
+ window.installedModules = [
267
+ ['@openmrs/esm-test-app', { backendDependencies: { 'module-0': '1.0.0', 'module-50': '1.0.0' } }],
268
+ ];
269
+
270
+ const result = await checkModules();
271
+
272
+ // Should find both modules across pages
273
+ expect(result[0].dependencies).toHaveLength(2);
274
+ expect(result[0].dependencies.every((d) => d.type === 'okay')).toBe(true);
275
+ });
276
+
277
+ it('should handle fetch errors gracefully by returning empty backend modules', async () => {
278
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
279
+
280
+ mockOpenmrsFetch.mockRejectedValue(new Error('Network error'));
281
+
282
+ window.installedModules = [['@openmrs/esm-test-app', { backendDependencies: { 'webservices.rest': '2.0.0' } }]];
283
+
284
+ const result = await checkModules();
285
+
286
+ // Error should be logged
287
+ expect(consoleErrorSpy).toHaveBeenCalled();
288
+
289
+ // Should treat all dependencies as missing when fetch fails
290
+ expect(result).toHaveLength(1);
291
+ expect(result[0].dependencies).toHaveLength(1);
292
+ expect(result[0].dependencies[0].type).toBe('missing');
293
+
294
+ consoleErrorSpy.mockRestore();
295
+ });
296
+
297
+ it('should warn when reaching pagination limit', async () => {
298
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
299
+
300
+ mockOpenmrsFetch.mockResolvedValue({
301
+ data: {
302
+ results: [{ uuid: 'test-module', version: '1.0.0' }],
303
+ links: [{ rel: 'next', uri: 'module?startIndex=50' }],
304
+ },
305
+ } as any);
306
+
307
+ window.installedModules = [['@openmrs/esm-test-app', { backendDependencies: { 'test-module': '1.0.0' } }]];
308
+
309
+ await checkModules();
310
+
311
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Reached maximum page limit'));
312
+
313
+ consoleWarnSpy.mockRestore();
314
+ });
315
+ });
316
+
317
+ describe('hasInvalidDependencies', () => {
318
+ it('should return false when all dependencies are okay', () => {
319
+ const modules = [
320
+ {
321
+ name: '@openmrs/esm-test-app',
322
+ dependencies: [
323
+ {
324
+ name: 'webservices.rest',
325
+ requiredVersion: '^2.0.0',
326
+ installedVersion: '2.24.0',
327
+ type: 'okay' as ResolvedBackendModuleType,
328
+ },
329
+ ],
330
+ },
331
+ ];
332
+
333
+ expect(hasInvalidDependencies(modules)).toBe(false);
334
+ });
335
+
336
+ it('should return false when there are no dependencies', () => {
337
+ const modules = [
338
+ {
339
+ name: '@openmrs/esm-test-app',
340
+ dependencies: [],
341
+ },
342
+ ];
343
+
344
+ expect(hasInvalidDependencies(modules)).toBe(false);
345
+ });
346
+
347
+ it('should return true when there are missing dependencies', () => {
348
+ const modules = [
349
+ {
350
+ name: '@openmrs/esm-test-app',
351
+ dependencies: [
352
+ {
353
+ name: 'missing-module',
354
+ requiredVersion: '1.0.0',
355
+ type: 'missing' as ResolvedBackendModuleType,
356
+ },
357
+ ],
358
+ },
359
+ ];
360
+
361
+ expect(hasInvalidDependencies(modules)).toBe(true);
362
+ });
363
+
364
+ it('should return true when there are version mismatches', () => {
365
+ const modules = [
366
+ {
367
+ name: '@openmrs/esm-test-app',
368
+ dependencies: [
369
+ {
370
+ name: 'webservices.rest',
371
+ requiredVersion: '^3.0.0',
372
+ installedVersion: '2.24.0',
373
+ type: 'version-mismatch' as ResolvedBackendModuleType,
374
+ },
375
+ ],
376
+ },
377
+ ];
378
+
379
+ expect(hasInvalidDependencies(modules)).toBe(true);
380
+ });
381
+
382
+ it('should return true if any module has invalid dependencies', () => {
383
+ const modules = [
384
+ {
385
+ name: '@openmrs/esm-app-1',
386
+ dependencies: [
387
+ {
388
+ name: 'module-1',
389
+ requiredVersion: '1.0.0',
390
+ installedVersion: '1.0.0',
391
+ type: 'okay' as ResolvedBackendModuleType,
392
+ },
393
+ ],
394
+ },
395
+ {
396
+ name: '@openmrs/esm-app-2',
397
+ dependencies: [
398
+ {
399
+ name: 'module-2',
400
+ requiredVersion: '2.0.0',
401
+ type: 'missing' as ResolvedBackendModuleType,
402
+ },
403
+ ],
404
+ },
405
+ ];
406
+
407
+ expect(hasInvalidDependencies(modules)).toBe(true);
408
+ });
409
+
410
+ it('should return true if any dependency in any module is invalid', () => {
411
+ const modules = [
412
+ {
413
+ name: '@openmrs/esm-app',
414
+ dependencies: [
415
+ {
416
+ name: 'module-1',
417
+ requiredVersion: '1.0.0',
418
+ installedVersion: '1.0.0',
419
+ type: 'okay' as ResolvedBackendModuleType,
420
+ },
421
+ {
422
+ name: 'module-2',
423
+ requiredVersion: '2.0.0',
424
+ installedVersion: '1.0.0',
425
+ type: 'version-mismatch' as ResolvedBackendModuleType,
426
+ },
427
+ ],
428
+ },
429
+ ];
430
+
431
+ expect(hasInvalidDependencies(modules)).toBe(true);
432
+ });
433
+ });
434
+ });
@@ -39,10 +39,12 @@ interface BackendModule {
39
39
 
40
40
  let cachedFrontendModules: Array<ResolvedDependenciesModule>;
41
41
 
42
+ const MAX_PAGES = 50;
43
+
42
44
  async function initInstalledBackendModules(): Promise<Array<BackendModule>> {
43
45
  try {
44
- const response = await fetchInstalledBackendModules();
45
- return response.data.results;
46
+ const modules = await fetchInstalledBackendModules();
47
+ return modules;
46
48
  } catch (err) {
47
49
  console.error(err);
48
50
  }
@@ -88,10 +90,49 @@ function checkIfModulesAreInstalled(
88
90
  };
89
91
  }
90
92
 
91
- function fetchInstalledBackendModules() {
92
- return openmrsFetch(`${restBaseUrl}/module?v=custom:(uuid,version)`, {
93
- method: 'GET',
94
- });
93
+ async function fetchInstalledBackendModules(): Promise<Array<BackendModule>> {
94
+ const collected: Array<BackendModule> = [];
95
+ let nextUrl: string | null = `${restBaseUrl}/module?v=custom:(uuid,version)`;
96
+ let safetyCounter = 0;
97
+
98
+ const resolveNext = (url?: string | null) => {
99
+ if (!url) {
100
+ return null;
101
+ }
102
+ if (/^https?:\/\//i.test(url)) {
103
+ return url;
104
+ }
105
+ if (url.startsWith('/')) {
106
+ return url;
107
+ }
108
+ return `${restBaseUrl}/${url.replace(/^\/?/, '')}`;
109
+ };
110
+
111
+ while (nextUrl && safetyCounter < MAX_PAGES) {
112
+ try {
113
+ const { data } = await openmrsFetch(nextUrl, { method: 'GET' });
114
+ const pageResults: Array<BackendModule> = Array.isArray(data?.results) ? data.results : [];
115
+
116
+ collected.push(...pageResults);
117
+
118
+ const links: Array<{ rel?: string; uri?: string }> = Array.isArray(data?.links) ? data.links : [];
119
+ const nextLink = links.find((l) => (l.rel || '').toLowerCase() === 'next');
120
+
121
+ nextUrl = resolveNext(nextLink?.uri ?? null);
122
+ safetyCounter += 1;
123
+ } catch (e) {
124
+ console.error(`Failed to fetch backend modules on request ${safetyCounter + 1} (URL: ${nextUrl})`, e);
125
+ throw new Error(`Failed to fetch backend modules: ${e instanceof Error ? e.message : 'Unknown error'}`);
126
+ }
127
+ }
128
+
129
+ if (nextUrl && safetyCounter >= MAX_PAGES) {
130
+ console.warn(
131
+ `Reached maximum page limit (${MAX_PAGES}) while fetching backend modules. There may be more data available at: ${nextUrl}`,
132
+ );
133
+ }
134
+
135
+ return collected;
95
136
  }
96
137
 
97
138
  function getMissingBackendModules(
@@ -129,11 +170,8 @@ function getInstalledAndRequiredBackendModules(
129
170
  version: declaredBackendModules[key],
130
171
  }));
131
172
 
132
- return requiredModules.filter((requiredModule) => {
133
- return installedBackendModules.find((installedModule) => {
134
- return requiredModule.uuid === installedModule.uuid;
135
- });
136
- });
173
+ const installedUuids = new Set(installedBackendModules.map((module) => module.uuid));
174
+ return requiredModules.filter((requiredModule) => installedUuids.has(requiredModule.uuid));
137
175
  }
138
176
 
139
177
  function getInstalledVersion(
@@ -141,7 +179,7 @@ function getInstalledVersion(
141
179
  installedBackendModules: Array<BackendModule>,
142
180
  ) {
143
181
  const moduleName = installedAndRequiredBackendModule.uuid;
144
- return installedBackendModules.find((mod) => mod.uuid == moduleName)?.version ?? '';
182
+ return installedBackendModules.find((mod) => mod.uuid === moduleName)?.version ?? '';
145
183
  }
146
184
 
147
185
  function getResolvedModuleType(requiredVersion: string, installedVersion: string): ResolvedBackendModuleType {
@@ -176,3 +214,8 @@ export async function checkModules(): Promise<Array<ResolvedDependenciesModule>>
176
214
  export function hasInvalidDependencies(frontendModules: Array<ResolvedDependenciesModule>) {
177
215
  return frontendModules.some((m) => m.dependencies.some((n) => n.type !== 'okay'));
178
216
  }
217
+
218
+ // For use in tests
219
+ export function clearCache() {
220
+ cachedFrontendModules = undefined as any;
221
+ }