Repository: CDCgov/dibbs-query-connector
Pull request #490: add auditable decorators
Author: login: fzhao99
Created: 2025-04-04T18:09:31Z | Merged: 2025-04-04T21:32:26Z

# PULL REQUEST

## Summary
Backend code that extends the `auditable` decorator / adds it to code interfacing with FHIR servers. 

This PR
- Adds the migration for the `audit_logs` table
- Rearchitects some of the audit log code to make the functionality more testable
- Adds retry logic to the audit function 
    - This is set to 3 for now but can be tweaked if we want to make the code try more 
- Annotates the FHIR interface code with `auditable` 
    - User management / group management / fhirServer addition are left for [future tickets ](https://linear.app/skylight-cdc/issue/QUE-193/methods-doing-a-database-write-update-are-marked-as-auditable) since I think we want these to have associated tests like the ones seeded for the code added here
- Adds integration tests for all the above code 
  
## Related Issue
Fixes QUE-179 and QUE-192

## Additional Information
Additional auditable code that touches user session and group management are left for other tickets, but looking to get this in to unblock other work (tracked under [this ticket](https://linear.app/skylight-cdc/issue/QUE-191/annotate-external-method-calls-with-auditable-decorator))

## Checklist

- [x] Descriptive Pull Request title
- [x] Link to relevant issues
- [x] Ensure test coverage is above agreed upon threshold
- [x] Update documentation



Diff:
diff --git a/query-connector/flyway/sql/V10_01__add_audit_tables.sql b/query-connector/flyway/sql/V10_01__add_audit_tables.sql
new file mode 100644
index 000000000..033d71e94
--- /dev/null
+++ b/query-connector/flyway/sql/V10_01__add_audit_tables.sql
@@ -0,0 +1,11 @@
+CREATE TABLE IF NOT EXISTS audit_logs (
+    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
+    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+    author TEXT,
+    action_type TEXT,
+    audit_checksum TEXT,
+    audit_message JSON
+);
+
+-- Create indexes for ids and foreign keys
+CREATE INDEX IF NOT EXISTS audit_logs_id_index ON audit_logs (id);
diff --git a/query-connector/src/app/(pages)/auditLogs/AuditLogs.test.tsx b/query-connector/src/app/(pages)/auditLogs/AuditLogs.test.tsx
index ae86ea3cc..8263fbede 100644
--- a/query-connector/src/app/(pages)/auditLogs/AuditLogs.test.tsx
+++ b/query-connector/src/app/(pages)/auditLogs/AuditLogs.test.tsx
@@ -7,6 +7,34 @@ const TEST_NAME = "Rocky Balboa";
 const TEST_REPORT = "Created Report";
 const NUM_ROWS = 26;
 
+export const BASE_TEST_DATA = [
+  {
+    name: "Rocky Balboa",
+    action: "Created Report",
+    date: new Date("2025-03-10T14:30:00Z"),
+  },
+  {
+    name: "Apollo Creed",
+    action: "Edited Report",
+    date: new Date("2025-03-09T09:15:00Z"),
+  },
+  {
+    name: "Rocky Balboa",
+    action: "Deleted Entry",
+    date: new Date("2022-03-08T17:45:00Z"),
+  },
+  {
+    name: "Clubber Lang",
+    action: "Created Report",
+    date: new Date("2024-03-07T12:00:00Z"),
+  },
+  {
+    name: "Ivan Drago",
+    action: "Viewed Entry",
+    date: new Date("2025-03-06T22:10:00Z"),
+  },
+];
+
 /**
  * Creates an enhanced user event with custom helper methods.
  * @param user - The user event from renderWithUser
diff --git a/query-connector/src/app/auditLogs/decorator.ts b/query-connector/src/app/auditLogs/decorator.ts
new file mode 100644
index 000000000..b777c6821
--- /dev/null
+++ b/query-connector/src/app/auditLogs/decorator.ts
@@ -0,0 +1,103 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { getDbClient } from "../backend/dbClient";
+import { generateAuditValues } from "./lib";
+
+export const AUDIT_LOG_MAX_RETRIES = 3;
+/**
+ * Decorator that adds audit log write logic to an annotated function
+ * @param target - reference back to the Object prototype of the class
+ * @param key - name of method
+ * @param descriptor - metadata about the functin
+ * @returns The result of the function, with the args / result logged appropriately
+ */
+export function auditable(
+  target: any,
+  key: string,
+  descriptor: PropertyDescriptor,
+) {
+  const dbConnection = getDbClient();
+  const method = descriptor.value;
+  const argLabels = target[key]
+    .toString()
+    // match everything between the first "(" and first ")" and use [1] to get
+    // everything in the first capture group (ie exclude the lead / trail parens)
+    .match(/\(([^)]*)\)/)[1]
+    .split(", ");
+
+  const writeToAuditTable = async (args: any[]) => {
+    const query = `INSERT INTO 
+        audit_logs (author, action_type, audit_checksum, audit_message) 
+        VALUES ($1, $2, $3, $4)
+        RETURNING id, action_type, audit_checksum`;
+
+    // using this recursive pattern with .then's rather than async / await to
+    // allow for the decorator to work on synchronous functions
+    const attemptWrite = (retryCounter: number): Promise<void> => {
+      return generateAuditValues(key, argLabels, args)
+        .then((auditValues) => dbConnection.query(query, auditValues))
+        .then((result) => {
+          const auditMetadata = result.rows[0];
+          console.info(
+            `${auditMetadata.action_type} audit action with id ${auditMetadata?.id} and checksum ${auditMetadata?.audit_checksum} added to audit table`,
+          );
+          return;
+        })
+        .catch((e) => {
+          console.error(
+            `Audit log write attempt ${
+              retryCounter + 1
+            } failed with error: ${e}. ${
+              retryCounter + 1 < AUDIT_LOG_MAX_RETRIES ? "Retrying..." : ""
+            }`,
+          );
+
+          if (retryCounter + 1 >= AUDIT_LOG_MAX_RETRIES) {
+            console.info(
+              `Audit log write attempt for ${key} failed after ${AUDIT_LOG_MAX_RETRIES} attempts.`,
+            );
+            return;
+          }
+
+          // Add exponential backoff
+          return new Promise<void>((resolve) =>
+            setTimeout(
+              () => {
+                resolve(attemptWrite(retryCounter + 1));
+              },
+              1000 * Math.pow(2, retryCounter),
+            ),
+          );
+        });
+    };
+
+    // Start the first attempt
+    return attemptWrite(0);
+  };
+
+  const decoratedFunctionIsAsync = method.constructor.name === "AsyncFunction";
+
+  // need to do this check so that async vs sync returns are properly typed
+  descriptor.value = decoratedFunctionIsAsync
+    ? async function (this: unknown, ...args: unknown[]) {
+        try {
+          writeToAuditTable(args);
+
+          return await method.apply(this, args);
+        } catch (error) {
+          console.error(`Async method ${key} threw error`, error);
+          throw error;
+        }
+      }
+    : function (this: unknown, ...args: unknown[]) {
+        try {
+          writeToAuditTable(args);
+
+          return method.apply(this, args);
+        } catch (error) {
+          console.error(`Method ${key} threw error`, error);
+          throw error;
+        }
+      };
+
+  return descriptor;
+}
diff --git a/query-connector/src/app/auditLogs/lib.ts b/query-connector/src/app/auditLogs/lib.ts
new file mode 100644
index 000000000..4cefbb643
--- /dev/null
+++ b/query-connector/src/app/auditLogs/lib.ts
@@ -0,0 +1,71 @@
+import { auth } from "@/auth";
+
+// these functions are exported from another file into the auditable file
+//  to allow us to spy on them when calling @auditable functions
+// Relevant thread: https://stackoverflow.com/questions/61111423/expected-spyon-function-to-be-called-jest
+
+/**
+ * Helper function that formats method input into the values needed by the audit
+ * log table
+ * @param methodName - name of the method to audit
+ * @param argLabels - name of the params
+ * @param args - value of the arguments
+ * @returns an array of values to be inserted into the table
+ */
+export async function generateAuditValues(
+  methodName: string,
+  argLabels: string[],
+  args: unknown[],
+) {
+  const session = await auth();
+  const author = `${session?.user?.username}`;
+
+  const auditContents = generateAuditMessage(argLabels, args);
+  const auditChecksum = generateAuditChecksum(author, auditContents);
+  return [author, methodName, auditChecksum, auditContents];
+}
+
+/**
+ * Helper function that generates message contents
+ * @param argLabels - the name of the parameters being passed into the function
+ * @param args - the values of the parameters being passed in
+ * @returns A JSON object with {argName: argValue}
+ */
+function generateAuditMessage(argLabels: string[], args: unknown[]) {
+  // can do more fancy serialization here if needed
+  const mappedArgs: { [s: string]: string } = {};
+  args.forEach((obj, i) => {
+    if (obj) {
+      const val = JSON.stringify(obj);
+      // these strings return truthy in the if(obj) so explicitly compare them
+      // to filter them on
+      if (val !== "{}" && val !== "[]") {
+        mappedArgs[argLabels[i]] = sanitizeString(JSON.stringify(obj));
+      }
+    }
+  });
+
+  return mappedArgs;
+}
+
+/**
+ * Helper function that generates a checksum for audit contents based on audit
+ * contents, author, and timestamp
+ * @param author - name of the author
+ * @param auditContents - the contents of the message being stored
+ * @returns A SHA-2 strength or greater checksum
+ */
+function generateAuditChecksum(author: string, auditContents: Object) {
+  // TODO: implement this properly
+  return "result of some SHA-2 hashing algo based on author and audit contents";
+}
+
+function sanitizeString(s: string) {
+  // add any other sanitization functions we need to this array
+  const functionsToApply = [redactBearerToken];
+  return functionsToApply.reduce((acc, cur) => cur(acc), s);
+}
+
+function redactBearerToken(s: string) {
+  return s.replace(/"Bearer\s+([^\s]+)\"}/g, '"Bearer REDACTED"}');
+}
diff --git a/query-connector/src/app/backend/dbServices/decorators.ts b/query-connector/src/app/backend/dbServices/decorators.ts
index 5ae499aca..b95627088 100644
--- a/query-connector/src/app/backend/dbServices/decorators.ts
+++ b/query-connector/src/app/backend/dbServices/decorators.ts
@@ -1,61 +1,6 @@
 import { adminAccessCheck, superAdminAccessCheck } from "@/app/utils/auth";
 import { getDbClient } from "../dbClient";
 
-/**
- * Decorator that adds audit log write logic to an annotated function
- * @param async - whether the method is async. Defaults to false
- * @returns The result of the function, with the args / result logged appropriately
- */
-export function auditable(async = false) {
-  return function (_: Object, key: string, descriptor: PropertyDescriptor) {
-    const dbConnection = getDbClient();
-    const method = descriptor.value;
-
-    const logAndReturnResult = (resultToLog: unknown, args?: unknown[]) => {
-      // would replace the below round trip to the DB with the actual write to
-      // the table
-      const query = "SELECT * FROM conditions";
-      const result = dbConnection.query(query);
-
-      result.then((v) => {
-        console.log(v.rows[0]); // in case we need to do anything with the db response
-        console.log("To add to audit log: ", resultToLog); // can write the result
-        if (args) {
-          console.log("Args to add to the audit log log: ", args); // or the args
-        }
-      });
-
-      return resultToLog;
-    };
-    // need to do this check so that async vs sync returns are properly typed
-    descriptor.value = async
-      ? async function (this: unknown, ...args: unknown[]) {
-          try {
-            const resultToLog = await method.apply(this, args);
-            logAndReturnResult("", args);
-
-            return resultToLog;
-          } catch (error) {
-            console.error(`Async method ${key} threw error`, error);
-            throw error;
-          }
-        }
-      : function (this: unknown, ...args: unknown[]) {
-          try {
-            const resultToLog = method.apply(this, args);
-            logAndReturnResult(resultToLog);
-
-            return resultToLog;
-          } catch (error) {
-            console.error(`Async method ${key} threw error`, error);
-            throw error;
-          }
-        };
-
-    return descriptor;
-  };
-}
-
 /**
  * Annotation to make a db query into a transaction. Requires all return branches
  * to include {success: boolean}, which will control the rollback behavior,
diff --git a/query-connector/src/app/backend/dbServices/fhir-servers.ts b/query-connector/src/app/backend/dbServices/fhir-servers.ts
index 437193981..e60efe345 100644
--- a/query-connector/src/app/backend/dbServices/fhir-servers.ts
+++ b/query-connector/src/app/backend/dbServices/fhir-servers.ts
@@ -4,6 +4,7 @@ import { getDbClient } from "../dbClient";
 import { transaction } from "./decorators";
 import { FhirServerConfig } from "@/app/models/entities/fhir-servers";
 import { superAdminAccessCheck } from "@/app/utils/auth";
+import { auditable } from "@/app/auditLogs/decorator";
 
 // Define an interface for authentication data
 export interface AuthData {
@@ -114,6 +115,7 @@ class FhirServerConfigService {
    * @returns An object indicating success or failure with optional error message
    */
   @transaction
+  @auditable
   static async updateFhirServer(
     id: string,
     name: string,
@@ -221,7 +223,9 @@ class FhirServerConfigService {
    * @param authData - Authentication data including auth type and credentials
    * @returns An object indicating success or failure with optional error message
    */
+
   @transaction
+  @auditable
   static async insertFhirServer(
     name: string,
     hostname: string,
@@ -301,6 +305,7 @@ class FhirServerConfigService {
    * @returns An object indicating success or failure with optional error message
    */
   @transaction
+  @auditable
   static async deleteFhirServer(id: string) {
     const deleteQuery = `
     DELETE FROM fhir_servers 
diff --git a/query-connector/src/app/shared/query-service.ts b/query-connector/src/app/shared/query-service.ts
index 85705a7de..c9c18d205 100644
--- a/query-connector/src/app/shared/query-service.ts
+++ b/query-connector/src/app/shared/query-service.ts
@@ -10,6 +10,7 @@ import { getSavedQueryByName } from "./database-service";
 import type { QueryDataColumn } from "../(pages)/queryBuilding/utils";
 import { getFhirServerConfigs } from "../backend/dbServices/fhir-servers";
 import { DibbsValueSet } from "../models/entities/valuesets";
+import { auditable } from "../auditLogs/decorator";
 import FHIRClient from "./fhirClient";
 import { FhirServerConfig } from "../models/entities/fhir-servers";
 
@@ -234,6 +235,7 @@ class QueryService {
    * @param includeImmunization Whether to include immunization in the query execution
    * @returns A promise for an updated query response.
    */
+  @auditable
   static async patientRecordsQuery(
     request: PatientRecordsRequest,
     valueSetOverrides?: DibbsValueSet[],
@@ -318,6 +320,7 @@ class QueryService {
    * fhir client info.
    * @returns - The response body from the FHIR server.
    */
+  @auditable
   static async patientDiscoveryQuery(
     request: PatientDiscoveryRequest,
   ): Promise<QueryResponse["Patient"]> {
@@ -472,7 +475,6 @@ class QueryService {
         }
       }
     }
-
     return resourceArray;
   }
 }
diff --git a/query-connector/src/app/tests/integration/audit-logging.test.ts b/query-connector/src/app/tests/integration/audit-logging.test.ts
index 02ef0c9ca..81b2a1e5b 100644
--- a/query-connector/src/app/tests/integration/audit-logging.test.ts
+++ b/query-connector/src/app/tests/integration/audit-logging.test.ts
@@ -1,19 +1,27 @@
-import {
-  addUserIfNotExists,
-  updateUserRole,
-} from "@/app/backend/user-management";
 import { auth } from "@/auth";
 import { Bundle, BundleEntry, Patient } from "fhir/r4";
-import { getDbClient } from "@/app/backend/dbClient";
-import { POST } from "@/app/api/query/route";
 import { readJsonFile } from "../shared_utils/readJsonFile";
-import { USE_CASE_DETAILS } from "@/app/shared/constants";
-import { UserRole } from "@/app/models/entities/users";
-import { createNextRequest } from "./api-query.test";
+import { hyperUnluckyPatient, USE_CASE_DETAILS } from "@/app/shared/constants";
+import {
+  patientDiscoveryQuery,
+  PatientDiscoveryRequest,
+  patientRecordsQuery,
+  PatientRecordsRequest,
+} from "@/app/shared/query-service";
+import { getDbClient } from "@/app/backend/dbClient";
+import { AUDIT_LOG_MAX_RETRIES, auditable } from "@/app/auditLogs/decorator";
+import * as DecoratorUtils from "@/app/auditLogs/lib";
 import { suppressConsoleLogs } from "./fixtures";
 
 const dbClient = getDbClient();
 
+jest.mock("@/app/auditLogs/lib", () => {
+  return {
+    __esModule: true,
+    ...jest.requireActual("@/app/auditLogs/lib"),
+  };
+});
+
 jest.mock("@/app/utils/auth", () => {
   return {
     superAdminAccessCheck: jest.fn(() => Promise.resolve(true)),
@@ -39,39 +47,93 @@ if (!PatientResource || PatientResource.resourceType !== "Patient") {
   throw new Error("Invalid Patient resource in the test bundle.");
 }
 
-const SYPHILIS_QUERY_ID = USE_CASE_DETAILS.syphilis.id;
-
-describe("Audit Logging Integration Tests", () => {
-  beforeAll(async () => {
+describe("audit log", () => {
+  beforeAll(() => {
     suppressConsoleLogs();
-    await dbClient.query("BEGIN");
   });
 
-  afterAll(async () => {
-    await dbClient.query("ROLLBACK");
+  afterAll(() => {
+    jest.resetAllMocks();
+  });
+
+  it("patient discovery query should generate an audit entry", async () => {
+    const auditQuery = "SELECT * FROM audit_logs;";
+    const auditRows = await dbClient.query(auditQuery);
+
+    const request: PatientDiscoveryRequest = {
+      fhir_server: "Aidbox",
+      first_name: hyperUnluckyPatient.FirstName,
+      last_name: hyperUnluckyPatient.LastName,
+      dob: hyperUnluckyPatient.DOB,
+      mrn: hyperUnluckyPatient.MRN,
+      phone: hyperUnluckyPatient.Phone,
+    };
+    await patientDiscoveryQuery(request);
+
+    const newAuditRows = await dbClient.query(auditQuery);
+
+    const addedVal = newAuditRows.rows.filter((item) => {
+      if (!auditRows.rows.map((v) => v.id).includes(item.id)) {
+        return item;
+      }
+    });
+
+    expect(addedVal[0]?.action_type).toBe("patientDiscoveryQuery");
+    expect(addedVal[0]?.audit_message).toStrictEqual({
+      request: JSON.stringify(request),
+    });
   });
+  it("patient records query should generate an audit entry", async () => {
+    const auditQuery = "SELECT * FROM audit_logs;";
+    const auditRows = await dbClient.query(auditQuery);
 
-  test("should find user and know their role", async () => {
-    const result = await addUserIfNotExists(TEST_USER);
-    const createdUserId = result.id;
+    const request: PatientRecordsRequest = {
+      fhir_server: "Aidbox",
+      patient_id: hyperUnluckyPatient.Id,
+      query_name: USE_CASE_DETAILS.gonorrhea.queryName,
+    };
+    await patientRecordsQuery(request);
 
-    const newresult = await updateUserRole(createdUserId, UserRole.SUPER_ADMIN);
-    expect(newresult.items![0]).toHaveProperty("qc_role", UserRole.SUPER_ADMIN);
+    const newAuditRows = await dbClient.query(auditQuery);
+
+    const addedVal = newAuditRows.rows.filter((item) => {
+      if (!auditRows.rows.map((v) => v.id).includes(item.id)) {
+        return item;
+      }
+    });
+
+    expect(addedVal[0]?.action_type).toBe("patientRecordsQuery");
+    expect(addedVal[0]?.audit_message).toStrictEqual({
+      request: JSON.stringify(request),
+    });
   });
-});
 
-describe("Audit Log of POST Query to FHIR Server", () => {
-  it("should create an audit log if query is successful", async () => {
-    const request = createNextRequest(
-      PatientResource,
-      new URLSearchParams(
-        `id=${SYPHILIS_QUERY_ID}&fhir_server=HELIOS Meld: Direct`,
-      ),
+  it("an audited function should  retries successfully", async () => {
+    const auditGenerationSpy = jest.spyOn(
+      DecoratorUtils,
+      "generateAuditValues",
     );
-    const response = await POST(request);
-    const body = await response.json();
-    expect(body.resourceType).toBe("Bundle");
-    //TO DO = once there's an actual DB write, we can check that the auditable query shows up as it should.
-    //Right now, it's generating console logs in the terminal, which doesn't make sense to test.
-  });
+
+    const querySpy = jest.spyOn(dbClient, "query");
+
+    // generate errors within the query write AUDIT_LOG_MAX_RETRIES - 1 times
+    for (let i = 0; i < AUDIT_LOG_MAX_RETRIES; i++) {
+      querySpy.mockImplementationOnce(() => {
+        throw new Error("test error");
+      });
+    }
+
+    class MockClass {
+      @auditable
+      testFunction() {
+        return;
+      }
+    }
+
+    const testObj = new MockClass();
+    testObj.testFunction();
+
+    await new Promise((r) => setTimeout(r, 6000));
+    expect(auditGenerationSpy).toHaveBeenCalledTimes(AUDIT_LOG_MAX_RETRIES);
+  }, 10000);
 });


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.

<details>
<summary>Files not reviewed (1)</summary>

* **query-connector/flyway/sql/V10_01__add_audit_tables.sql**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**query-connector/src/app/tests/integration/audit-logging.test.ts:58**
* [nitpick] Consider refactoring the filter callback to return a boolean directly (for example, using .some instead of .map and .includes) for improved clarity.
```
if (!auditRows.rows.map((v) => v.id).includes(item.id)) {
```
**query-connector/src/app/auditLogs/decorator.ts:66**
* [nitpick] Consider updating this error message to accurately reflect that the method may be synchronous rather than hardcoding 'Async method'.
```
console.error(`Async method ${key} threw error`, error);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T18:10:46Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 7 out of 8 changed files in this pull request and generated no comments.

<details>
<summary>Files not reviewed (1)</summary>

* **query-connector/flyway/sql/V10_01__add_audit_tables.sql**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**query-connector/src/app/auditLogs/decorator.ts:20**
* The regex match on the function signature may return null if the function has no parameters; consider adding a null-check before accessing the match result to avoid potential runtime errors.
```
const argLabels = target[key].toString().match(/\(([^)]*)\)/)[1].split(", ");
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T19:52:24Z
user: login: nickclyde
body: Just gave it a go locally, looking good so far! Great work Bob!!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T21:18:46Z

Review comments:
diff_hunk: @@ -0,0 +1,138 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { auth } from "@/auth";
+import { getDbClient } from "../backend/dbClient";
+
+/**
+ * Decorator that adds audit log write logic to an annotated function
+ * @param target - reference back to the Object prototype of the class
+ * @param key - name of method
+ * @param descriptor - metadata about the functin
+ * @returns The result of the function, with the args / result logged appropriately
+ */
+export function auditable(
+  target: any,
+  key: string,
+  descriptor: PropertyDescriptor,
+) {
+  const dbConnection = getDbClient();
+  const method = descriptor.value;
+  const argLabels = target[key]
+    .toString()
+    // match everything between the first "(" and first ")" and use [1] to get
+    // everything in the first capture group (ie exclude the lead / trail parens)
+    .match(/\(([^)]*)\)/)[1]
+    .split(", ");
+
+  const writeToAuditTable = async (args: any[]) => {
+    const query = `INSERT INTO 
+        audit_logs (author, action_type, audit_checksum, audit_message) 
+        VALUES ($1, $2, $3, $4)
+        RETURNING id, action_type, audit_checksum`;
+
+    generateAuditValues(key, argLabels, args)
path: query-connector/src/app/auditLogs/decorator.ts
user: login: Copilot
body: Consider adding error handling (e.g., a catch clause) to the promise chain in writeToAuditTable to manage potential failures during audit log insertion.
created_at: 2025-04-04T18:10:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,71 @@
+import { auth } from "@/auth";
+
+// these functions are exported from another file into the auditable file
+//  to allow us to spy on them when calling @auditable functions
+// Relevant thread: https://stackoverflow.com/questions/61111423/expected-spyon-function-to-be-called-jest
+
+/**
+ * Helper function that formats method input into the values needed by the audit
+ * log table
+ * @param methodName - name of the method to audit
+ * @param argLabels - name of the params
+ * @param args - value of the arguments
path: query-connector/src/app/auditLogs/lib.ts
user: login: nickclyde
body: Gotta love some good metaprogramming 😎 
created_at: 2025-04-04T21:13:04Z
author_association: MEMBER

PR comments:
user: login: linear[bot]
created_at: 2025-04-04T18:09:33Z
body: <p><a href="https://linear.app/skylight-cdc/issue/QUE-179/backend-create-audit-logs-table">QUE-179 Backend: Create audit_logs table</a></p>
<p><a href="https://linear.app/skylight-cdc/issue/QUE-192/methods-calling-fetch-to-post-against-a-fhir-server-are-marked-as">QUE-192 Methods calling fetch to POST against a FHIR server are marked as `@auditable`</a></p>
author_association: NONE
user: login: codecov-commenter
created_at: 2025-04-04T18:11:53Z
body: ## [Codecov](https://app.codecov.io/gh/CDCgov/dibbs-query-connector/pull/490?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CDCgov) Report
:x: Patch coverage is `94.11765%` with `4 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 61.70%. Comparing base ([`6626dcd`](https://app.codecov.io/gh/CDCgov/dibbs-query-connector/commit/6626dcd8e878a958165074d87c6173e817f38399?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CDCgov)) to head ([`26dc81e`](https://app.codecov.io/gh/CDCgov/dibbs-query-connector/commit/26dc81e359196e2624d3389cca276050c07ec195?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CDCgov)).
:warning: Report is 198 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/CDCgov/dibbs-query-connector/pull/490?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CDCgov) | Patch % | Lines |
|---|---|---|
| [query-connector/src/app/auditLogs/decorator.ts](https://app.codecov.io/gh/CDCgov/dibbs-query-connector/pull/490?src=pr&el=tree&filepath=query-connector%2Fsrc%2Fapp%2FauditLogs%2Fdecorator.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CDCgov#diff-cXVlcnktY29ubmVjdG9yL3NyYy9hcHAvYXVkaXRMb2dzL2RlY29yYXRvci50cw==) | 90.69% | [4 Missing :warning: ](https://app.codecov.io/gh/CDCgov/dibbs-query-connector/pull/490?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CDCgov) |

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##             main     #490      +/-   ##
==========================================
+ Coverage   60.50%   61.70%   +1.20%     
==========================================
  Files          71       73       +2     
  Lines        3390     3431      +41     
  Branches      744      747       +3     
==========================================
+ Hits         2051     2117      +66     
+ Misses       1265     1242      -23     
+ Partials       74       72       -2     
```
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/CDCgov/dibbs-query-connector/pull/490?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CDCgov).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CDCgov).
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE