Repository: medplum/medplum
Pull request #6280: feat(server): add `qualification-code` search param for `Practitioner` 
Author: login: ThatOneBro
Labels: name: documentation
description: Improvements or additions to documentation
name: fhir-datastore
description: Related to the FHIR datastore, includes API and FHIR operations
name: search
description: Features and fixes related to search
Created: 2025-04-01T01:06:40Z | Merged: 2025-04-09T19:06:01Z

Fixes #6277

Linked issue #6277: Practitioner custom search param: qualification-code
Add `qualification-code` as a custom search parameter for Practitioner. It searches on `Practitioner.qualification.code`

See [FHIR R6 Practitioner](https://build.fhir.org/practitioner.html#search) for reference

Diff:
diff --git a/packages/definitions/dist/fhir/r4/search-parameters-medplum.json b/packages/definitions/dist/fhir/r4/search-parameters-medplum.json
index 3303046bd31..628b73b0160 100644
--- a/packages/definitions/dist/fhir/r4/search-parameters-medplum.json
+++ b/packages/definitions/dist/fhir/r4/search-parameters-medplum.json
@@ -1318,6 +1318,23 @@
         "type": "token",
         "expression": "AsyncJob.status"
       }
+    },
+    {
+      "fullUrl": "https://medplum.com/fhir/SearchParameter/Practitioner-qualification-code",
+      "resource": {
+        "resourceType": "SearchParameter",
+        "id": "Practitioner-qualification-code",
+        "url": "https://medplum.com/fhir/SearchParameter/Practitioner-qualification-code",
+        "version": "4.0.1",
+        "name": "qualification-code",
+        "status": "draft",
+        "publisher": "Medplum",
+        "description": "The type of qualification",
+        "code": "qualification-code",
+        "base": ["Practitioner"],
+        "type": "token",
+        "expression": "Practitioner.qualification.code"
+      }
     }
   ]
 }
diff --git a/packages/docs/static/data/resourceDefinitions/practitioner.json b/packages/docs/static/data/resourceDefinitions/practitioner.json
index a9e1a8401d7..31281d4102e 100644
--- a/packages/docs/static/data/resourceDefinitions/practitioner.json
+++ b/packages/docs/static/data/resourceDefinitions/practitioner.json
@@ -555,6 +555,12 @@
       "type": "string",
       "description": "A server defined search that may match any of the string fields in the HumanName, including family, give, prefix, suffix, suffix, and/or text",
       "expression": "Practitioner.name"
+    },
+    {
+      "name": "topic",
+      "type": "token",
+      "description": "The type of qualification",
+      "expression": "Practitioner.qualification.code"
     }
   ]
 }
diff --git a/packages/server/src/fhir/repo.ts b/packages/server/src/fhir/repo.ts
index dea43a855a1..c4bee0127a0 100644
--- a/packages/server/src/fhir/repo.ts
+++ b/packages/server/src/fhir/repo.ts
@@ -1338,7 +1338,7 @@ export class Repository extends FhirRepository<PoolClient> implements Disposable
    * The value should be incremented each time there is a change in the schema (really just columns)
    * of the resource tables or when there are code changes to `buildResourceRow`.
    */
-  static readonly VERSION: number = 1;
+  static readonly VERSION: number = 2;
 
   private buildResourceRow(resource: Resource): Record<string, any> {
     const resourceType = resource.resourceType;
diff --git a/packages/server/src/fhir/search-params.test.ts b/packages/server/src/fhir/search-params.test.ts
index 00514f35d46..fe57e88cd7a 100644
--- a/packages/server/src/fhir/search-params.test.ts
+++ b/packages/server/src/fhir/search-params.test.ts
@@ -327,4 +327,45 @@ describe('Medplum Custom Search Parameters', () => {
 
       expect(result.entry).toHaveLength(1);
     }));
+
+  test('Search for Practitioner by qualification-code', () =>
+    withTestContext(async () => {
+      const practitioner1 = await repo.createResource<Practitioner>({
+        resourceType: 'Practitioner',
+        name: [{ given: ['Alice'], family: 'A' }],
+        qualification: [
+          {
+            code: {
+              coding: [
+                { code: 'MD', system: 'http://terminology.hl7.org/CodeSystem/v2-0360', display: 'Doctor of Medicine' },
+              ],
+            },
+          },
+        ],
+      });
+      expect(practitioner1).toBeDefined();
+
+      const practitioner2 = await repo.createResource<Practitioner>({
+        resourceType: 'Practitioner',
+        name: [{ given: ['Bob'], family: 'B' }],
+        qualification: [
+          {
+            code: {
+              coding: [
+                { code: 'RN', system: 'http://terminology.hl7.org/CodeSystem/v2-0360', display: 'Registered Nurse' },
+              ],
+            },
+          },
+        ],
+      });
+      expect(practitioner2).toBeDefined();
+
+      const result = await repo.search({
+        resourceType: 'Practitioner',
+        filters: [{ code: 'qualification-code', operator: Operator.EQUALS, value: 'MD' }],
+      });
+
+      expect(result.entry).toHaveLength(1);
+      expect(result.entry?.[0]?.resource).toMatchObject(practitioner1);
+    }));
 });


Reviews:
user: login: ThatOneBro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-01T01:08:11Z
user: login: finnbergquist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T17:11:55Z
user: login: mattlong
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-01T17:47:26Z

