dataconv-client-sdk-ts 0.2.0 → 0.2.1

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.
@@ -1,608 +0,0 @@
1
- import axios from 'axios';
2
- import { DataConvClient } from '../DataConvClient';
3
- import type {
4
- ConvertedBundleResource,
5
- DataConvCrypto,
6
- DataConvDidCommResponse,
7
- TenantAdapterConfigResource
8
- } from '../types';
9
-
10
- jest.mock('axios');
11
-
12
- const mockedAxios = axios as jest.Mocked<typeof axios>;
13
- const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
14
- const originalFetch = global.fetch;
15
-
16
- function createMockResponse(status: number, headers: Headers = new Headers(), data: any = {}) {
17
- return {
18
- status,
19
- headers,
20
- json: jest.fn().mockResolvedValue(data),
21
- text: jest.fn().mockResolvedValue(JSON.stringify(data)),
22
- ok: status >= 200 && status < 300,
23
- statusText: '',
24
- type: 'basic',
25
- url: 'http://localhost:8080',
26
- redirected: false,
27
- clone: () => ({} as Response),
28
- body: null,
29
- bodyUsed: false,
30
- arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)),
31
- blob: jest.fn().mockResolvedValue(new Blob([])),
32
- formData: jest.fn().mockResolvedValue(new FormData())
33
- } as unknown as Response;
34
- }
35
-
36
- describe('DataConvClient', () => {
37
- let client: DataConvClient;
38
-
39
- beforeEach(() => {
40
- mockedAxios.create.mockReturnValue(mockedAxios);
41
- if (!mockedAxios.request) {
42
- mockedAxios.request = jest.fn();
43
- }
44
- mockedAxios.request.mockReset();
45
-
46
- client = new DataConvClient({
47
- issuerDid: 'did:web:clinic.example:employee:it:loader',
48
- alternateName: 'clinic-demo',
49
- tenantId: 'clinic-demo',
50
- jurisdiction: 'ES',
51
- baseUrl: 'http://localhost:8080',
52
- retryTimes: 3,
53
- retryDelayMs: 1
54
- });
55
- });
56
-
57
- afterEach(() => {
58
- global.fetch = originalFetch;
59
- });
60
-
61
- it('creates tenant config requests', async () => {
62
- mockedAxios.request.mockResolvedValueOnce({
63
- status: 202,
64
- headers: { location: '/host/cds-ES/v1/onehealth-research/clinic-demo/qvet-v1.0/config/_create-response?thid=cfg-1' }
65
- });
66
-
67
- const result = await client.createConfig({
68
- entries: [
69
- {
70
- softwareId: 'qvet-v1.0',
71
- config: {
72
- mappingConfig: {
73
- headerRowIndex: 1,
74
- fieldMap: { section: 'SECCION', concept: 'CONCEPTO' }
75
- }
76
- }
77
- }
78
- ]
79
- });
80
-
81
- expect(result.thid).toMatch(UUID_V4_REGEX);
82
- expect(result.location).toContain('_create-response');
83
- expect(mockedAxios.request).toHaveBeenCalledWith(expect.objectContaining({
84
- method: 'POST',
85
- url: '/host/cds-ES/v1/onehealth-research/clinic-demo/qvet-v1.0/config/_create',
86
- headers: { 'Content-Type': 'application/didcomm-plain+json' },
87
- data: expect.objectContaining({
88
- iss: 'did:web:clinic.example:employee:it:loader',
89
- thid: expect.any(String),
90
- jti: expect.any(String),
91
- data: [
92
- expect.objectContaining({ softwareId: 'qvet-v1.0' })
93
- ]
94
- })
95
- }));
96
- });
97
-
98
- it('prefers tenantId over alternateName for tenant config endpoints', async () => {
99
- const vatClient = new DataConvClient({
100
- issuerDid: 'did:web:clinic.example:employee:it:loader',
101
- alternateName: 'clinic-demo',
102
- tenantId: 'VATES-B00000000',
103
- jurisdiction: 'ES',
104
- baseUrl: 'http://localhost:8080',
105
- retryTimes: 3,
106
- retryDelayMs: 1
107
- });
108
-
109
- mockedAxios.request.mockResolvedValueOnce({
110
- status: 202,
111
- headers: { location: '/host/cds-ES/v1/onehealth-research/VATES-B00000000/qvet-v1.0/config/_create-response?thid=cfg-1' }
112
- });
113
-
114
- await vatClient.createConfig({
115
- entries: [{ softwareId: 'qvet-v1.0' }]
116
- });
117
-
118
- expect(mockedAxios.request).toHaveBeenCalledWith(expect.objectContaining({
119
- url: '/host/cds-ES/v1/onehealth-research/VATES-B00000000/qvet-v1.0/config/_create'
120
- }));
121
- });
122
-
123
- it('polls tenant config responses and stores successful resources', async () => {
124
- const payload: DataConvDidCommResponse<TenantAdapterConfigResource> = {
125
- thid: 'cfg-1',
126
- iss: 'did:web:globaldatacare.es:employee:preconversion',
127
- aud: 'did:web:clinic.example:employee:it:loader',
128
- type: 'https://didcomm.org/plaintext/2.0/message',
129
- iat: 1760000000,
130
- exp: 1760000300,
131
- body: {
132
- resourceType: 'Bundle',
133
- type: 'batch-response',
134
- total: 1,
135
- data: [
136
- {
137
- type: 'TenantAdapterConfig',
138
- response: { status: '200' },
139
- resource: {
140
- id: 'cfg-1',
141
- type: 'tenant-adapter-config',
142
- alternateName: 'clinic-demo',
143
- softwareId: 'qvet-v1.0',
144
- country: 'ES',
145
- facilityId: '',
146
- revision: '1',
147
- createdAt: '2026-03-12T06:55:50Z',
148
- updatedAt: '2026-03-12T07:11:23Z',
149
- audit: {},
150
- content: {}
151
- }
152
- }
153
- ]
154
- }
155
- };
156
-
157
- mockedAxios.request
158
- .mockResolvedValueOnce({ status: 202, headers: { 'retry-after': '0' } })
159
- .mockResolvedValueOnce({ status: 200, headers: {}, data: payload });
160
-
161
- const response = await client.pollConfig({ thid: 'cfg-1', softwareId: 'qvet-v1.0' });
162
-
163
- expect(response).toEqual(payload);
164
- expect(client.getLastTenantConfigResponse()).toEqual(payload);
165
- expect(client.getSuccessfulTenantConfigs()).toEqual([payload.body?.data?.[0]?.resource]);
166
- });
167
-
168
- it('uploads a spreadsheet by link as a DIDComm attachment', async () => {
169
- mockedAxios.request.mockResolvedValueOnce({
170
- status: 202,
171
- headers: { location: '/clinic-demo/cds-ES/v1/onehealth-research/digitaltwin/qvet-v1.0/Composition/_upload-response?thid=up-1' }
172
- });
173
-
174
- const result = await client.uploadWithLink('https://example.com/exampleQvetES.xlsx?dl=1', {
175
- softwareId: 'qvet-v1.0',
176
- fileName: 'exampleQvetES.xlsx'
177
- });
178
-
179
- expect(result.thid).toMatch(UUID_V4_REGEX);
180
- expect(mockedAxios.request).toHaveBeenCalledWith(expect.objectContaining({
181
- method: 'POST',
182
- url: '/clinic-demo/cds-ES/v1/onehealth-research/digitaltwin/qvet-v1.0/Composition/_upload',
183
- headers: { 'Content-Type': 'application/didcomm-plain+json' },
184
- data: expect.objectContaining({
185
- sourceFormat: 'excel',
186
- attachments: [
187
- expect.objectContaining({
188
- filename: 'exampleQvetES.xlsx',
189
- data: { links: ['https://example.com/exampleQvetES.xlsx?dl=1'] }
190
- })
191
- ]
192
- })
193
- }));
194
- });
195
-
196
- it('uploads a spreadsheet by base64 and includes tokens', async () => {
197
- client.setIdToken('id-token-1');
198
- client.setVpToken('vp-token-1');
199
- mockedAxios.request.mockResolvedValueOnce({
200
- status: 202,
201
- headers: { location: '/dummy' }
202
- });
203
-
204
- await client.uploadSpreadsheet(new Uint8Array([1, 2, 3]), {
205
- softwareId: 'qvet-v1.0',
206
- fileName: 'exampleQvetES.xlsx'
207
- });
208
-
209
- expect(mockedAxios.request).toHaveBeenCalledWith(expect.objectContaining({
210
- data: expect.objectContaining({
211
- id_token: 'id-token-1',
212
- vp_token: 'vp-token-1',
213
- sourceFormat: 'excel',
214
- attachments: [
215
- expect.objectContaining({
216
- data: { base64: Buffer.from([1, 2, 3]).toString('base64') }
217
- })
218
- ]
219
- })
220
- }));
221
- });
222
-
223
- it('builds multipart upload requests', async () => {
224
- mockedAxios.request.mockResolvedValueOnce({
225
- status: 202,
226
- headers: { location: '/dummy' }
227
- });
228
-
229
- await client.uploadWithFile({
230
- softwareId: 'qvet-v1.0',
231
- fileBytes: new Uint8Array([4, 5, 6]),
232
- fileName: 'exampleQvetES.xlsx'
233
- });
234
-
235
- const requestConfig = mockedAxios.request.mock.calls[0]?.[0];
236
- expect(requestConfig?.url).toBe('/clinic-demo/cds-ES/v1/onehealth-research/digitaltwin/qvet-v1.0/Composition/_upload');
237
- expect(requestConfig?.data).toBeInstanceOf(FormData);
238
- const payloadEntry = Array.from((requestConfig?.data as FormData).entries()).find(([key]) => key === 'payload');
239
- expect(payloadEntry?.[1]).toEqual(expect.any(String));
240
- const payloadJson = JSON.parse(String(payloadEntry?.[1]));
241
- expect(payloadJson.sourceFormat).toBe('excel');
242
- });
243
-
244
- it('polls conversion responses and extracts the converted bundle', async () => {
245
- const payload: DataConvDidCommResponse<ConvertedBundleResource> = {
246
- thid: 'up-1',
247
- iss: 'did:web:globaldatacare.es:employee:preconversion',
248
- aud: 'did:web:clinic.example:employee:it:loader',
249
- type: 'https://didcomm.org/plaintext/2.0/message',
250
- iat: 1760000000,
251
- exp: 1760000300,
252
- body: {
253
- resourceType: 'Bundle',
254
- type: 'batch-response',
255
- total: 1,
256
- data: [
257
- {
258
- type: 'ConversionResult',
259
- response: { status: '200' },
260
- resource: {
261
- resourceType: 'Bundle',
262
- type: 'batch',
263
- total: 1,
264
- data: [{ resource: { resourceType: 'Patient', id: 'patient-1' } }]
265
- }
266
- }
267
- ]
268
- }
269
- };
270
-
271
- mockedAxios.request
272
- .mockResolvedValueOnce({ status: 202, headers: { 'retry-after': '0' } })
273
- .mockResolvedValueOnce({ status: 200, headers: {}, data: payload });
274
-
275
- const response = await client.pollUploadResponse({
276
- thid: 'up-1',
277
- softwareId: 'qvet-v1.0'
278
- });
279
-
280
- expect(response).toEqual(payload);
281
- expect(client.getLastConversionResponse()).toEqual(payload);
282
- expect(client.getConversionEntry()?.response?.status).toBe('200');
283
- expect(client.getConvertedBundle()).toEqual(payload.body?.data?.[0]?.resource);
284
- });
285
-
286
- it('uses injected crypto for generated thread ids', async () => {
287
- const crypto: DataConvCrypto = {
288
- randomUUID: jest.fn(() => '11111111-2222-4333-8444-555555555555')
289
- };
290
- const injectedClient = new DataConvClient({
291
- issuerDid: 'did:web:clinic.example:employee:it:loader',
292
- alternateName: 'clinic-demo',
293
- tenantId: 'clinic-demo',
294
- jurisdiction: 'ES',
295
- crypto
296
- });
297
-
298
- mockedAxios.request.mockResolvedValueOnce({
299
- status: 202,
300
- headers: { location: '/dummy' }
301
- });
302
-
303
- const result = await injectedClient.createConfig({
304
- entries: [{ softwareId: 'qvet-v1.0' }]
305
- });
306
-
307
- expect(result.thid).toBe('11111111-2222-4333-8444-555555555555');
308
- expect(crypto.randomUUID).toHaveBeenCalledTimes(1);
309
- });
310
-
311
- it('uploads a spreadsheet using fetch when axios is not injected', async () => {
312
- mockedAxios.create.mockReset();
313
- mockedAxios.create.mockReturnValue(undefined as any);
314
-
315
- const mockFetch = jest.fn();
316
- global.fetch = mockFetch as typeof fetch;
317
-
318
- const fetchClient = new DataConvClient({
319
- issuerDid: 'did:web:clinic.example:employee:it:loader',
320
- alternateName: 'clinic-demo',
321
- tenantId: 'clinic-demo',
322
- jurisdiction: 'ES',
323
- baseUrl: 'http://localhost:8080',
324
- retryTimes: 2,
325
- retryDelayMs: 1,
326
- fetch: mockFetch as typeof fetch
327
- });
328
-
329
- mockFetch.mockResolvedValue(
330
- createMockResponse(202, new Headers({ location: '/clinic-demo/cds-ES/v1/onehealth-research/digitaltwin/qvet-v1.0/Composition/_upload-response?thid=up-1' }), {})
331
- );
332
-
333
- const result = await fetchClient.uploadWithLink('https://example.com/exampleQvetES.xlsx?dl=1', {
334
- softwareId: 'qvet-v1.0',
335
- fileName: 'exampleQvetES.xlsx'
336
- });
337
-
338
- expect(result.thid).toMatch(UUID_V4_REGEX);
339
- expect(mockFetch).toHaveBeenCalledWith(
340
- 'http://localhost:8080/clinic-demo/cds-ES/v1/onehealth-research/digitaltwin/qvet-v1.0/Composition/_upload',
341
- expect.objectContaining({
342
- method: 'POST',
343
- headers: { 'Content-Type': 'application/didcomm-plain+json' },
344
- body: expect.any(String)
345
- })
346
- );
347
-
348
- const rawBody = mockFetch.mock.calls[0]?.[1]?.body;
349
- const payload = typeof rawBody === 'string' ? JSON.parse(rawBody) : undefined;
350
- expect(payload?.iss).toBe('did:web:clinic.example:employee:it:loader');
351
- expect(payload?.thid).toEqual(expect.any(String));
352
- expect(payload?.sourceFormat).toBe('excel');
353
- expect(payload?.attachments?.[0]?.data?.links).toEqual(['https://example.com/exampleQvetES.xlsx?dl=1']);
354
- });
355
-
356
- it('polls conversion responses using fetch', async () => {
357
- mockedAxios.create.mockReset();
358
- mockedAxios.create.mockReturnValue(undefined as any);
359
-
360
- const mockFetch = jest.fn();
361
- global.fetch = mockFetch as typeof fetch;
362
-
363
- const fetchClient = new DataConvClient({
364
- issuerDid: 'did:web:clinic.example:employee:it:loader',
365
- alternateName: 'clinic-demo',
366
- tenantId: 'clinic-demo',
367
- jurisdiction: 'ES',
368
- baseUrl: 'http://localhost:8080',
369
- retryTimes: 2,
370
- retryDelayMs: 1,
371
- fetch: mockFetch as typeof fetch
372
- });
373
-
374
- mockFetch
375
- .mockResolvedValueOnce(createMockResponse(202, new Headers({ 'retry-after': '0' }), {}))
376
- .mockResolvedValueOnce(createMockResponse(200, new Headers({ 'content-type': 'application/json' }), {
377
- thid: 'up-1',
378
- body: { data: [{ type: 'ConversionResult', response: { status: '200' }, resource: { resourceType: 'Bundle' } }] }
379
- }));
380
-
381
- const response = await fetchClient.pollUploadResponse({
382
- thid: 'up-1',
383
- softwareId: 'qvet-v1.0'
384
- });
385
-
386
- expect(response.thid).toBe('up-1');
387
- expect(mockFetch).toHaveBeenCalledTimes(2);
388
- expect(fetchClient.getConversionEntry(response)?.type).toBe('ConversionResult');
389
- });
390
-
391
-
392
- it('patches promoted conversion resources through the canonical digital twin endpoint', async () => {
393
- client.setIdToken('session-id-1');
394
- client.setVpToken('vp-token-1');
395
- mockedAxios.request.mockResolvedValueOnce({
396
- status: 200,
397
- headers: {},
398
- data: {
399
- type: 'https://didcomm.org/plaintext/2.0/message',
400
- thid: 'up-1',
401
- body: { status: 'success', promotedCount: 2 }
402
- }
403
- });
404
-
405
- const response = await client.patchConversion({
406
- thid: 'up-1',
407
- softwareId: 'qvet-v1.0'
408
- });
409
-
410
- expect(response.body?.status).toBe('success');
411
- expect(response.body?.promotedCount).toBe(2);
412
- expect(mockedAxios.request).toHaveBeenCalledWith(expect.objectContaining({
413
- method: 'POST',
414
- url: '/clinic-demo/cds-ES/v1/onehealth-research/digitaltwin/qvet-v1.0/Composition/_patch?thid=up-1',
415
- headers: { 'Content-Type': 'application/didcomm-plain+json' },
416
- data: expect.objectContaining({
417
- id_token: 'session-id-1',
418
- vp_token: 'vp-token-1',
419
- thid: 'up-1'
420
- })
421
- }));
422
- });
423
-
424
- it('searches research resources with FHIR-like parameters', async () => {
425
- client.setIdToken('session-id-1');
426
- mockedAxios.request.mockResolvedValueOnce({
427
- status: 200,
428
- headers: {},
429
- data: {
430
- resourceType: 'Bundle',
431
- type: 'searchset',
432
- total: 1,
433
- entry: [
434
- {
435
- resource: {
436
- resourceType: 'DocumentReference',
437
- id: 'doc-1',
438
- meta: {
439
- claims: {
440
- 'DocumentReference.userSelected': 'false'
441
- }
442
- }
443
- }
444
- }
445
- ]
446
- }
447
- });
448
-
449
- const response = await client.searchResources({
450
- resourceType: 'DocumentReference',
451
- searchParams: {
452
- userselected: 'false',
453
- date: 'ge2026-01-01'
454
- }
455
- });
456
-
457
- expect(response.total).toBe(1);
458
- expect(mockedAxios.request).toHaveBeenCalledWith(expect.objectContaining({
459
- method: 'POST',
460
- url: '/host/cds-ES/v1/onehealth-research/clinic-demo/org.hl7.fhir.api/DocumentReference/_search',
461
- headers: {
462
- 'Content-Type': 'application/json',
463
- Authorization: 'Bearer session-id-1'
464
- },
465
- data: {
466
- userselected: 'false',
467
- date: 'ge2026-01-01'
468
- }
469
- }));
470
- });
471
-
472
- it('normalizes FHIR search parameter names to lowercase before sending the request', async () => {
473
- client.setIdToken('session-id-1');
474
- mockedAxios.request.mockResolvedValueOnce({
475
- status: 200,
476
- headers: {},
477
- data: {
478
- resourceType: 'Bundle',
479
- type: 'searchset',
480
- total: 0,
481
- entry: []
482
- }
483
- });
484
-
485
- await client.searchResources({
486
- resourceType: 'DocumentReference',
487
- searchParams: {
488
- userSelected: 'false',
489
- date: 'ge2026-01-01'
490
- }
491
- });
492
-
493
- expect(mockedAxios.request).toHaveBeenCalledWith(expect.objectContaining({
494
- method: 'POST',
495
- url: '/host/cds-ES/v1/onehealth-research/clinic-demo/org.hl7.fhir.api/DocumentReference/_search',
496
- headers: {
497
- 'Content-Type': 'application/json',
498
- Authorization: 'Bearer session-id-1'
499
- },
500
- data: {
501
- userselected: 'false',
502
- date: 'ge2026-01-01'
503
- }
504
- }));
505
- });
506
-
507
- it('supports a mocked config, upload, poll, batch and search workflow', async () => {
508
- mockedAxios.request
509
- .mockResolvedValueOnce({
510
- status: 202,
511
- headers: { location: '/host/cds-ES/v1/onehealth-research/clinic-demo/qvet-v1.0/config/_create-response?thid=cfg-1' }
512
- })
513
- .mockResolvedValueOnce({ status: 202, headers: { 'retry-after': '0' } })
514
- .mockResolvedValueOnce({
515
- status: 200,
516
- headers: {},
517
- data: {
518
- thid: 'cfg-1',
519
- body: {
520
- data: [
521
- {
522
- type: 'TenantAdapterConfig',
523
- response: { status: '200' },
524
- resource: { id: 'cfg-1', softwareId: 'qvet-v1.0' }
525
- }
526
- ]
527
- }
528
- }
529
- })
530
- .mockResolvedValueOnce({
531
- status: 202,
532
- headers: { location: '/clinic-demo/cds-ES/v1/onehealth-research/digitaltwin/qvet-v1.0/Composition/_upload-response?thid=up-1' }
533
- })
534
- .mockResolvedValueOnce({ status: 202, headers: { 'retry-after': '0' } })
535
- .mockResolvedValueOnce({
536
- status: 200,
537
- headers: {},
538
- data: {
539
- thid: 'up-1',
540
- body: {
541
- data: [
542
- {
543
- type: 'ConversionResult',
544
- response: { status: '200' },
545
- resource: { resourceType: 'Bundle', total: 1 }
546
- }
547
- ]
548
- }
549
- }
550
- })
551
- .mockResolvedValueOnce({
552
- status: 200,
553
- headers: {},
554
- data: { body: { status: 'success', promotedCount: 2 } }
555
- })
556
- .mockResolvedValueOnce({
557
- status: 200,
558
- headers: {},
559
- data: {
560
- resourceType: 'Bundle',
561
- type: 'searchset',
562
- total: 1,
563
- entry: [{ resource: { resourceType: 'Composition', id: 'comp-1' } }]
564
- }
565
- });
566
-
567
- const configResponse = await client.createTenantConfigAndWait({
568
- softwareId: 'qvet-v1.0',
569
- entries: [{ softwareId: 'qvet-v1.0' }]
570
- });
571
- const uploadResponse = await client.uploadSpreadsheetAndWait('https://example.com/exampleQvetES.xlsx?dl=1', {
572
- softwareId: 'qvet-v1.0',
573
- fileName: 'exampleQvetES.xlsx'
574
- });
575
- const patchResponse = await client.batchPromotion({
576
- thid: 'up-1',
577
- softwareId: 'qvet-v1.0'
578
- });
579
- const searchResponse = await client.searchResources({
580
- resourceType: 'Composition',
581
- searchParams: { 'relatesto-target': 'up-1' }
582
- });
583
-
584
- expect(configResponse.body?.data?.[0]?.resource?.softwareId).toBe('qvet-v1.0');
585
- expect(uploadResponse.thid).toBe('up-1');
586
- expect(patchResponse.body?.promotedCount).toBe(2);
587
- expect(searchResponse.total).toBe(1);
588
- });
589
-
590
- it('throws if neither axios nor fetch transport is available', async () => {
591
- mockedAxios.create.mockReset();
592
- mockedAxios.create.mockReturnValue(undefined as any);
593
- Reflect.set(globalThis as object, 'fetch', undefined);
594
-
595
- const clientNoTransport = new DataConvClient({
596
- issuerDid: 'did:web:clinic.example:employee:it:loader',
597
- alternateName: 'clinic-demo',
598
- tenantId: 'clinic-demo',
599
- jurisdiction: 'ES'
600
- });
601
-
602
- await expect(clientNoTransport.createConfig({
603
- entries: [{ softwareId: 'qvet-v1.0' }]
604
- })).rejects.toThrow(
605
- 'No HTTP transport available: provide axios httpClient or fetch implementation'
606
- );
607
- });
608
- });
@@ -1,8 +0,0 @@
1
- export const DEFAULT_DIDCOMM_TYPE = 'https://didcomm.org/plaintext/2.0/message';
2
- export const DEFAULT_SECTOR = 'onehealth-research';
3
- export const DEFAULT_UPLOAD_BODY = {
4
- resourceType: 'Bundle',
5
- type: 'batch',
6
- data: [],
7
- total: 0
8
- };