From 03e08989ceba1b550ee2c4afadc3acf6f59abbeb Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Thu, 28 May 2026 10:58:23 +0300 Subject: [PATCH 01/14] feat: allow async value change Signed-off-by: Hunter Achieng --- packages/dhis2/src/tracker.js | 8 ++-- packages/dhis2/test/tracker.test.js | 58 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/packages/dhis2/src/tracker.js b/packages/dhis2/src/tracker.js index c4346145fd..49566e765a 100644 --- a/packages/dhis2/src/tracker.js +++ b/packages/dhis2/src/tracker.js @@ -47,6 +47,7 @@ import * as util from './util.js'; * @param {string} strategy - The effect the import should have. Can either be CREATE, UPDATE, CREATE_AND_UPDATE and DELETE. * @param {object} payload - The data to be imported. * @param {TrackerOptions} [options] - An optional object containing parseAs, and apiVersion, and queries for the request + * @param {boolean} [options.async=false] - Whether to perform the import asynchronously. Defaults to false. * @state {DHIS2State} * @returns {Operation} */ @@ -57,7 +58,7 @@ function _import(strategy, payload, options = {}) { const [resolvedStrategy, resolvedPayload, resolvedOptions] = expandReferences(state, strategy, payload, options); - const { apiVersion, parseAs, ...query } = resolvedOptions; + const { apiVersion, parseAs, async: asyncOption, ...query } = resolvedOptions; const response = await util.request(state.configuration, { method: 'POST', @@ -74,7 +75,7 @@ function _import(strategy, payload, options = {}) { parseAs, query: { ...query, - async: false, + async: asyncOption || false, }, }, data: resolvedPayload, @@ -99,6 +100,7 @@ export { _import as import }; * @function * @param {string} path - Path to the resource, relative to the /tracker endpoint * @param {object} query - An object of query parameters to be encoded into the URL + * @param {boolean} [query.async=false] - Whether to perform the export asynchronously. Defaults to false. * @param {TrackerOptions} [options] - An optional object containing parseAs, and apiVersion for the request * @state {DHIS2State} * @returns {Operation} @@ -125,7 +127,7 @@ function _export(path, query, options = {}) { ...resolvedOptions, query: { ...resolvedQuery, - async: false, + async: resolvedQuery.async || false, }, }, }); diff --git a/packages/dhis2/test/tracker.test.js b/packages/dhis2/test/tracker.test.js index e9ada1a1a5..019f81d80b 100644 --- a/packages/dhis2/test/tracker.test.js +++ b/packages/dhis2/test/tracker.test.js @@ -19,7 +19,7 @@ const getPath = path => { return `/stable-2-40-7/api/42/${path}`; }; -describe('tracker', () => { +describe.only('tracker', () => { const state = { configuration, data: { @@ -95,4 +95,60 @@ describe('tracker', () => { message: 'the response', }); }); + + it('should export events asynchronously', async () => { + const query = { + orgUnit: 'TSyzvBiovKh', + async: true, + }; + testServer + .intercept({ + path: getPath('tracker/enrollments'), + method: 'GET', + query, + }) + .reply(200, { + httpStatus: 'OK', + message: 'the response', + }); + + const finalState = await execute( + tracker.export('enrollments', { + orgUnit: 'TSyzvBiovKh', + async: true + }) + )(state); + expect(finalState.response.query.async).to.eql(true); + }); + it('should default to async false when not specified', async () => { + testServer + .intercept({ + path: getPath('tracker'), + method: 'POST', + query: { async: false }, + }) + .reply(200, { + httpStatus: 'OK', + message: 'the response', + }); + + const finalState = await execute( + tracker.import('CREATE', { + trackedEntities: [ + { + orgUnit: 'TSyzvBiovKh', + trackedEntityType: 'nEenWmSyUEp', + attributes: [ + { + attribute: 'w75KJ2mc4zz', + value: 'Gigiwe', + }, + ], + }, + ], + }) + )(state); + + expect(finalState.response.query.async).to.eql(false); + }) }); From 4cb86493b24277d742ab8451569018540e93d168 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Thu, 28 May 2026 13:59:51 +0300 Subject: [PATCH 02/14] feat: update tests Signed-off-by: Hunter Achieng --- packages/dhis2/src/tracker.js | 6 ++- packages/dhis2/test/tracker.test.js | 57 +++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/dhis2/src/tracker.js b/packages/dhis2/src/tracker.js index 49566e765a..286af5c289 100644 --- a/packages/dhis2/src/tracker.js +++ b/packages/dhis2/src/tracker.js @@ -97,15 +97,17 @@ export { _import as import }; * tracker.export('enrollments', {orgUnit: 'TSyzvBiovKh'}); * @example Export all events * tracker.export('events') + * @example Export all events with pagination + * tracker.export('events', { page: 1, pageSize: 100 }); * @function * @param {string} path - Path to the resource, relative to the /tracker endpoint - * @param {object} query - An object of query parameters to be encoded into the URL + * @param {object} query - An object of query parameters to be encoded into the URL. Can include pagination parameters, filters, etc. * @param {boolean} [query.async=false] - Whether to perform the export asynchronously. Defaults to false. * @param {TrackerOptions} [options] - An optional object containing parseAs, and apiVersion for the request * @state {DHIS2State} * @returns {Operation} */ -function _export(path, query, options = {}) { +function _export(path, query = {}, options = {}) { return async state => { console.log('Preparing tracker export operation...'); diff --git a/packages/dhis2/test/tracker.test.js b/packages/dhis2/test/tracker.test.js index 019f81d80b..11ae050d88 100644 --- a/packages/dhis2/test/tracker.test.js +++ b/packages/dhis2/test/tracker.test.js @@ -19,7 +19,7 @@ const getPath = path => { return `/stable-2-40-7/api/42/${path}`; }; -describe.only('tracker', () => { +describe('tracker', () => { const state = { configuration, data: { @@ -57,7 +57,7 @@ describe.only('tracker', () => { ], }, ], - }) + }), )(state); expect(finalState.data).to.eql({ @@ -87,7 +87,7 @@ describe.only('tracker', () => { const finalState = await execute( tracker.export('enrollments', { orgUnit: 'TSyzvBiovKh', - }) + }), )(state); expect(finalState.data).to.eql({ @@ -97,9 +97,9 @@ describe.only('tracker', () => { }); it('should export events asynchronously', async () => { - const query = { + const query = { orgUnit: 'TSyzvBiovKh', - async: true, + async: true, }; testServer .intercept({ @@ -115,13 +115,13 @@ describe.only('tracker', () => { const finalState = await execute( tracker.export('enrollments', { orgUnit: 'TSyzvBiovKh', - async: true - }) - )(state); + async: true, + }), + )(state); expect(finalState.response.query.async).to.eql(true); }); it('should default to async false when not specified', async () => { - testServer + testServer .intercept({ path: getPath('tracker'), method: 'POST', @@ -146,9 +146,44 @@ describe.only('tracker', () => { ], }, ], - }) + }), )(state); expect(finalState.response.query.async).to.eql(false); - }) + }); + + it('should export all events when paging is true', async () => { + const events = Array.from({ length: 300 }, () => ({ event: 1 })); + + testServer + .intercept({ + path: getPath('tracker/events'), + method: 'GET', + query: { async: false, paging: true }, + }) + .reply(200, { events }); + + const finalState = await execute( + tracker.export('events', { paging: true }), + )(state); + + expect(finalState.data.events).to.have.lengthOf(300); + expect(finalState.data.events[0]).to.eql({ event: 1 }); + }); + it('should export only 50 events when paging is not sent', async () => { + const events = Array.from({ length: 300 }, () => ({ event: 1 })); + + testServer + .intercept({ + path: getPath('tracker/events'), + method: 'GET', + query: { async: false }, + }) + .reply(200, { events: events.slice(0, 50) }); + + const finalState = await execute(tracker.export('events'))(state); + + expect(finalState.data.events).to.have.lengthOf(50); + expect(finalState.data.events[0]).to.eql({ event: 1 }); + }); }); From 58dd4e1c787bd5da4b086caa570673cad854480a Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Thu, 28 May 2026 14:02:09 +0300 Subject: [PATCH 03/14] feat: add changeset Signed-off-by: Hunter Achieng --- .changeset/hip-planes-see.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hip-planes-see.md diff --git a/.changeset/hip-planes-see.md b/.changeset/hip-planes-see.md new file mode 100644 index 0000000000..be48a842e4 --- /dev/null +++ b/.changeset/hip-planes-see.md @@ -0,0 +1,5 @@ +--- +'@openfn/language-dhis2': patch +--- + +Fix tracker exports and imports when no query object is provided by defaulting `async` to `false` From 291ed1d00cc84d7334d31c541e9d8d5f80570bb9 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Tue, 2 Jun 2026 12:02:46 +0300 Subject: [PATCH 04/14] add integration tests for tracker.export --- packages/dhis2/test/integration.js | 79 +++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/dhis2/test/integration.js b/packages/dhis2/test/integration.js index a250682fbb..abe483ce2d 100644 --- a/packages/dhis2/test/integration.js +++ b/packages/dhis2/test/integration.js @@ -1,6 +1,16 @@ import { expect } from 'chai'; import crypto from 'node:crypto'; -import { execute, create, update, upsert, get } from '../dist/index.js'; +import { + execute, + tracker, + combine, + create, + update, + upsert, + each, + get, + fn, +} from '../src/index.js'; const getRandomProgramPayload = () => { const name = crypto.randomBytes(16).toString('hex'); @@ -471,4 +481,71 @@ describe('Integration tests', () => { ); }); }); + describe.skip('tracker', () => { + it('should export 50 events by default', async () => { + // v2.41+ for older version `skipPaging: true` + const state = { + configuration, + }; + const finalState = await execute(tracker.export('events'))(state); + + expect(finalState.data.instances.length).to.eql(50); + }).timeout(2e4); + + it('should export 1000 events with pageSize 1000', async () => { + const state = { + configuration, + }; + const { data } = await execute( + tracker.export('events', { totalPages: true, pageSize: 1e3 }), + )(state); + + expect(Object.keys(data).sort()).to.eql([ + 'instances', + 'page', + 'pageCount', + 'pageSize', + 'total', + ]); + expect(data.instances.length).to.eql(1000); + }).timeout(2e4); + + it('should export all events with pagination', async () => { + const state = { + configuration, + }; + const { data, results } = await execute( + tracker.export('events', { totalPages: true, pageSize: 1e4 }), + fn(state => { + console.log(Object.keys(state.data)); + state.results = state.data.instances; + const { page, pageSize, pageCount, total } = state.data; + const remainingPages = pageCount - page; + + state.pages = Array.from( + { length: remainingPages }, + (_, i) => page + i + 1, + ); + state.pageSize = pageSize; + return state; + }), + + each( + state => state.pages, + combine( + tracker.export('events', state => ({ + pageSize: state.pageSize, + page: state.data, + })), + fn(state => { + state.results = state.results.concat(state.data.instances); + return state; + }), + ), + ), + )(state); + + expect(results).to.be.greaterThan(3e4); + }).timeout(3e4); + }); }); From bd90f85211793832c711337fd5f401498afcf193 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Tue, 2 Jun 2026 12:49:08 +0300 Subject: [PATCH 05/14] add pagination example --- packages/dhis2/src/tracker.js | 36 +++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/dhis2/src/tracker.js b/packages/dhis2/src/tracker.js index 286af5c289..6c79b85e46 100644 --- a/packages/dhis2/src/tracker.js +++ b/packages/dhis2/src/tracker.js @@ -58,7 +58,12 @@ function _import(strategy, payload, options = {}) { const [resolvedStrategy, resolvedPayload, resolvedOptions] = expandReferences(state, strategy, payload, options); - const { apiVersion, parseAs, async: asyncOption, ...query } = resolvedOptions; + const { + apiVersion, + parseAs, + async: asyncOption, + ...query + } = resolvedOptions; const response = await util.request(state.configuration, { method: 'POST', @@ -68,7 +73,7 @@ function _import(strategy, payload, options = {}) { ...resolvedOptions, resolvedStrategy, }, - 'tracker' + 'tracker', ), options: { apiVersion, @@ -96,9 +101,28 @@ export { _import as import }; * @example Export all enrollment resources * tracker.export('enrollments', {orgUnit: 'TSyzvBiovKh'}); * @example Export all events - * tracker.export('events') + * tracker.export('events', { paging: false}) * @example Export all events with pagination - * tracker.export('events', { page: 1, pageSize: 100 }); + * tracker.export('events', { totalPages: true, pageSize: 1e4 }); + * fn(state => { + * state.results = state.data.events; + * const { page, pageSize, pageCount, total } = state.data.pager; + * const remainingPages = pageCount - page; + * + * state.pages = Array.from({ length: remainingPages }, (_, i) => page + i + 1); + * state.pageSize = pageSize; + * return state; + * }); + * + * each( + * $.pages, + * tracker + * .export('events', { pageSize: $.pageSize, page: $.data }) + * .then(state => { + * state.results = state.results.concat(state.data.events); + * return state; + * }), + * ); * @function * @param {string} path - Path to the resource, relative to the /tracker endpoint * @param {object} query - An object of query parameters to be encoded into the URL. Can include pagination parameters, filters, etc. @@ -115,7 +139,7 @@ function _export(path, query = {}, options = {}) { state, path, query, - options + options, ); const response = await util.request(state.configuration, { @@ -123,7 +147,7 @@ function _export(path, query = {}, options = {}) { path: util.prefixVersionToPath( state.configuration, resolvedOptions, - `tracker/${resolvedPath}` + `tracker/${resolvedPath}`, ), options: { ...resolvedOptions, From f81d8ad70feab623763ee901ebe4444f823f0454 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Wed, 3 Jun 2026 11:12:24 +0300 Subject: [PATCH 06/14] update example --- packages/dhis2/src/tracker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dhis2/src/tracker.js b/packages/dhis2/src/tracker.js index 6c79b85e46..65b6063a49 100644 --- a/packages/dhis2/src/tracker.js +++ b/packages/dhis2/src/tracker.js @@ -105,7 +105,7 @@ export { _import as import }; * @example Export all events with pagination * tracker.export('events', { totalPages: true, pageSize: 1e4 }); * fn(state => { - * state.results = state.data.events; + * state.results = state.data.instances; * const { page, pageSize, pageCount, total } = state.data.pager; * const remainingPages = pageCount - page; * @@ -119,7 +119,7 @@ export { _import as import }; * tracker * .export('events', { pageSize: $.pageSize, page: $.data }) * .then(state => { - * state.results = state.results.concat(state.data.events); + * state.results = state.results.concat(state.data.instances); * return state; * }), * ); From f9a873360c8fdebc755052db703e75fd744c95b5 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Wed, 3 Jun 2026 11:13:00 +0300 Subject: [PATCH 07/14] update integration test --- packages/dhis2/test/integration.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dhis2/test/integration.js b/packages/dhis2/test/integration.js index abe483ce2d..c249a1d2b7 100644 --- a/packages/dhis2/test/integration.js +++ b/packages/dhis2/test/integration.js @@ -481,7 +481,7 @@ describe('Integration tests', () => { ); }); }); - describe.skip('tracker', () => { + describe('tracker', () => { it('should export 50 events by default', async () => { // v2.41+ for older version `skipPaging: true` const state = { @@ -546,6 +546,6 @@ describe('Integration tests', () => { )(state); expect(results).to.be.greaterThan(3e4); - }).timeout(3e4); + }).timeout(5e4); }); }); From bc358ef59169478fced897f1592a1e4319efd8ed Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Wed, 3 Jun 2026 11:19:55 +0300 Subject: [PATCH 08/14] improve format --- packages/dhis2/src/tracker.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/dhis2/src/tracker.js b/packages/dhis2/src/tracker.js index 65b6063a49..423d05ca60 100644 --- a/packages/dhis2/src/tracker.js +++ b/packages/dhis2/src/tracker.js @@ -58,12 +58,7 @@ function _import(strategy, payload, options = {}) { const [resolvedStrategy, resolvedPayload, resolvedOptions] = expandReferences(state, strategy, payload, options); - const { - apiVersion, - parseAs, - async: asyncOption, - ...query - } = resolvedOptions; + const { apiVersion, parseAs, async = false, ...query } = resolvedOptions; const response = await util.request(state.configuration, { method: 'POST', @@ -80,7 +75,7 @@ function _import(strategy, payload, options = {}) { parseAs, query: { ...query, - async: asyncOption || false, + async, }, }, data: resolvedPayload, From 85445c9e56747d926e5dc96ec620bb1944574640 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Wed, 3 Jun 2026 12:30:33 +0300 Subject: [PATCH 09/14] remove async and add pagination docs --- packages/dhis2/src/tracker.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/dhis2/src/tracker.js b/packages/dhis2/src/tracker.js index 423d05ca60..8a7c501ac9 100644 --- a/packages/dhis2/src/tracker.js +++ b/packages/dhis2/src/tracker.js @@ -121,7 +121,11 @@ export { _import as import }; * @function * @param {string} path - Path to the resource, relative to the /tracker endpoint * @param {object} query - An object of query parameters to be encoded into the URL. Can include pagination parameters, filters, etc. - * @param {boolean} [query.async=false] - Whether to perform the export asynchronously. Defaults to false. + * @param {number} [query.page=1] - Page number to return + * @param {number} [query.pageSize=50] - Number of results per page + * @param {boolean} [query.totalPages=false] - Whether to return total number of elements and pages + * @param {boolean} [query.paging=true] - Set to false to return all rows without paging + * @param {string} [query.order] - Comma-separated field:sortDirection pairs, e.g. `createdAt:desc` * @param {TrackerOptions} [options] - An optional object containing parseAs, and apiVersion for the request * @state {DHIS2State} * @returns {Operation} @@ -148,7 +152,6 @@ function _export(path, query = {}, options = {}) { ...resolvedOptions, query: { ...resolvedQuery, - async: resolvedQuery.async || false, }, }, }); From bf0f34c16f5679c55dcb22b343dd36f2bdc9d2de Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Wed, 3 Jun 2026 12:52:14 +0300 Subject: [PATCH 10/14] update tracker unit tests --- packages/dhis2/test/tracker.test.js | 317 ++++++++++++++-------------- 1 file changed, 160 insertions(+), 157 deletions(-) diff --git a/packages/dhis2/test/tracker.test.js b/packages/dhis2/test/tracker.test.js index 11ae050d88..328193e480 100644 --- a/packages/dhis2/test/tracker.test.js +++ b/packages/dhis2/test/tracker.test.js @@ -15,175 +15,178 @@ const configuration = { apiVersion: '42', }; -const getPath = path => { - return `/stable-2-40-7/api/42/${path}`; -}; +const getPath = path => `/stable-2-40-7/api/42/${path}`; -describe('tracker', () => { - const state = { - configuration, - data: { - program: 'program1', - orgUnit: 'org50', +const trackedEntityPayload = { + trackedEntities: [ + { + orgUnit: 'TSyzvBiovKh', trackedEntityType: 'nEenWmSyUEp', - status: 'COMPLETED', - date: '02-02-20', + attributes: [{ attribute: 'w75KJ2mc4zz', value: 'Gigiwe' }], }, - }; - - it('should import a trackedEntity', async () => { - testServer - .intercept({ - path: getPath('tracker'), - method: 'POST', - query: { async: false }, - }) - .reply(200, { - httpStatus: 'OK', - message: 'the response', - }); - - const finalState = await execute( - tracker.import('CREATE', { - trackedEntities: [ - { - orgUnit: 'TSyzvBiovKh', - trackedEntityType: 'nEenWmSyUEp', - attributes: [ - { - attribute: 'w75KJ2mc4zz', - value: 'Gigiwe', - }, - ], - }, - ], - }), - )(state); - - expect(finalState.data).to.eql({ - httpStatus: 'OK', - message: 'the response', + ], +}; + +describe('tracker', () => { + const state = { configuration, data: {} }; + + describe('import', () => { + it('should default to async: false', async () => { + testServer + .intercept({ + path: getPath('tracker'), + method: 'POST', + query: { async: false }, + }) + .reply(200, { httpStatus: 'OK', status: 'OK' }); + + const finalState = await execute( + tracker.import('CREATE', trackedEntityPayload), + )(state); + + expect(finalState.response.query.async).to.eql(false); + expect(finalState.data.httpStatus).to.eql('OK'); }); - }); - it('should export all enrollements', async () => { - const query = { - orgUnit: 'TSyzvBiovKh', - }; - testServer - .intercept({ - path: getPath('tracker/enrollments'), - method: 'GET', - query: { - ...query, - async: false, - }, - }) - .reply(200, { - httpStatus: 'OK', - message: 'the response', - }); - - const finalState = await execute( - tracker.export('enrollments', { - orgUnit: 'TSyzvBiovKh', - }), - )(state); - - expect(finalState.data).to.eql({ - httpStatus: 'OK', - message: 'the response', + it('should send async: true when specified in options', async () => { + testServer + .intercept({ + path: getPath('tracker'), + method: 'POST', + query: { async: true }, + }) + .reply(200, { + httpStatus: 'OK', + response: { id: 'abc123', jobType: 'TRACKER_IMPORT_JOB' }, + }); + + const finalState = await execute( + tracker.import('CREATE', trackedEntityPayload, { async: true }), + )(state); + + expect(finalState.response.query.async).to.eql(true); + expect(finalState.data.response.id).to.eql('abc123'); }); - }); - it('should export events asynchronously', async () => { - const query = { - orgUnit: 'TSyzvBiovKh', - async: true, - }; - testServer - .intercept({ - path: getPath('tracker/enrollments'), - method: 'GET', - query, - }) - .reply(200, { - httpStatus: 'OK', - message: 'the response', - }); - - const finalState = await execute( - tracker.export('enrollments', { - orgUnit: 'TSyzvBiovKh', - async: true, - }), - )(state); - expect(finalState.response.query.async).to.eql(true); - }); - it('should default to async false when not specified', async () => { - testServer - .intercept({ - path: getPath('tracker'), - method: 'POST', - query: { async: false }, - }) - .reply(200, { - httpStatus: 'OK', - message: 'the response', - }); - - const finalState = await execute( - tracker.import('CREATE', { - trackedEntities: [ - { - orgUnit: 'TSyzvBiovKh', - trackedEntityType: 'nEenWmSyUEp', - attributes: [ - { - attribute: 'w75KJ2mc4zz', - value: 'Gigiwe', - }, - ], - }, - ], - }), - )(state); - - expect(finalState.response.query.async).to.eql(false); + it('should forward extra options as query params alongside async', async () => { + testServer + .intercept({ + path: getPath('tracker'), + method: 'POST', + query: { async: false, atomicMode: 'ALL' }, + }) + .reply(200, { httpStatus: 'OK' }); + + const finalState = await execute( + tracker.import('CREATE', trackedEntityPayload, { atomicMode: 'ALL' }), + )(state); + + expect(finalState.response.query.atomicMode).to.eql('ALL'); + expect(finalState.response.query.async).to.eql(false); + }); }); - it('should export all events when paging is true', async () => { - const events = Array.from({ length: 300 }, () => ({ event: 1 })); - - testServer - .intercept({ - path: getPath('tracker/events'), - method: 'GET', - query: { async: false, paging: true }, - }) - .reply(200, { events }); - - const finalState = await execute( - tracker.export('events', { paging: true }), - )(state); + describe('export', () => { + it('should export a resource by path with filter query params', async () => { + testServer + .intercept({ + path: getPath('tracker/enrollments'), + method: 'GET', + query: { orgUnit: 'TSyzvBiovKh' }, + }) + .reply(200, { + instances: [{ enrollment: 'abc123', orgUnit: 'TSyzvBiovKh' }], + pager: { page: 1, pageSize: 50 }, + }); + + const finalState = await execute( + tracker.export('enrollments', { orgUnit: 'TSyzvBiovKh' }), + )(state); + + expect(finalState.data.instances).to.have.lengthOf(1); + expect(finalState.data.instances[0].enrollment).to.eql('abc123'); + }); - expect(finalState.data.events).to.have.lengthOf(300); - expect(finalState.data.events[0]).to.eql({ event: 1 }); - }); - it('should export only 50 events when paging is not sent', async () => { - const events = Array.from({ length: 300 }, () => ({ event: 1 })); + it('should send paging: false to fetch all results without paging', async () => { + const allEvents = Array.from({ length: 300 }, (_, i) => ({ + event: `ev${i}`, + })); + + testServer + .intercept({ + path: getPath('tracker/events'), + method: 'GET', + query: { paging: false }, + }) + .reply(200, { instances: allEvents }); + + const finalState = await execute( + tracker.export('events', { paging: false }), + )(state); + + expect(finalState.response.query.paging).to.eql(false); + expect(finalState.data.instances).to.have.lengthOf(300); + }); - testServer - .intercept({ - path: getPath('tracker/events'), - method: 'GET', - query: { async: false }, - }) - .reply(200, { events: events.slice(0, 50) }); + it('should send page and pageSize for paginated requests', async () => { + testServer + .intercept({ + path: getPath('tracker/events'), + method: 'GET', + query: { page: 2, pageSize: 10 }, + }) + .reply(200, { + instances: Array.from({ length: 10 }, (_, i) => ({ + event: `ev${i}`, + })), + pager: { page: 2, pageSize: 10 }, + }); + + const finalState = await execute( + tracker.export('events', { page: 2, pageSize: 10 }), + )(state); + + expect(finalState.response.query.page).to.eql(2); + expect(finalState.response.query.pageSize).to.eql(10); + expect(finalState.data.instances).to.have.lengthOf(10); + expect(finalState.data.pager.page).to.eql(2); + }); - const finalState = await execute(tracker.export('events'))(state); + it('should send totalPages: true to include total count in pager', async () => { + testServer + .intercept({ + path: getPath('tracker/events'), + method: 'GET', + query: { totalPages: true, pageSize: 10000 }, + }) + .reply(200, { + instances: [], + pager: { page: 1, pageSize: 10000, pageCount: 3, total: 25000 }, + }); + + const finalState = await execute( + tracker.export('events', { totalPages: true, pageSize: 10000 }), + )(state); + + expect(finalState.response.query.totalPages).to.eql(true); + expect(finalState.data.pager).to.include({ pageCount: 3, total: 25000 }); + }); - expect(finalState.data.events).to.have.lengthOf(50); - expect(finalState.data.events[0]).to.eql({ event: 1 }); + it('should send order param for sorted results', async () => { + testServer + .intercept({ + path: getPath('tracker/events'), + method: 'GET', + query: { order: 'createdAt:desc' }, + }) + .reply(200, { instances: [{ event: 'ev1' }] }); + + const finalState = await execute( + tracker.export('events', { order: 'createdAt:desc' }), + )(state); + + expect(finalState.response.query.order).to.eql('createdAt:desc'); + expect(finalState.data.instances).to.have.lengthOf(1); + }); }); }); From fd37b92d6bddb796df91d90f6a4d9f2032439ff2 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Wed, 3 Jun 2026 12:52:27 +0300 Subject: [PATCH 11/14] update changeset --- .changeset/hip-planes-see.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/hip-planes-see.md b/.changeset/hip-planes-see.md index be48a842e4..0365c576a6 100644 --- a/.changeset/hip-planes-see.md +++ b/.changeset/hip-planes-see.md @@ -2,4 +2,5 @@ '@openfn/language-dhis2': patch --- -Fix tracker exports and imports when no query object is provided by defaulting `async` to `false` +Default `async:false` in `tracker.import` and paging examples to +`tracker.export()` From 0f34f299320505740a2c2b0dce865f93caf34957 Mon Sep 17 00:00:00 2001 From: Emmanuel Evance Date: Wed, 3 Jun 2026 12:53:30 +0300 Subject: [PATCH 12/14] fix typo --- .changeset/hip-planes-see.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/hip-planes-see.md b/.changeset/hip-planes-see.md index 0365c576a6..78cd218a20 100644 --- a/.changeset/hip-planes-see.md +++ b/.changeset/hip-planes-see.md @@ -2,5 +2,5 @@ '@openfn/language-dhis2': patch --- -Default `async:false` in `tracker.import` and paging examples to +Default `async:false` in `tracker.import` and add paging examples to `tracker.export()` From 04428e8b3bbb7462b4dd92c1eb713cd88ae07b57 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Tue, 16 Jun 2026 12:07:48 +0300 Subject: [PATCH 13/14] fix: update chngeset and tracker pagination examples Signed-off-by: Hunter Achieng --- .changeset/hip-planes-see.md | 5 +++-- packages/dhis2/src/tracker.js | 29 ++--------------------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/.changeset/hip-planes-see.md b/.changeset/hip-planes-see.md index 78cd218a20..283ebd0fd5 100644 --- a/.changeset/hip-planes-see.md +++ b/.changeset/hip-planes-see.md @@ -1,6 +1,7 @@ --- -'@openfn/language-dhis2': patch +'@openfn/language-dhis2': minor --- -Default `async:false` in `tracker.import` and add paging examples to +Add async option to `tracker.import` and +Add paging examples to `tracker.export()` diff --git a/packages/dhis2/src/tracker.js b/packages/dhis2/src/tracker.js index 8a7c501ac9..baff4be435 100644 --- a/packages/dhis2/src/tracker.js +++ b/packages/dhis2/src/tracker.js @@ -47,7 +47,7 @@ import * as util from './util.js'; * @param {string} strategy - The effect the import should have. Can either be CREATE, UPDATE, CREATE_AND_UPDATE and DELETE. * @param {object} payload - The data to be imported. * @param {TrackerOptions} [options] - An optional object containing parseAs, and apiVersion, and queries for the request - * @param {boolean} [options.async=false] - Whether to perform the import asynchronously. Defaults to false. + * @param {boolean} [options.async=false] - Whether to perform the import asynchronously. Defaults to false. See [Sync and async imports](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-master/tracker.html#sync-and-async) * @state {DHIS2State} * @returns {Operation} */ @@ -97,34 +97,9 @@ export { _import as import }; * tracker.export('enrollments', {orgUnit: 'TSyzvBiovKh'}); * @example Export all events * tracker.export('events', { paging: false}) - * @example Export all events with pagination - * tracker.export('events', { totalPages: true, pageSize: 1e4 }); - * fn(state => { - * state.results = state.data.instances; - * const { page, pageSize, pageCount, total } = state.data.pager; - * const remainingPages = pageCount - page; - * - * state.pages = Array.from({ length: remainingPages }, (_, i) => page + i + 1); - * state.pageSize = pageSize; - * return state; - * }); - * - * each( - * $.pages, - * tracker - * .export('events', { pageSize: $.pageSize, page: $.data }) - * .then(state => { - * state.results = state.results.concat(state.data.instances); - * return state; - * }), - * ); * @function * @param {string} path - Path to the resource, relative to the /tracker endpoint - * @param {object} query - An object of query parameters to be encoded into the URL. Can include pagination parameters, filters, etc. - * @param {number} [query.page=1] - Page number to return - * @param {number} [query.pageSize=50] - Number of results per page - * @param {boolean} [query.totalPages=false] - Whether to return total number of elements and pages - * @param {boolean} [query.paging=true] - Set to false to return all rows without paging + * @param {object} query - An object of query parameters to be encoded into the URL. Can include [pagination parameters](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-master/tracker.html#request-parameters-for-pagination), filters, etc. * @param {string} [query.order] - Comma-separated field:sortDirection pairs, e.g. `createdAt:desc` * @param {TrackerOptions} [options] - An optional object containing parseAs, and apiVersion for the request * @state {DHIS2State} From a38e154b91aec529d9b825b7e5062a19fab13054 Mon Sep 17 00:00:00 2001 From: Hunter Achieng Date: Thu, 18 Jun 2026 00:29:54 +0300 Subject: [PATCH 14/14] fix: update docs Signed-off-by: Hunter Achieng --- .changeset/hip-planes-see.md | 6 +++--- packages/dhis2/src/tracker.js | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.changeset/hip-planes-see.md b/.changeset/hip-planes-see.md index 283ebd0fd5..e5623e5ca0 100644 --- a/.changeset/hip-planes-see.md +++ b/.changeset/hip-planes-see.md @@ -2,6 +2,6 @@ '@openfn/language-dhis2': minor --- -Add async option to `tracker.import` and -Add paging examples to -`tracker.export()` +Add async option to `tracker.import`. +Improve `tracker.export` docs by adding a pagination example and +linking pagination query parameters. diff --git a/packages/dhis2/src/tracker.js b/packages/dhis2/src/tracker.js index baff4be435..539e82d856 100644 --- a/packages/dhis2/src/tracker.js +++ b/packages/dhis2/src/tracker.js @@ -97,10 +97,11 @@ export { _import as import }; * tracker.export('enrollments', {orgUnit: 'TSyzvBiovKh'}); * @example Export all events * tracker.export('events', { paging: false}) + * @example Export the first page of events with pagination metadata + * tracker.export('events', { totalPages: true, pageSize: 1000, page: 1 }) * @function * @param {string} path - Path to the resource, relative to the /tracker endpoint * @param {object} query - An object of query parameters to be encoded into the URL. Can include [pagination parameters](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-master/tracker.html#request-parameters-for-pagination), filters, etc. - * @param {string} [query.order] - Comma-separated field:sortDirection pairs, e.g. `createdAt:desc` * @param {TrackerOptions} [options] - An optional object containing parseAs, and apiVersion for the request * @state {DHIS2State} * @returns {Operation}