webapp/src/ts/services/get-subject-summaries.service.ts:118 |
{ keys: [['shortcode', ref], ...] }, ids only; findSubjectId matches row.key[1] === id.toString() |
Cause 3: needs the doc path to map each summary's reference back to a contact, so the ids-only query becomes include_docs. Cause 1 for online users. Cause 4: a numeric reference is sent raw today and never matches the view's String() key; coercing at the boundary makes it start matching. |
Doc path or per-reference calls. Accept the new permission for online users. Whether numeric references should resolve (they arguably always should have). |
admin/src/js/services/get-subject-summaries.js:110 |
Same as the webapp copy |
Same, and the admin app is always online so cause 1 always applies. admin/src/js cannot use async/await. |
Same. |
admin/src/js/services/message-queue.js:135 |
{ keys: referenceKeys }, ids only; findIdByKey on row.key[1] |
Cause 3 and cause 1. Sits in the same $q.all as the reports_by_subject query that #<11150-migration-issue> migrates; a role reaching the message queue without can_view_contacts gets a rejected page rather than one without patient/place names. |
Doc path or per-key calls. Same permission decision as #11442's getRecipients in the same file; make it once. Sequence with the reports_by_subject migration of this function. |
shared-libs/lineage/src/hydration.js:248 (contactUuidByShortcode) |
{ keys }, ids only; row.key[1] to build a shortcode→uuid Map, falling back to the shortcode itself |
No data context: the lib is constructed with (Promise, DB) by api, sentinel, transitions and admin. Migrating means adding a context to the factory signature at every construction site, which is the #10019 problem. Cause 3 as well. |
Whether to give lineage a data context here or leave this caller on db.query with a comment pointing at a lineage-specific ticket. Recommend the latter. |
shared-libs/transitions/src/lib/utils.js:120 (getContact) |
{ key: ['shortcode', id], include_docs }, warns on rows.length > 1, returns rows[0].doc or .id |
Rows newly isContact-filtered (cause 2). Keeping the "more than one contact" warning needs limit: 2 rather than 1. Cause 4: shortCodeId is guarded for falsiness but not type; a numeric value throws instead of matching nothing. |
Confirm the filter. Keep the warning or drop it. Coerce or guard at the boundary. |
shared-libs/transitions/src/transitions/utils.js:62 (isIdUnique) |
{ key: ['shortcode', id] }, !rows.length |
Cause 2 is a correctness risk here. The id generator uses this to avoid re-issuing a shortcode. A shortcode held by a national_office doc or an unconfigured type: contact doc is in the view but filtered by the datasource, so it reports as unique and gets assigned again. |
Either leave this caller on db.query with a comment, or accept the risk explicitly with a release note. Recommend leaving it. |
shared-libs/transitions/src/transitions/update_clinics.js:26 (getContactByRefid) |
{ key: ['external', doc.refid], include_docs, limit: 1 }, then contactTypesUtils.getContactType |
Already discards non-contacts, so cause 2 costs nothing. refid is upper-cased on save (#5373), so the builder's upper-casing is a no-op. Cause 4: an undefined refid is sent as ['external', null] today and matches nothing; byExternalRefs([undefined]) throws, so guard before calling. Already has dataContext in scope. |
Lowest risk of the nine. Migrate; add the guard. |
shared-libs/rules-engine/src/pouchdb-provider.js:57 (contactsBySubjectId) |
{ keys, include_docs }; returns matched doc ids plus the subject ids that matched nothing, using row.key[1] to tell them apart |
No data context: the provider takes a PouchDB instance. Cause 3: which subject ids missed has to be re-derived from the returned docs' patient_id / place_id. Offline hot path; the provider already chunks keys at MAX_QUERY_KEYS. |
Whether to inject a data context into the provider (the webapp's rules-engine.service.ts would supply it) or leave this caller with a comment. Depends on the same decision for the reports_by_subject calls in this file (#<11150-migration-issue>). |
api/src/services/replication/authorization.js:456 (findContactsByReplicationKeys) |
{ keys }, row.key[1] to map each replication key to doc ids, falling back to the key as a doc id, then allDocs |
Security-sensitive. Cause 2: a subject that is a filtered doc drops out of the replication-key resolution, and its shortcode falls through to allDocs as a doc id and misses, so the reports about it can leave the user's replication scope. Cause 3 as well. Has dataContext available. |
Confirm with whoever owns replication that narrowing to configured contacts is acceptable here, or leave on db.query. Do not migrate this one on the strength of the sweep alone. |
Title: Migrate the
medic-client/contacts_by_referencecall sites onto theshortcodesandexternalRefsqualifierstitle: Migrate the
medic-client/contacts_by_referencecall sites onto theshortcodesandexternalRefsqualifierstype: improvement
priority: medium
domain: data-sync
labels:
Description
Split out of #10976, which is scoped to adding the qualifiers themselves.
#10976 adds
byShortcodesandbyExternalRefstocht-datasourceandexposes them on both contact list endpoints, but migrates no call site.
Every one of the nine existing
medic-client/contacts_by_referencereaderschanges behaviour when it moves onto the datasource, and each of those changes
needs a decision that the qualifier PR should not be making on its own.
This ticket is that work. It is not a mechanical sweep: the four causes below
affect different callers differently, one of them is a correctness risk for
shortcode generation, and two are visible to configurations in the field.
The four behaviour changes
/api/v1/contact, which is gated bycan_view_contacts. The CouchDB proxythe callers use today is not. Any online caller that migrates newly
requires that permission. The webapp picks the remote context for
online-only users (
cht-datasource.service.ts) and the admin app alwaysuses it.
isContactfiltering.contacts_by_referenceemits any doc whosetypeis in its hard-coded list, includingnational_officeandtype: contactdocs whosecontact_typeis not in settings. Bothdatasource paths filter rows through
isContact, so a migrated callerstops seeing those docs. For most callers that is a harmless narrowing.
For
isIdUniqueit is the opposite: a shortcode held by a filtered doclooks free, so the id generator can hand it out twice.
of keys and read
row.key[1]off the result to work out which key eachcontact matched. The datasource returns a page of ids or docs with no key
attached. A migrated batch caller either re-derives the match from the
returned doc (
patient_id/place_id/rc_code), which means using thedoc path and paying
include_docs, or issues one call per key.byShortcodesandbyExternalRefsreject blank,padded and non-string entries rather than passing them through to a query
that can only ever match nothing. Callers that today hand the view a
numeric shortcode or an
undefinedreference get a thrown error where theyused to get a silent empty result — or, if they add
String()at theboundary, start matching numeric references that never matched before.
Two callers also have no datasource context to migrate onto (see below).
Technical Context
Components:
webapp/src/ts/services/get-subject-summaries.service.ts—processReferencesadmin/src/js/services/get-subject-summaries.js—processReferencesadmin/src/js/services/message-queue.js—getSubjectsAndRegistrationsshared-libs/lineage/src/hydration.js—contactUuidByShortcodeshared-libs/transitions/src/lib/utils.js—getContact(behindgetContactUuid/getContact)shared-libs/transitions/src/transitions/utils.js—isIdUniqueshared-libs/transitions/src/transitions/update_clinics.js—getContactByRefidshared-libs/rules-engine/src/pouchdb-provider.js—contactsBySubjectIdapi/src/services/replication/authorization.js—findContactsByReplicationKeysExisting References:
byShortcodesandbyExternalRefsqualifiers toContact.v1#10976 — the qualifier ticket this one is split out of.medic-client/contacts_by_phonecall sites onto thephonesqualifier #11442 — the equivalent follow-up split out of AddbyPhonesqualifier toContact.v1#10973, for thecontacts_by_phonecallers. Same causes 1, 2 and 4; this ticket adds 3.earlier, closed attempt to move
shared-libs/lineageonto the datasource.What will exist to migrate onto (from #10976):
Qualifier.byShortcodes([...]),Qualifier.byExternalRefs([...]),Contact.v1.getUuidsPage/getUuids/getPage/getAllgetUuidsPageByShortcodes,getPageByShortcodes,getByShortcodesand theByExternalRefsequivalents ongetDatasource(ctx).v1.contactGET /api/v1/contact/uuidandGET /api/v1/contactwith?shortcode=and?external_ref=, both accepting a comma-joined listWhere a data context already exists:
api/src/services/data-context.js,shared-libs/transitions/src/data-context.js,admin/src/js/services/data-context.js, the webapp'sCHTDatasourceService.shared-libs/lineageandshared-libs/rules-enginehave none: lineage is built from
(Promise, DB)and the rules engine'sprovider from a PouchDB instance.
Requirements
Call sites
webapp/src/ts/services/get-subject-summaries.service.ts:118{ keys: [['shortcode', ref], ...] }, ids only;findSubjectIdmatchesrow.key[1] === id.toString()include_docs. Cause 1 for online users. Cause 4: a numeric reference is sent raw today and never matches the view'sString()key; coercing at the boundary makes it start matching.admin/src/js/services/get-subject-summaries.js:110admin/src/jscannot useasync/await.admin/src/js/services/message-queue.js:135{ keys: referenceKeys }, ids only;findIdByKeyonrow.key[1]$q.allas thereports_by_subjectquery that #<11150-migration-issue> migrates; a role reaching the message queue withoutcan_view_contactsgets a rejected page rather than one without patient/place names.getRecipientsin the same file; make it once. Sequence with thereports_by_subjectmigration of this function.shared-libs/lineage/src/hydration.js:248(contactUuidByShortcode){ keys }, ids only;row.key[1]to build a shortcode→uuidMap, falling back to the shortcode itself(Promise, DB)by api, sentinel, transitions and admin. Migrating means adding a context to the factory signature at every construction site, which is the #10019 problem. Cause 3 as well.db.querywith a comment pointing at a lineage-specific ticket. Recommend the latter.shared-libs/transitions/src/lib/utils.js:120(getContact){ key: ['shortcode', id], include_docs }, warns onrows.length > 1, returnsrows[0].docor.idisContact-filtered (cause 2). Keeping the "more than one contact" warning needslimit: 2rather than1. Cause 4:shortCodeIdis guarded for falsiness but not type; a numeric value throws instead of matching nothing.shared-libs/transitions/src/transitions/utils.js:62(isIdUnique){ key: ['shortcode', id] },!rows.lengthnational_officedoc or an unconfiguredtype: contactdoc is in the view but filtered by the datasource, so it reports as unique and gets assigned again.db.querywith a comment, or accept the risk explicitly with a release note. Recommend leaving it.shared-libs/transitions/src/transitions/update_clinics.js:26(getContactByRefid){ key: ['external', doc.refid], include_docs, limit: 1 }, thencontactTypesUtils.getContactTyperefidis upper-cased on save (#5373), so the builder's upper-casing is a no-op. Cause 4: an undefinedrefidis sent as['external', null]today and matches nothing;byExternalRefs([undefined])throws, so guard before calling. Already hasdataContextin scope.shared-libs/rules-engine/src/pouchdb-provider.js:57(contactsBySubjectId){ keys, include_docs }; returns matched doc ids plus the subject ids that matched nothing, usingrow.key[1]to tell them apartpatient_id/place_id. Offline hot path; the provider already chunkskeysatMAX_QUERY_KEYS.rules-engine.service.tswould supply it) or leave this caller with a comment. Depends on the same decision for thereports_by_subjectcalls in this file (#<11150-migration-issue>).api/src/services/replication/authorization.js:456(findContactsByReplicationKeys){ keys },row.key[1]to map each replication key to doc ids, falling back to the key as a doc id, thenallDocsallDocsas a doc id and misses, so the reports about it can leave the user's replication scope. Cause 3 as well. HasdataContextavailable.db.query. Do not migrate this one on the strength of the sweep alone.Migrate every site whose decision comes out in favour. Any site deliberately
left on
db.querykeeps a comment pointing at whatever ticket covers it, as#11151 did for its aggregate callers.
Tests
db.medic.queryarguments —shared-libs/transitions/test/unit/utils.js,shared-libs/transitions/test/unit/patient_registration.js,admin/tests/unit/services/message-queue.spec.js,webapp/tests/karma/ts/services/get-subject-summaries.service.spec.ts,api/tests/mocha/services/replication/authorization.spec.js— assert thedatasource call instead. Do not keep both.
behaviour: unconfigured
type: contactdoc → not returned; numericreference → whatever is decided; undefined
refid→ no lookup.N keys in, each summary/message/subject matched to the right contact, and a
key that matches nothing handled the way it is today.
tests/integration/sentinel/transitions/registration.spec.jsand
tests/integration/transitions/sentinel-api-transitions.spec.jsstaygreen without modification.
Acceptance Criteria
cht-datasource; any site leftbehind carries a comment saying why and which ticket covers it.
grep -rnE "query\(['\"]medic-client/contacts_by_reference['\"]" webapp/src admin/src api/src shared-libs/*/srcreturns only the deliberately-skipped sites. The view name legitimately
survives in
shared-libs/cht-datasource/src/local/contact.tsand its tests.npm run unit-shared-lib,npm run unit-api,npm run unit-adminandnpm run unit-webapppass.Constraints
admin/src/jsis bundled through ng-annotate's 2015 acorn and cannot useasync/await. Both admin callers stay promise-chained.byShortcodescoerce or trim. Coercion is the caller's job;that is the parity constraint inherited from Add
byShortcodesandbyExternalRefsqualifiers toContact.v1#10976.ddocs/medic-db/medic-client/views/contacts_by_reference/map.js.Emitting the doc type as the view value would let
isContactrun onrow.valueand save the uuid path aninclude_docs, and emitting thematched field would restore the key association without docs — both are
ddoc changes and a reindex, so out of scope here.
shared-libs/lineageorshared-libs/rules-enginea datacontext as a side effect of this sweep. If either is decided on, it is its
own change with its own ticket.
caller migrates and starts requiring
can_view_contacts, that goes in therelease notes.
References
Similar Implementations:
medic-client/contacts_by_phonecall sites onto thephonesqualifier #11442 — thecontacts_by_phonecaller migration, same shape of ticketbyForms, whose out-of-scopecallers were split into Add an aggregate API surface to Report.v1 (countByForm) for the grouped reports_by_form callers #11328 the same way
Documentation:
ddocs/medic-db/medic-client/views/contacts_by_reference/map.jsshared-libs/cht-datasource/README.md