Add: airqo adaptor - #1757
Add: airqo adaptor#1757Dhie-boop wants to merge 6 commits into
Conversation
|
@mtuchi @josephjclark @PiusKariuki Happy to address any feedback. |
|
Hey @Dhie-boop, thanks for putting this together! Nice to see AirQo getting adaptor support. Could you say a bit more about the use case driving this? What jobs or integrations are you planning to build on top of it? Asking because our usual guidance for a new adaptor is to start small with the generic HTTP helpers ( Would that work as a starting point here?
|
|
Thanks for the thoughtful feedback! @mtuchi That approach makes sense to me. Primarily I wanted to bridge the gap between environmental monitoring and national health/environmental reporting workflows. The goal is to create data pipelines that pull real-time air quality metrics (PM2.5 and PM10) from AirQo sensors and automatically sync them with respective platforms. I completely agree with starting small Then, once we have our cron and webhook pipelines running end-to-end and can see the precise query parameters we rely on most, I will promote the highest-value helpers starting with Let me know if you would like me to adjust the current PR to this scope. |
|
@Dhie-boop yes please update this pr to cover that |
|
@mtuchi Thanks will update. |
…ial measurement functions
…cope down to core HTTP operations
Resolve conflict in pnpm-workspace.yaml: keep main's overrides (form-data ^2.5.6, json-sql>underscore, twilio>axios) and drop the local-only scriptShell override that had been accidentally committed.
|
@mtuchi I made some changes checkout I have remove the followings;
All other resource-specific functions (and their now-unused helpers) from the Changes
Validation
|
|
|
||
| // Namespace alias grouping the generic helpers above; each member is already | ||
| // documented and exported individually as `get`, `post`, and `request`. | ||
| export const http = { get, post, request }; |
There was a problem hiding this comment.
@Dhie-boop please move the http functions in http.js
and then export it in index.js
export * as http from './http.js';There was a problem hiding this comment.
@Dhie-boop can you add a rectangle.png (512x190px) logo here ?
| } | ||
| }; | ||
|
|
||
| const ISO_8601_RE = |
There was a problem hiding this comment.
what does this do ? 🤔
This ISO_8601_RE which I have rename it to ISO_8601_DATE_TIME_RE, is a regex which is used by assertMeasurementParams to validate startTime/endTime query params before they're compared as dates or sent to AirQo. so this rejects malformed strings and date-only strings without a timezone. It's actively used and covered by existing tests, so I thought keeping it.
| * Also available as `http.get`. | ||
| * | ||
| * @example <caption>Get raw data from any AirQo path</caption> | ||
| * get('devices/measurements/sites/60d058c8048305120d######/recent'); |
| * Also available as `http.post`. | ||
| * | ||
| * @example <caption>Post a body to any AirQo path</caption> | ||
| * post('devices/some-endpoint', { foo: 'bar' }); |
There was a problem hiding this comment.
once moved to http.js this should be http.post()
| * Also available as `http.request`. | ||
| * | ||
| * @example <caption>Make an arbitrary request</caption> | ||
| * request('GET', 'devices/metadata/grids'); |
| @@ -0,0 +1,84 @@ | |||
| /** | |||
| * INVARIANT: Must export a function named `request`. | |||
There was a problem hiding this comment.
Can you remove the AI generated comments ?, unless they are very useful?
|
Hey @Dhie-boop i have reported couple of areas you can fix in this pr |
Thanks for the valuable feedback @mtuchi |
|
@mtuchi Have made the changes
|
| const opts = { | ||
| parseAs: 'json', | ||
| errors, | ||
| baseUrl: baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`, |
There was a problem hiding this comment.
you don't need "baseUrl.endsWith('/') ? baseUrl : ${baseUrl}/". Just baseUrl is enough
| } | ||
| }; | ||
|
|
||
| const assertAllowedValue = (value, allowedValues, label) => { |
There was a problem hiding this comment.
Please move this function to Utils.js, you can use it in adaptor functions util.assertAllowedValue
|
|
||
| const VALID_ENTITY_TYPES = ['sites', 'devices', 'grids', 'cohorts']; | ||
|
|
||
| const assertNonEmptyString = (value, label) => { |
There was a problem hiding this comment.
Please move this to Utils.js
| * - `'grids'` — a named geographic grid (e.g. a city or country) | ||
| * - `'cohorts'` — a user-defined group of devices | ||
| * | ||
| * @example <caption>Get recent measurements for a site</caption> |
| * | ||
| * @example <caption>Get recent measurements for a site</caption> | ||
|
|
||
| * @example <caption>Get recent measurements for a grid</caption> |
|
|
||
| * @example <caption>Get recent measurements for a grid</caption> | ||
|
|
||
| * @example <caption>Get recent measurements for a device</caption> |
| * | ||
| * @example <caption>Get historical measurements for a site</caption> | ||
|
|
||
| * @example <caption>Get historical measurements for a grid with date range</caption> |
| /** | ||
| * Make a generic authenticated GET request to any AirQo endpoint. | ||
| * | ||
| * @example <caption>Get raw data from any AirQo path</caption> |
| import { expandReferences } from '@openfn/language-common/util'; | ||
| import * as util from './Utils.js'; | ||
|
|
||
| const assertNonEmptyString = (value, label) => { |
There was a problem hiding this comment.
please move this to Util.js
|
Thanks @mtuchi for feedback. |
| */ | ||
|
|
||
| /** | ||
| * @typedef {Object} AirQoState |
There was a problem hiding this comment.
This typedef is not used anywhere i would suggest you remove it. and run pnpm build to make sure docs are built okay @Dhie-boop
| * @param {object} options | ||
| */ | ||
| export const request = (configuration = {}, method, path, options = {}) => { | ||
| const { baseUrl = 'https://api.airqo.net/api/v2', token } = configuration; |
There was a problem hiding this comment.
Please add support for X-Client-Secret https://platform.airqo.net/docs/api/getting-started/authentication/#additional-security-controls
The token by it self is not very secure since it's passed on query params
There was a problem hiding this comment.
@Dhie-boop i have just couple of more change request for you
- Remove the
AirQoStateandAirQoMeasurementtypedef, since they are not used anywhere. I would i advice to add link to the airqo docs instead - For
paramsdocs especially ingetMesurementsfunctions please include link to airqo docs Eg:{@link https://platform.airqo.net/docs/api/for-partners/historical-data/#request-parameters Request parameters} - Add support for
X-Client-Secreteinutil.request. This encourages best security controls sincetokenis being passed in query params getRecentMeasurements()does not have request params from airqo api, please update the function signature- Add actual examples in each adaptor function.
* @example <caption>Get recent measurements for a site</caption>
* getRecentMeasurements("sites", "site123")
or in http.get()
* @example <caption>Get recent measurements for a site</caption>
* http.get("devices/measurements/sites/site123/recent")
Don't forget to update pr notes to reflect the latest changes, and run pnpm build before pushing changes
|
@mtuchi summary of the new changes;
Output of the final
|

Summary
Add a new
@openfn/language-airqoadaptor for the AirQo API, which provides air quality monitoring data from African cities.Details
Functions added
getRecentMeasurements(entityType, entityId, params)getHistoricalMeasurements(entityType, entityId, params)getAllHistoricalMeasurements(entityType, entityId, params)listMetadata(resource, params)listSites(params)listMetadata('sites')listDevices(params)listMetadata('devices')listGrids(params)listMetadata('grids')listCohorts(params)listMetadata('cohorts')getGridSummary()getDailyForecast(params)getHourlyForecast(params)toDhis2DataValues(measurements, mappingConfig, options)dataValuesshape for batch importOther files added
src/Adaptor.js— all job-facing functionssrc/Utils.js—requesthelper andprepareNextStatesrc/index.js— package entry pointtest/Adaptor.test.js— 56 unit tests (Undici MockAgent, no live network calls)configuration-schema.json— credential schema (token, optionalbaseUrl)package.json— package definitionREADME.md— usage guide and DHIS2 workflow exampleKey design decisions
tokenquery parameter, as required by the AirQo API.getAllHistoricalMeasurementshandles pagination automatically. Callers get one flat array regardless of how many pages the API returns.toDhis2DataValuesproduces asummaryobject alongside thedataValuesarray, listing exactly which measurements were skipped and why — useful for diagnosing org-unit mapping gaps without inspecting every record.tokencredential is never written to logs. The AirQo API takes auth as a query parameter, so the request logger redacts it before printing the request URL.Testing
AI Usage
Please disclose how you've used AI in this work (it's cool, we just want to
know!):
Parts of this adaptor — including initial function scaffolding, JSDoc, unit test structure, and code review — were developed with assistance from Claude Opus (Anthropic) and GPT-5.5 (OpenAI). All AI-generated code was reviewed, tested, and validated by the author before inclusion. The final implementation, architectural decisions, and test coverage are the responsibility of the human author. No proprietary data or credentials were shared with any AI tool during development.
You can read more details in our
Responsible AI Policy
Review Checklist
Before merging, the reviewer should check the following items:
production? Is it safe to release?
dev only changes don't need a changeset. — Not added; this is a new adaptor with no existing consumers to break.