Review comments:
diff_hunk: @@ -11,7 +11,7 @@ import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components
 
 # PasswordChangeRequest
 
-@deprecated Password change request for the 'forgot password' flow. Use UserSecurityRequest instead.
+@deprecated Password change request for the 'forgot password' flow. Use UserSecurityCheck instead.
path: packages/docs/docs/api/fhir/medplum/passwordchangerequest.mdx
user: login: ThatOneBro
body: Looks like a change that came from previous PR that was not run through generator
created_at: 2025-04-01T01:08:08Z
author_association: MEMBER
diff_hunk: @@ -11,7 +11,7 @@ import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components
 
 # PasswordChangeRequest
 
-@deprecated Password change request for the 'forgot password' flow. Use UserSecurityRequest instead.
+@deprecated Password change request for the 'forgot password' flow. Use UserSecurityCheck instead.
path: packages/docs/docs/api/fhir/medplum/passwordchangerequest.mdx
user: login: finnbergquist
body: Oh I made that change. Didn't realize that gets auto generated.
created_at: 2025-04-01T17:11:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-01T01:06:45Z
body: [vc]: #9R7iTOrat08ASWwBlIQEhqPogMLaP2M3+SkMRraoEdw=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJtZWRwbHVtLWFwcCIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy9hcHAiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9tZWRwbHVtL21lZHBsdW0tYXBwL0FUZFB1Q01Xcjk0WWFDNUY4QmdmYTlSVVU4MUQiLCJuZXh0Q29tbWl0U3RhdHVzIjoiU0tJUFBFRCIsInByZXZpZXdVcmwiOiIifSx7Im5hbWUiOiJtZWRwbHVtLXN0b3J5Ym9vayIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy9yZWFjdCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL21lZHBsdW0vbWVkcGx1bS1zdG9yeWJvb2svRGtZVnV1Wm1MMkQ4WmVYaUcxcDJ2bmVUaDZSdSIsIm5leHRDb21taXRTdGF0dXMiOiJTS0lQUEVEIiwicHJldmlld1VybCI6IiJ9LHsibmFtZSI6Im1lZHBsdW0td3d3Iiwicm9vdERpcmVjdG9yeSI6InBhY2thZ2VzL2RvY3MiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbWVkcGx1bS9tZWRwbHVtLXd3dy80UXhTSjJocTZZMU5NelhWM0hFaWt0SDNZam82IiwicHJldmlld1VybCI6Im1lZHBsdW0td3d3LWdpdC1kZXJyaWNrLXByYWN0aXRpb25lci1xdWFsaWZpY2F0aW9uLWNvZGUtbWVkcGx1bS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6Im1lZHBsdW0td3d3LWdpdC1kZXJyaWNrLXByYWN0aXRpb25lci1xdWFsaWZpY2F0aW9uLWNvZGUtbWVkcGx1bS52ZXJjZWwuYXBwIn19LHsibmFtZSI6Im1lZHBsdW0tcHJvdmlkZXIiLCJyb290RGlyZWN0b3J5IjoiZXhhbXBsZXMvbWVkcGx1bS1wcm92aWRlci8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbWVkcGx1bS9tZWRwbHVtLXByb3ZpZGVyL0hYVGJGcFBXVVRyQkY2b3VqWnNtcUppaEdpVDUiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **medplum-www** | ✅ Ready ([Inspect](https://vercel.com/medplum/medplum-www/4QxSJ2hq6Y1NMzXV3HEiktH3Yjo6)) | [Visit Preview](https://medplum-www-git-derrick-practitioner-qualification-code-medplum.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/medplum-www-git-derrick-practitioner-qualification-code-medplum.vercel.app?via=pr-comment-feedback-link) | Apr 6, 2025 11:16pm |

<details><summary>3 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **medplum-app** | ⬜️ Skipped ([Inspect](https://vercel.com/medplum/medplum-app/ATdPuCMWr94YaC5F8Bgfa9RUU81D)) |  |  | Apr 6, 2025 11:16pm |
| **medplum-provider** | ⬜️ Skipped ([Inspect](https://vercel.com/medplum/medplum-provider/HXTbFpPWUTrBF6oujZsmqJihGiT5)) |  |  | Apr 6, 2025 11:16pm |
| **medplum-storybook** | ⬜️ Skipped ([Inspect](https://vercel.com/medplum/medplum-storybook/DkYVuuZmL2D8ZeXiG1p2vneTh6Ru)) |  |  | Apr 6, 2025 11:16pm |
</details>





author_association: NONE
user: login: mattlong
created_at: 2025-04-01T01:52:43Z
body: We need to get in the habit of bumping Repository.VERSION for new search parameters (including this one)
author_association: MEMBER
user: login: ThatOneBro
created_at: 2025-04-04T21:03:30Z
body: I think we want to batch this with https://github.com/medplum/medplum/pull/6155 just before merging https://github.com/medplum/medplum/pull/6291 to avoid too many bumps in minor version (for post-deploy migrations)
author_association: MEMBER
user: login: sonarqubecloud[bot]
created_at: 2025-04-06T23:30:44Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=medplum_medplum&pullRequest=6280) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=medplum_medplum&pullRequest=6280&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=medplum_medplum&pullRequest=6280&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=medplum_medplum&pullRequest=6280&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=medplum_medplum&pullRequest=6280&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=medplum_medplum&pullRequest=6280&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=medplum_medplum&pullRequest=6280)


author_association: NONE