From c2f06af5cbc128c046f513bafb71d6ab5f1cb2a4 Mon Sep 17 00:00:00 2001 From: Rob Mitchell <40571882+robertandremitchell@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:11:57 -0500 Subject: [PATCH 1/2] Initialize UserGroup insert/update/delete work (#382) Co-authored-by: fzhao99 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- query-connector/jest.config.js | 9 +- query-connector/package-lock.json | 102 +++++----- .../src/app/backend/user-management.ts | 184 +++++++++++++++++- .../tests/integration/user_management.test.ts | 177 +++++++++++++++++ query-connector/src/app/utils/auth.ts | 21 +- 5 files changed, 437 insertions(+), 56 deletions(-) create mode 100644 query-connector/src/app/tests/integration/user_management.test.ts diff --git a/query-connector/jest.config.js b/query-connector/jest.config.js index ec61e82db..0802fdc2b 100644 --- a/query-connector/jest.config.js +++ b/query-connector/jest.config.js @@ -14,7 +14,14 @@ const customJestConfig = { setupFilesAfterEnv: ["/jest.setup.ts"], testEnvironment: "jest-fixed-jsdom", modulePathIgnorePatterns: ["/.next/"], + moduleNameMapper: { "^@/(.*)$": "/src/$1" }, }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async -module.exports = createJestConfig(customJestConfig); +/** + * Creates and exports Jest configuration. + * @returns The Jest configuration object. + */ +module.exports = async () => ({ + ...(await createJestConfig(customJestConfig)()), +}); diff --git a/query-connector/package-lock.json b/query-connector/package-lock.json index b037de862..33ab9364f 100644 --- a/query-connector/package-lock.json +++ b/query-connector/package-lock.json @@ -1106,9 +1106,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1145,13 +1145,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", "dev": true, "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -1161,12 +1161,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.25.9", + "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -1207,9 +1207,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1256,12 +1256,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", "dev": true, "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.9" }, "bin": { "parser": "bin/babel-parser.js" @@ -1505,30 +1505,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1537,9 +1537,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -6149,9 +6149,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -6168,9 +6168,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { @@ -6296,9 +6296,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001683", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001683.tgz", - "integrity": "sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==", + "version": "1.0.30001700", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", + "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "funding": [ { "type": "opencollective", @@ -7370,9 +7370,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.64", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz", - "integrity": "sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==", + "version": "1.5.102", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", + "integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==", "dev": true }, "node_modules/element-closest": { @@ -11519,9 +11519,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, "node_modules/normalize-path": { diff --git a/query-connector/src/app/backend/user-management.ts b/query-connector/src/app/backend/user-management.ts index 43849e68c..49c7398ad 100644 --- a/query-connector/src/app/backend/user-management.ts +++ b/query-connector/src/app/backend/user-management.ts @@ -1,8 +1,11 @@ "use server"; - -import { RoleTypeValues, User } from "../models/entities/user-management"; +import { + RoleTypeValues, + User, + UserGroup, +} from "../models/entities/user-management"; import { QCResponse } from "../models/responses/collections"; -import { superAdminAccessCheck } from "../utils/auth"; +import { adminAccessCheck, superAdminAccessCheck } from "../utils/auth"; import { getDbClient } from "./dbClient"; const dbClient = getDbClient(); @@ -161,3 +164,178 @@ export async function getUserRole(username: string): Promise { throw error; } } + +/** + * Retrieves all registered user groups in query connector along with member and query counts. + * @returns A list of user groups registered in the query connector. + */ +export async function getUserGroups(): Promise> { + if (!(await adminAccessCheck())) { + throw new Error("Unauthorized"); + } + + try { + const selectAllUserGroupQuery = ` + SELECT + ug.id, + ug.name, + COALESCE(member_count, 0) AS memberSize, + COALESCE(query_count, 0) AS querySize + FROM usergroup ug + LEFT JOIN ( + SELECT usergroup_id, COUNT(*) AS member_count + FROM usergroup_to_users + GROUP BY usergroup_id + ) uu ON ug.id = uu.usergroup_id + LEFT JOIN ( + SELECT usergroup_id, COUNT(*) AS query_count + FROM usergroup_to_query + GROUP BY usergroup_id + ) uq ON ug.id = uq.usergroup_id + ORDER BY ug.name ASC; + `; + + const result = await dbClient.query(selectAllUserGroupQuery); + + return { + totalItems: result.rowCount, + items: result.rows, + } as QCResponse; + } catch (error) { + console.error("Error retrieving user groups:", error); + throw error; + } +} + +/** + * Creates a new user group if it does not already exist. + * @param groupName - The name of the user group to create. + * @returns The created user group or an error message if it already exists. + */ +export async function createUserGroup( + groupName: string, +): Promise { + if (!(await adminAccessCheck())) { + throw new Error("Unauthorized"); + } + + try { + // Check if the group name already exists + const existingGroups = await getUserGroups(); + const groupExists = + existingGroups.items?.some((group) => group.name === groupName) ?? false; + + if (groupExists) { + console.warn(`Group with name '${groupName}' already exists.`); + return `Group '${groupName}' already exists.`; + } + + const createGroupQuery = ` + INSERT INTO usergroup (name) + VALUES ($1) + RETURNING id, name; + `; + + const result = await dbClient.query(createGroupQuery, [groupName]); + + return { + id: result.rows[0].id, + name: result.rows[0].name, + memberSize: 0, + querySize: 0, + }; + } catch (error) { + console.error("Error creating user group:", error); + throw error; + } +} + +/** + * Updates the name of an existing user group. + * @param id - The unique identifier of the user group to update. + * @param newName - The new name to assign to the user group. + * @returns The updated user group or an error if the update fails. + */ +export async function updateUserGroup( + id: string, + newName: string, +): Promise { + if (!(await adminAccessCheck())) { + throw new Error("Unauthorized"); + } + + try { + // Check if the new name already exists + const existingGroups = await getUserGroups(); + const groupExists = + existingGroups.items?.some((group) => group.name === newName) ?? false; + + if (groupExists) { + console.warn(`Group with name '${newName}' already exists.`); + return `Group '${newName}' already exists.`; + } + + const updateGroupQuery = ` + UPDATE usergroup + SET name = $1 + WHERE id = $2 + RETURNING id, name; + `; + + const result = await dbClient.query(updateGroupQuery, [newName, id]); + + if (result.rows.length === 0) { + throw new Error(`User group with ID '${id}' not found.`); + } + + // Get updated memberSize and querySize from getUserGroup + const userGroups = await getUserGroups(); + const updatedGroup = + userGroups?.items?.find((group) => group.id === id) ?? null; + + return { + id: result.rows[0].id, + name: result.rows[0].name, + memberSize: updatedGroup?.memberSize ?? 0, + querySize: updatedGroup?.querySize ?? 0, + }; + } catch (error) { + console.error("Error updating user group:", error); + throw error; + } +} + +/** + * Deletes a user group by its unique identifier. + * @param id - The unique identifier of the user group to delete. + * @returns The deleted user group or an error if the deletion fails. + */ +export async function deleteUserGroup(id: string): Promise { + if (!(await adminAccessCheck())) { + throw new Error("Unauthorized"); + } + + try { + const deleteGroupQuery = ` + DELETE FROM usergroup + WHERE id = $1 + RETURNING id, name; + `; + + const result = await dbClient.query(deleteGroupQuery, [id]); + + if (result.rows.length === 0) { + throw new Error(`User group with ID '${id}' not found.`); + } + + return { + id: result.rows[0].id, + name: result.rows[0].name, + memberSize: 0, + querySize: 0, + }; + } catch (error) { + console.error("Error deleting user group:", error); + throw error; + } +} diff --git a/query-connector/src/app/tests/integration/user_management.test.ts b/query-connector/src/app/tests/integration/user_management.test.ts new file mode 100644 index 000000000..dbd9d4409 --- /dev/null +++ b/query-connector/src/app/tests/integration/user_management.test.ts @@ -0,0 +1,177 @@ +import { + createUserGroup, + getUserGroups, + updateUserGroup, + deleteUserGroup, + addUserIfNotExists, + updateUserRole, + getUsers, + getUserRole, +} from "@/app/backend/user-management"; +import { getDbClient } from "@/app/backend/dbClient"; +import { auth } from "@/auth"; +import { RoleTypeValues } from "@/app/models/entities/user-management"; + +const dbClient = getDbClient(); +jest.mock("@/auth", () => ({ + auth: jest.fn(), +})); + +jest.mock("@/app/utils/auth", () => { + return { + superAdminAccessCheck: jest.fn(() => Promise.resolve(true)), + adminAccessCheck: jest.fn(() => Promise.resolve(true)), + }; +}); + +const TEST_USER = { + id: "13e1efb2-5889-4157-8f34-78d7f02dbf84", + username: "Ima User", + email: "ima.user@example.com", + firstName: "Ima", + lastName: "User", +}; + +(auth as jest.Mock).mockResolvedValue(TEST_USER); + +describe("User Management Integration Tests", () => { + let createdUserId: string; + + beforeAll(async () => { + await dbClient.query("BEGIN"); + }); + + afterAll(async () => { + await dbClient.query("ROLLBACK"); + }); + + /** + * Tests adding a new user if they do not already exist. + */ + test("should add a user if they do not exist", async () => { + const result = await addUserIfNotExists(TEST_USER); + + expect(result).toHaveProperty("id"); + expect(result).toHaveProperty("username", TEST_USER.username); + expect(result).toHaveProperty("qc_role", RoleTypeValues.Standard); + createdUserId = result.id; + }); + + /** + * Tests updating the role of an existing user. + */ + test("should update a user's role", async () => { + const result = await updateUserRole( + createdUserId, + RoleTypeValues.SuperAdmin, + ); + expect(result.items).not.toBeNull(); + expect(result.items![0]).toHaveProperty( + "qc_role", + RoleTypeValues.SuperAdmin, + ); + }); + + /** + * Tests retrieving all registered users. + */ + test("should retrieve all users", async () => { + const result = await getUsers(); + expect(result.totalItems).toBeGreaterThan(0); + }); + + /** + * Tests retrieving a user's role by username. + */ + test("should retrieve user role by username", async () => { + const result = await getUserRole(TEST_USER.username); + expect(result).toBe(RoleTypeValues.SuperAdmin); + }); +}); + +describe("User Group Integration Tests", () => { + let testGroupId: string; + + beforeAll(async () => { + await dbClient.query("BEGIN"); + }); + + afterAll(async () => { + await dbClient.query("ROLLBACK"); + }); + + /** + * Tests creating a new user group. + */ + test("should create a new user group", async () => { + const groupName = "Test Group"; + const result = await createUserGroup(groupName); + + expect(result).toHaveProperty("id"); + expect(result).toHaveProperty("name", groupName); + expect(result).toHaveProperty("memberSize", 0); + expect(result).toHaveProperty("querySize", 0); + + if (typeof result === "string") { + throw new Error(`Failed to create test group: ${result}`); + } + + expect(result).toHaveProperty("id"); + expect(result).toHaveProperty("name", groupName); + testGroupId = result.id; + }); + + /** + * Tests preventing duplicate user group creation. + */ + test("should not create duplicate user group", async () => { + const result = await createUserGroup("Test Group"); + expect(typeof result).toBe("string"); // It should return a string error + expect(result).toBe(`Group 'Test Group' already exists.`); + }); + + /** + * Tests retrieving all user groups. + */ + test("should retrieve user groups", async () => { + const result = await getUserGroups(); + expect(result.items).not.toBeNull(); + expect(result.items!.length).toBeGreaterThanOrEqual(1); + }); + + /** + * Tests updating a user group's name. + */ + test("should update a user group name", async () => { + const result = await updateUserGroup(testGroupId, "Updated Group Name"); + expect(result).toHaveProperty("id", testGroupId); + expect(result).toHaveProperty("name", "Updated Group Name"); + }); + + /** + * Tests deleting a user group. + */ + test("should delete a user group", async () => { + const result = await deleteUserGroup(testGroupId); + expect(result).toHaveProperty("id", testGroupId); + }); + + const NON_EXISTENT_UUID = "00000000-0000-0000-0000-000000000000"; + /** + * Tests preventing updates to a non-existent user group. + */ + test("should not update a non-existent user group", async () => { + await expect( + updateUserGroup(NON_EXISTENT_UUID, "New Name"), + ).rejects.toThrow(`User group with ID '${NON_EXISTENT_UUID}' not found.`); + }); + + /** + * Tests preventing deletion of a non-existent user group. + */ + test("should not delete a non-existent user group", async () => { + await expect(deleteUserGroup(NON_EXISTENT_UUID)).rejects.toThrow( + `User group with ID '${NON_EXISTENT_UUID}' not found.`, + ); + }); +}); diff --git a/query-connector/src/app/utils/auth.ts b/query-connector/src/app/utils/auth.ts index 1b8c1e9ab..770cf49b1 100644 --- a/query-connector/src/app/utils/auth.ts +++ b/query-connector/src/app/utils/auth.ts @@ -25,7 +25,6 @@ export function isAuthDisabled(): boolean { */ export async function getLoggedInUser(): Promise { const session = await auth(); - return session ? session.user : undefined; } @@ -47,3 +46,23 @@ export async function superAdminAccessCheck(): Promise { return false; } + +/** + * Performs admin role check + * @returns true if there is an session and the user has an admin or super-admin role + */ +export async function adminAccessCheck(): Promise { + const user = await getLoggedInUser(); + + if ( + isAuthDisabled() || + (user && + [RoleTypeValues.Admin, RoleTypeValues.SuperAdmin].includes( + (await getUserRole(user.username as string)) as RoleTypeValues, + )) + ) { + return true; + } + + return false; +} From c2ae154e936e6f3d3e2f5f9c8d25a3a7556ba69a Mon Sep 17 00:00:00 2001 From: fzhao99 Date: Fri, 21 Feb 2025 09:51:15 -0500 Subject: [PATCH 2/2] seed api documentation (#383) Co-authored-by: DanPaseltiner Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- query-connector/.eslintrc.json | 11 +- query-connector/package-lock.json | 870 +++++++----------- query-connector/package.json | 3 + .../src/app/(pages)/apiDocs/openapi.json | 297 ++++++ .../src/app/(pages)/apiDocs/openapi.yaml | 239 +++++ .../src/app/(pages)/apiDocs/route.ts | 9 + .../src/app/(pages)/fhirServers/page.tsx | 2 +- .../buildFromTemplates/BuildFromTemplates.tsx | 2 +- .../src/app/(pages)/userManagement/page.tsx | 4 +- .../app/api/query/error-handling-service.ts | 24 + .../query/{parsing-service.ts => parsers.ts} | 51 +- query-connector/src/app/api/query/route.ts | 242 +++-- query-connector/src/app/api/route.ts | 9 + query-connector/src/app/shared/constants.ts | 4 + .../src/app/shared/database-service.ts | 2 +- .../app/tests/integration/api-query.test.ts | 72 +- .../src/app/tests/integration/fixtures.ts | 8 + query-connector/src/auth.ts | 4 +- 18 files changed, 1212 insertions(+), 641 deletions(-) create mode 100644 query-connector/src/app/(pages)/apiDocs/openapi.json create mode 100644 query-connector/src/app/(pages)/apiDocs/openapi.yaml create mode 100644 query-connector/src/app/(pages)/apiDocs/route.ts rename query-connector/src/app/api/query/{parsing-service.ts => parsers.ts} (63%) create mode 100644 query-connector/src/app/api/route.ts create mode 100644 query-connector/src/app/tests/integration/fixtures.ts diff --git a/query-connector/.eslintrc.json b/query-connector/.eslintrc.json index 08dbbc963..cd58d6144 100644 --- a/query-connector/.eslintrc.json +++ b/query-connector/.eslintrc.json @@ -1,10 +1,6 @@ { "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "unused-imports", - "jsdoc" - ], + "plugins": ["@typescript-eslint", "unused-imports", "jsdoc"], "extends": [ "plugin:@next/next/recommended", "plugin:jsdoc/recommended-typescript-error", @@ -44,10 +40,7 @@ }, "overrides": [ { - "files": [ - "*.test*", - "**/tests/**/*" - ], + "files": ["*.test*", "**/tests/**/*"], "rules": { "jsdoc/require-jsdoc": "off" } diff --git a/query-connector/package-lock.json b/query-connector/package-lock.json index 33ab9364f..584c0c09e 100644 --- a/query-connector/package-lock.json +++ b/query-connector/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.1", "dependencies": { "@aws-sdk/client-s3": "^3.515.0", + "@scalar/nextjs-api-reference": "^0.4.106", + "@scalar/nextjs-openapi": "^0.0.36", "@trussworks/react-uswds": "^9.1.0", "@uswds/uswds": "^3.8.2", "base-64": "^1.0.0", @@ -22,6 +24,7 @@ "next-auth": "^5.0.0-beta.25", "next-nprogress-bar": "^2.4.4", "node-fetch": "^2.7.0", + "node-hl7-client": "^3.0.0", "pg": "^8.12.0", "pg-promise": "^11.5.4", "react": "^18", @@ -1561,15 +1564,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@es-joy/jsdoccomment": { "version": "0.46.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", @@ -1764,27 +1758,6 @@ "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", @@ -1800,291 +1773,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2894,7 +2582,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2907,7 +2594,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -2916,7 +2602,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2979,26 +2664,6 @@ "@parcel/watcher-win32-x64": "2.5.0" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", @@ -3019,305 +2684,412 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", - "cpu": [ - "x64" - ], + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" + "bin": { + "detect-libc": "bin/detect-libc.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=0.10" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", - "cpu": [ - "x64" - ], + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">= 10.0.0" + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@playwright/test": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", + "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "devOptional": true, + "dependencies": { + "playwright": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "devOptional": true, + "dependencies": { + "playwright-core": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "dev": true + }, + "node_modules/@scalar/nextjs-api-reference": { + "version": "0.4.106", + "resolved": "https://registry.npmjs.org/@scalar/nextjs-api-reference/-/nextjs-api-reference-0.4.106.tgz", + "integrity": "sha512-lhdA/Vjst1vxaX0NJnYb42JNdyJpN/omEVaEvRFsnBDi3b7UQlZAte8ceUetpOtihhtZ57DPC1mxZmr7pmbesQ==", + "dependencies": { + "@scalar/types": "0.0.25" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "next": "^14.0.0", + "react": "^18.0.0" + } + }, + "node_modules/@scalar/nextjs-openapi": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@scalar/nextjs-openapi/-/nextjs-openapi-0.0.36.tgz", + "integrity": "sha512-fK9s7JOFI3odO5+F9dS54Q3k2yBf4/hXDuA+JWPSH90toEUqISYum2wJKiKdw8Teua1iMhNI83VbPgoVENGXQw==", + "dependencies": { + "@scalar/nextjs-api-reference": "0.5.9", + "@scalar/ts-to-openapi": "0.0.5", + "@scalar/types": "0.0.33", + "fast-glob": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/nextjs-openapi/node_modules/@next/env": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.7.tgz", + "integrity": "sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==", + "peer": true + }, + "node_modules/@scalar/nextjs-openapi/node_modules/@next/swc-darwin-arm64": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.7.tgz", + "integrity": "sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==", "cpu": [ - "arm" + "arm64" ], - "dev": true, "optional": true, "os": [ - "linux" + "darwin" ], + "peer": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "node_modules/@scalar/nextjs-openapi/node_modules/@next/swc-darwin-x64": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.7.tgz", + "integrity": "sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==", "cpu": [ - "arm" + "x64" ], - "dev": true, "optional": true, "os": [ - "linux" + "darwin" ], + "peer": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "node_modules/@scalar/nextjs-openapi/node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.7.tgz", + "integrity": "sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "node_modules/@scalar/nextjs-openapi/node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.7.tgz", + "integrity": "sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "node_modules/@scalar/nextjs-openapi/node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.7.tgz", + "integrity": "sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "node_modules/@scalar/nextjs-openapi/node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.7.tgz", + "integrity": "sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "node_modules/@scalar/nextjs-openapi/node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.7.tgz", + "integrity": "sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" ], + "peer": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "node_modules/@scalar/nextjs-openapi/node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz", + "integrity": "sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==", "cpu": [ - "ia32" + "x64" ], - "dev": true, "optional": true, "os": [ "win32" ], + "peer": true, "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], + "node_modules/@scalar/nextjs-openapi/node_modules/@scalar/nextjs-api-reference": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@scalar/nextjs-api-reference/-/nextjs-api-reference-0.5.9.tgz", + "integrity": "sha512-sCMJWYkNWhTppfZUeWxnWVEv4DgsTT87uD2y+bywBVeK8ZFVsK/Nbpl3pFCkcGZ/eZoiPECN9IRhiU3kgM3K6A==", + "dependencies": { + "@scalar/types": "0.0.33" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "next": "^15.0.0", + "react": "^19.0.0" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, + "node_modules/@scalar/nextjs-openapi/node_modules/@scalar/openapi-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.7.tgz", + "integrity": "sha512-oOTG3JQifg55U3DhKB7WdNIxFnJzbPJe7rqdyWdio977l8IkxQTVmObftJhdNIMvhV2K+1f/bDoMQGu6yTaD0A==", "engines": { - "node": ">=0.10" + "node": ">=18" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, + "node_modules/@scalar/nextjs-openapi/node_modules/@scalar/types": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.0.33.tgz", + "integrity": "sha512-4mQYkQJO0HHaoFd8Z+vSdQAvYcCJ2bRLN9ewE+GneB8kvoLG/oM3ynroqzGQdoytH8BmhnJwD3aEUagfbK2x5g==", + "dependencies": { + "@scalar/openapi-types": "0.1.7", + "@unhead/schema": "^1.11.11" + }, "engines": { - "node": ">=14" + "node": ">=18" } }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, + "node_modules/@scalar/nextjs-openapi/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@scalar/nextjs-openapi/node_modules/next": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.7.tgz", + "integrity": "sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==", + "peer": true, + "dependencies": { + "@next/env": "15.1.7", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, - "funding": { - "url": "https://opencollective.com/unts" + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.1.7", + "@next/swc-darwin-x64": "15.1.7", + "@next/swc-linux-arm64-gnu": "15.1.7", + "@next/swc-linux-arm64-musl": "15.1.7", + "@next/swc-linux-x64-gnu": "15.1.7", + "@next/swc-linux-x64-musl": "15.1.7", + "@next/swc-win32-arm64-msvc": "15.1.7", + "@next/swc-win32-x64-msvc": "15.1.7", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } } }, - "node_modules/@playwright/test": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", - "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", - "devOptional": true, + "node_modules/@scalar/nextjs-openapi/node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@scalar/nextjs-openapi/node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "peer": true, "dependencies": { - "playwright": "1.49.0" + "client-only": "0.0.1" }, - "bin": { - "playwright": "cli.js" + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.5.tgz", + "integrity": "sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==", "engines": { "node": ">=18" } }, - "node_modules/@playwright/test/node_modules/playwright": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", - "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", - "devOptional": true, + "node_modules/@scalar/ts-to-openapi": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@scalar/ts-to-openapi/-/ts-to-openapi-0.0.5.tgz", + "integrity": "sha512-l/CpgVc2abP5ibwgVEq+YcaOpsoGUo2w9jumGveV9kCk0vsY5UHuLHQayaA2OlVgTnljPbg2uhKhMOSWQrzqbw==", "dependencies": { - "playwright-core": "1.49.0" - }, - "bin": { - "playwright": "cli.js" + "openapi-types": "^12.1.3" }, "engines": { "node": ">=18" }, - "optionalDependencies": { - "fsevents": "2.3.2" + "peerDependencies": { + "typescript": "^5.6.0" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", - "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", - "dev": true + "node_modules/@scalar/types": { + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.0.25.tgz", + "integrity": "sha512-sGnOFnfiSn4o23rklU/jrg81hO+630bsFIdHg8MZ/w2Nc6IoUwARA2hbe4d4Fg+D0KBu40Tan/L+WAYDXkTJQg==", + "dependencies": { + "@scalar/openapi-types": "0.1.5", + "@unhead/schema": "^1.11.11" + }, + "engines": { + "node": ">=18" + } }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -5249,6 +5021,18 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "node_modules/@unhead/schema": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.19.tgz", + "integrity": "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg==", + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, "node_modules/@uswds/uswds": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.10.0.tgz", @@ -6140,7 +5924,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -8365,7 +8148,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -8420,7 +8202,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -8485,7 +8266,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -8804,7 +8584,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -8979,6 +8758,11 @@ "resolved": "https://registry.npmjs.org/highlight-words-core/-/highlight-words-core-1.2.3.tgz", "integrity": "sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==" }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" + }, "node_modules/html-dom-parser": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.10.tgz", @@ -9378,7 +9162,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9443,7 +9226,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -9487,7 +9269,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -11217,7 +10998,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -11235,7 +11015,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -11512,6 +11291,14 @@ } } }, + "node_modules/node-hl7-client": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-hl7-client/-/node-hl7-client-3.0.0.tgz", + "integrity": "sha512-loMZ+X6yvGAoFCeVeFbsx7bmPTJGc3DUZ3nefX9mt43WjdzE0v7IQP2+tJgItme6JKrZYsk7TUsyeXDuFV/7aQ==", + "engines": { + "node": ">=20.15.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -11731,6 +11518,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -12059,7 +11851,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -12426,7 +12217,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -12721,7 +12511,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -12768,7 +12557,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -13746,7 +13534,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -13919,7 +13706,6 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14410,6 +14196,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zhead": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", + "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/query-connector/package.json b/query-connector/package.json index 959693c6b..2622df61a 100644 --- a/query-connector/package.json +++ b/query-connector/package.json @@ -24,6 +24,8 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.515.0", + "@scalar/nextjs-api-reference": "^0.4.106", + "@scalar/nextjs-openapi": "^0.0.36", "@trussworks/react-uswds": "^9.1.0", "@uswds/uswds": "^3.8.2", "base-64": "^1.0.0", @@ -37,6 +39,7 @@ "next-auth": "^5.0.0-beta.25", "next-nprogress-bar": "^2.4.4", "node-fetch": "^2.7.0", + "node-hl7-client": "^3.0.0", "pg": "^8.12.0", "pg-promise": "^11.5.4", "react": "^18", diff --git a/query-connector/src/app/(pages)/apiDocs/openapi.json b/query-connector/src/app/(pages)/apiDocs/openapi.json new file mode 100644 index 000000000..37494f7b1 --- /dev/null +++ b/query-connector/src/app/(pages)/apiDocs/openapi.json @@ -0,0 +1,297 @@ +{ + "openapi": "3.0.3", + "info": { + "version": "1.0.0", + "title": "Query Connector API - OpenAPI 3.0" + }, + "paths": { + "/api": { + "get": { + "description": "Returns the health status of the API", + "responses": { + "200": { + "description": "Health check for Query Connector" + } + } + } + }, + "/api/query": { + "post": { + "description": "A POST endpoint that accepts a FHIR patient resource or an HL7v2 message in the request body to execute a query within the Query Connector", + "parameters": [ + { + "name": "fhir_server", + "in": "query", + "description": "Name of the FHIR server to query", + "required": true, + "schema": { + "type": "string", + "example": "HELIOS Meld: Direct" + } + }, + { + "name": "id", + "in": "query", + "description": "ID of the query to use", + "required": true, + "schema": { + "type": "string", + "example": "cf580d8d-cc7b-4eae-8a0d-96c36f9222e3" + } + }, + { + "name": "message_format", + "in": "query", + "description": "Whether the request body contents are HL7 or FHIR formatted messages", + "schema": { + "type": "string", + "enum": [ + "HL7", + "FHIR" + ], + "example": "FHIR" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "resourceType": "Patient", + "id": "1C", + "meta": { + "versionId": "1", + "lastUpdated": "2024-01-16T15:08:24.000+00:00", + "source": "#Aolu2ZnQyoelPvRd", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" + ] + }, + "text": { + "status": "generated", + "div": "
This is a simple narrative with only plain text
" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2106-3", + "display": "White" + } + }, + { + "url": "text", + "valueString": "Mixed" + } + ] + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2135-2", + "display": "Hispanic or Latino" + } + }, + { + "url": "text", + "valueString": "Hispanic or Latino" + } + ] + } + ], + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR", + "display": "Medical Record Number" + } + ], + "text": "Medical Record Number" + }, + "system": "http://hospital.smarthealthit.org", + "value": "8692756" + } + ], + "active": true, + "name": [ + { + "family": "Shaw", + "given": [ + "Lee", + "A." + ], + "period": { + "start": "1975-12-06", + "end": "2020-01-22" + } + }, + { + "family": "Shaw", + "given": [ + "Lee", + "V." + ], + "suffix": [ + "MD" + ], + "period": { + "start": "2020-01-23" + } + } + ], + "telecom": [ + { + "system": "phone", + "value": "517-425-1398", + "use": "home" + }, + { + "system": "email", + "value": "lee.shaw@email.com" + } + ], + "gender": "male", + "birthDate": "1975-12-06", + "address": [ + { + "line": [ + "49 Meadow St" + ], + "city": "Lansing", + "state": "MI", + "postalCode": "48864", + "country": "US", + "period": { + "start": "2016-12-06", + "end": "2020-07-22" + } + }, + { + "line": [ + "183 Mountain View St" + ], + "city": "Lansing", + "state": "MI", + "postalCode": "48901", + "country": "US", + "period": { + "start": "2020-07-22" + } + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "The FHIR resources returned that match the information configured in the query referenced" + }, + "400": { + "description": "Missing patient identifiers" + }, + "500": { + "description": "Something went wrong :(" + } + } + }, + "get": { + "description": "A GET endpoint that accepts a series of query parameters to execute a query within the Query Connector. At least one of the patient identifier params are required", + "parameters": [ + { + "name": "fhir_server", + "in": "query", + "description": "Name of the FHIR server to query", + "required": true, + "schema": { + "type": "string", + "example": "HELIOS Meld: Direct" + } + }, + { + "name": "id", + "in": "query", + "description": "ID of the query to use", + "required": true, + "schema": { + "type": "string", + "example": "cf580d8d-cc7b-4eae-8a0d-96c36f9222e3" + } + }, + { + "name": "given", + "description": "Patient given name. At least one of the patient identifier params are required", + "in": "query", + "schema": { + "type": "string", + "example": "Lee" + } + }, + { + "name": "family", + "in": "query", + "description": "Patient family name. At least one of the patient identifier params are required", + "schema": { + "type": "string", + "example": "Shaw" + } + }, + { + "name": "dob", + "in": "query", + "description": "Patient date of birth in YYYY-MM-DD format. At least one of the patient identifier params are required", + "schema": { + "type": "string", + "example": "1975-12-06T00:00:00.000Z" + } + }, + { + "name": "mrn", + "in": "query", + "description": "Patient medical record number. At least one of the patient identifier params are required", + "schema": { + "type": "string", + "example": 8692756 + } + }, + { + "name": "phone", + "in": "query", + "description": "Patient phone number. At least one of the patient identifier params are required", + "schema": { + "type": "string", + "example": "517-425-1398" + } + } + ], + "responses": { + "200": { + "description": "The FHIR resources returned that match the information configured in the query referenced" + }, + "400": { + "description": "Missing patient identifiers" + }, + "500": { + "description": "Something went wrong :(" + } + } + } + } + } +} \ No newline at end of file diff --git a/query-connector/src/app/(pages)/apiDocs/openapi.yaml b/query-connector/src/app/(pages)/apiDocs/openapi.yaml new file mode 100644 index 000000000..1978cc680 --- /dev/null +++ b/query-connector/src/app/(pages)/apiDocs/openapi.yaml @@ -0,0 +1,239 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Query Connector API - OpenAPI 3.0 +# TODO: IMPLEMENT THIS PORTION PRIOR TO SHIPPING API +# components: +# securitySchemes: +# OAuth2: +# type: oauth2 +# flows: +# authorizationCode: +# authorizationUrl: https://example.com/oauth/authorize +# tokenUrl: https://example.com/oauth/token +# scopes: +# read: Grants read access +# write: Grants write access +# admin: Grants access to admin operations +paths: + /api: + get: + description: Returns the health status of the API + responses: + 200: + description: Health check for Query Connector + /api/query: + post: + description: A POST endpoint that accepts a FHIR patient resource or an HL7v2 message in the request body to execute a query within the Query Connector + parameters: + - name: fhir_server + in: query + description: Name of the FHIR server to query + required: true + schema: + type: string + example: "HELIOS Meld: Direct" + - name: id + in: query + description: ID of the query to use + required: true + schema: + type: string + example: cf580d8d-cc7b-4eae-8a0d-96c36f9222e3 + - name: message_format + in: query + description: Whether the request body contents are HL7 or FHIR formatted messages + schema: + type: string + enum: [HL7, FHIR] + example: FHIR + requestBody: + required: true + content: + application/json: + schema: + type: object + example: + { + "resourceType": "Patient", + "id": "1C", + "meta": + { + "versionId": "1", + "lastUpdated": "2024-01-16T15:08:24.000+00:00", + "source": "#Aolu2ZnQyoelPvRd", + "profile": + [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient", + ], + }, + "text": + { + "status": "generated", + "div": '
This is a simple narrative with only plain text
', + }, + "extension": + [ + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension": + [ + { + "url": "ombCategory", + "valueCoding": + { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2106-3", + "display": "White", + }, + }, + { "url": "text", "valueString": "Mixed" }, + ], + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + "extension": + [ + { + "url": "ombCategory", + "valueCoding": + { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2135-2", + "display": "Hispanic or Latino", + }, + }, + { + "url": "text", + "valueString": "Hispanic or Latino", + }, + ], + }, + ], + "identifier": + [ + { + "use": "usual", + "type": + { + "coding": + [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR", + "display": "Medical Record Number", + }, + ], + "text": "Medical Record Number", + }, + "system": "http://hospital.smarthealthit.org", + "value": "8692756", + }, + ], + "active": true, + "name": + [ + { + "family": "Shaw", + "given": ["Lee", "A."], + "period": { "start": "1975-12-06", "end": "2020-01-22" }, + }, + { + "family": "Shaw", + "given": ["Lee", "V."], + "suffix": ["MD"], + "period": { "start": "2020-01-23" }, + }, + ], + "telecom": + [ + { + "system": "phone", + "value": "517-425-1398", + "use": "home", + }, + { "system": "email", "value": "lee.shaw@email.com" }, + ], + "gender": "male", + "birthDate": "1975-12-06", + "address": + [ + { + "line": ["49 Meadow St"], + "city": "Lansing", + "state": "MI", + "postalCode": "48864", + "country": "US", + "period": { "start": "2016-12-06", "end": "2020-07-22" }, + }, + { + "line": ["183 Mountain View St"], + "city": "Lansing", + "state": "MI", + "postalCode": "48901", + "country": "US", + "period": { "start": "2020-07-22" }, + }, + ], + } + responses: + 200: + description: The FHIR resources returned that match the information configured in the query referenced + 400: + description: Missing patient identifiers + 500: + description: Something went wrong :( + get: + description: A GET endpoint that accepts a series of query parameters to execute a query within the Query Connector. At least one of the patient identifier params are required + parameters: + - name: fhir_server + in: query + description: Name of the FHIR server to query + required: true + schema: + type: string + example: "HELIOS Meld: Direct" + - name: id + in: query + description: ID of the query to use + required: true + schema: + type: string + example: cf580d8d-cc7b-4eae-8a0d-96c36f9222e3 + - name: given + description: Patient given name. At least one of the patient identifier params are required + in: query + schema: + type: string + example: Lee + - name: family + in: query + description: Patient family name. At least one of the patient identifier params are required + schema: + type: string + example: Shaw + - name: dob + in: query + description: Patient date of birth in YYYY-MM-DD format. At least one of the patient identifier params are required + schema: + type: string + example: 1975-12-06 + - name: mrn + in: query + description: Patient medical record number. At least one of the patient identifier params are required + schema: + type: string + example: 8692756 + - name: phone + in: query + description: Patient phone number. At least one of the patient identifier params are required + schema: + type: string + example: 517-425-1398 + responses: + 200: + description: The FHIR resources returned that match the information configured in the query referenced + 400: + description: Missing patient identifiers + 500: + description: Something went wrong :( diff --git a/query-connector/src/app/(pages)/apiDocs/route.ts b/query-connector/src/app/(pages)/apiDocs/route.ts new file mode 100644 index 000000000..f278fffb7 --- /dev/null +++ b/query-connector/src/app/(pages)/apiDocs/route.ts @@ -0,0 +1,9 @@ +import { ApiReference } from "@scalar/nextjs-api-reference"; +import content from "./openapi.json"; + +const config = { + spec: { + content: content, + }, +}; +export const GET = ApiReference(config); diff --git a/query-connector/src/app/(pages)/fhirServers/page.tsx b/query-connector/src/app/(pages)/fhirServers/page.tsx index b43bf78ec..dad60b2bb 100644 --- a/query-connector/src/app/(pages)/fhirServers/page.tsx +++ b/query-connector/src/app/(pages)/fhirServers/page.tsx @@ -125,7 +125,7 @@ const FhirServers: React.FC = () => { const result = await response.json(); return result; - } catch (error) { + } catch { return { success: false, error: "Failed to test connection. Please try again.", diff --git a/query-connector/src/app/(pages)/queryBuilding/buildFromTemplates/BuildFromTemplates.tsx b/query-connector/src/app/(pages)/queryBuilding/buildFromTemplates/BuildFromTemplates.tsx index caafd4adf..5178c382f 100644 --- a/query-connector/src/app/(pages)/queryBuilding/buildFromTemplates/BuildFromTemplates.tsx +++ b/query-connector/src/app/(pages)/queryBuilding/buildFromTemplates/BuildFromTemplates.tsx @@ -278,7 +278,7 @@ const BuildFromTemplates: React.FC = ({ body: `${queryName} successfully ${statusMessage}`, }); goBack(); - } catch (e) { + } catch { showToastConfirmation({ heading: "Something went wrong", body: `${queryName} wasn't successfully created. Please try again or contact us if the error persists`, diff --git a/query-connector/src/app/(pages)/userManagement/page.tsx b/query-connector/src/app/(pages)/userManagement/page.tsx index 3e7b129d2..e739e1c31 100644 --- a/query-connector/src/app/(pages)/userManagement/page.tsx +++ b/query-connector/src/app/(pages)/userManagement/page.tsx @@ -31,7 +31,7 @@ const UserManagement: React.FC = () => { try { const userList: QCResponse = await getUsers(); setUsers(userList.items ?? []); - } catch (e) { + } catch { showToastConfirmation({ body: "Unable to retrieve users. Please try again.", variant: "error", @@ -53,7 +53,7 @@ const UserManagement: React.FC = () => { showToastConfirmation({ body: "Role successfully updated.", }); - } catch (e) { + } catch { showToastConfirmation({ body: "Unable to update the user role. Please try again.", variant: "error", diff --git a/query-connector/src/app/api/query/error-handling-service.ts b/query-connector/src/app/api/query/error-handling-service.ts index 7d6d30952..e794f8760 100644 --- a/query-connector/src/app/api/query/error-handling-service.ts +++ b/query-connector/src/app/api/query/error-handling-service.ts @@ -1,4 +1,5 @@ import { OperationOutcome } from "fhir/r4"; +import { NextResponse } from "next/server"; /** * Handles a request error by returning an OperationOutcome with a diagnostics message. @@ -21,3 +22,26 @@ export async function handleRequestError( }; return OperationOutcome; } + +/** + * + * @param error - the error to parse / return as needed + * @param status - Status for error, defaults to 500 + * @returns a Next response with information about the outcome and the status + */ +export async function handleAndReturnError(error: unknown, status = 500) { + let diagnostics_message = "An error has occurred"; + + let OperationOutcome; + if (typeof error === "string") { + diagnostics_message = `${diagnostics_message}: ${error}`; + OperationOutcome = await handleRequestError(error as string); + } else { + if (error instanceof Error) { + diagnostics_message = `${diagnostics_message}: ${error}`; + } + OperationOutcome = await handleRequestError(diagnostics_message); + } + console.error(error); + return NextResponse.json(OperationOutcome, { status: status }); +} diff --git a/query-connector/src/app/api/query/parsing-service.ts b/query-connector/src/app/api/query/parsers.ts similarity index 63% rename from query-connector/src/app/api/query/parsing-service.ts rename to query-connector/src/app/api/query/parsers.ts index 526d4d668..93f079eaa 100644 --- a/query-connector/src/app/api/query/parsing-service.ts +++ b/query-connector/src/app/api/query/parsers.ts @@ -1,7 +1,6 @@ -"use server"; - import { Patient } from "fhir/r4"; import { FormatPhoneAsDigits } from "@/app/shared/format-service"; +import { USE_CASES, USE_CASE_DETAILS } from "@/app/shared/constants"; export type PatientIdentifiers = { first_name?: string; @@ -16,9 +15,7 @@ export type PatientIdentifiers = { * @param patient - The patient resource to parse. * @returns An array of patient demographics extracted from the patient resource. */ -export async function parsePatientDemographics( - patient: Patient, -): Promise { +export function parsePatientDemographics(patient: Patient): PatientIdentifiers { const identifiers: PatientIdentifiers = {}; if (patient.name) { @@ -36,7 +33,7 @@ export async function parsePatientDemographics( } // Extract MRNs from patient.identifier - const mrnIdentifiers = await parseMRNs(patient); + const mrnIdentifiers = parseMRNs(patient); // Add 1st value of MRN array to identifiers // TODO: Handle multiple MRNs to query if (mrnIdentifiers && mrnIdentifiers.length > 0) { @@ -44,7 +41,7 @@ export async function parsePatientDemographics( } // Extract phone numbers from patient telecom arrays - let phoneNumbers = await parsePhoneNumbers(patient); + let phoneNumbers = parsePhoneNumbers(patient); if (phoneNumbers) { // Strip formatting so the query service can generate options phoneNumbers = phoneNumbers @@ -65,9 +62,9 @@ export async function parsePatientDemographics( * @param patient - The patient resource to parse. * @returns An array of MRNs extracted from the patient resource. */ -export async function parseMRNs( +export function parseMRNs( patient: Patient, -): Promise<(string | undefined)[] | undefined> { +): (string | undefined)[] | undefined { if (patient.identifier) { const mrnIdentifiers = patient.identifier.filter((id) => id.type?.coding?.some( @@ -87,9 +84,9 @@ export async function parseMRNs( * @param patient A FHIR Patient resource. * @returns A list of phone numbers, or undefined if the patient has no telecom. */ -export async function parsePhoneNumbers( +export function parsePhoneNumbers( patient: Patient, -): Promise<(string | undefined)[] | undefined> { +): (string | undefined)[] | undefined { if (patient.telecom) { const phoneNumbers = patient.telecom.filter( (contactPoint) => @@ -99,3 +96,35 @@ export async function parsePhoneNumbers( return phoneNumbers.map((contactPoint) => contactPoint.value); } } + +/** + * Function to parse out the HL7 message from the requestBody of a POST request + * @param requestText the text to parse / return + * @returns - A parsed HL7 message for further processing + */ +export function parseHL7FromRequestBody(requestText: string) { + let result = requestText; + + // strip the leading { / closing } if they exist + if (requestText[0] === "{" || requestText[requestText.length - 1] === "}") { + const leadingClosingBraceRegex = /\{([\s\S]*)\}/; + const requestMatch = requestText.match(leadingClosingBraceRegex); + if (requestMatch) { + result = requestMatch[1].trim(); + } + } + return result; +} + +/** + * Deprecation method to backfill information from the old demo use cases into + * our new query table + * @param use_case - The old use case names that came out of the demo options + * @returns The ID that maps to the old use case params + */ +export function mapDeprecatedUseCaseToId(use_case: string | null) { + if (use_case === null) return null; + const potentialUseCaseMatch = USE_CASE_DETAILS[use_case as USE_CASES]; + const queryId = potentialUseCaseMatch?.id ?? null; + return queryId; +} diff --git a/query-connector/src/app/api/query/route.ts b/query-connector/src/app/api/query/route.ts index 889ff7c73..7fc3311bf 100644 --- a/query-connector/src/app/api/query/route.ts +++ b/query-connector/src/app/api/query/route.ts @@ -1,4 +1,8 @@ -import { NextResponse, NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; +import { + handleAndReturnError, + handleRequestError, +} from "./error-handling-service"; import { makeFhirQuery, QueryRequest, @@ -6,69 +10,28 @@ import { createBundle, APIQueryResponse, } from "../../shared/query-service"; -import { parsePatientDemographics } from "./parsing-service"; +import { getSavedQueryById } from "@/app/backend/query-building"; import { - INVALID_FHIR_SERVERS, - INVALID_QUERY, RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE, - MISSING_API_QUERY_PARAM, MISSING_PATIENT_IDENTIFIERS, - USE_CASE_DETAILS, - USE_CASES, -} from "../../shared/constants"; - -import { handleRequestError } from "./error-handling-service"; + MISSING_API_QUERY_PARAM, + INVALID_FHIR_SERVERS, + INVALID_QUERY, + INVALID_MESSAGE_FORMAT, +} from "@/app/shared/constants"; import { getFhirServerNames } from "@/app/shared/database-service"; -import { getSavedQueryById } from "@/app/backend/query-building"; - -/** - * Health check for TEFCA Viewer - * @returns Response with status OK. - */ -export async function GET() { - return NextResponse.json({ status: "OK" }, { status: 200 }); -} +import { + mapDeprecatedUseCaseToId, + parseHL7FromRequestBody, + parsePatientDemographics, +} from "./parsers"; +import { Message } from "node-hl7-client"; /** - * Handles a POST request to query a given FHIR server for a given query. The - * id and fhir_server are provided as query parameters in the request URL. The - * request body contains the FHIR patient resource to be queried. - * @param request - The incoming Next.js request object. + * @param request - A GET request as described by the Swagger docs * @returns Response with QueryResponse. */ -export async function POST(request: NextRequest) { - let requestBody; - let PatientIdentifiers; - - try { - requestBody = await request.json(); - - // Check if requestBody is a patient resource - if (requestBody.resourceType !== "Patient") { - const OperationOutcome = await handleRequestError( - RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE, - ); - return NextResponse.json(OperationOutcome); - } - } catch (error: unknown) { - let diagnostics_message = "An error occurred."; - if (error instanceof Error) { - diagnostics_message = `${error.message}`; - } - const OperationOutcome = await handleRequestError(diagnostics_message); - return NextResponse.json(OperationOutcome); - } - - // Parse patient identifiers from requestBody - PatientIdentifiers = await parsePatientDemographics(requestBody); - // Check if PatientIdentifiers is empty or there was an error parsing patient identifiers - if (Object.keys(PatientIdentifiers).length === 0) { - const OperationOutcome = await handleRequestError( - MISSING_PATIENT_IDENTIFIERS, - ); - return NextResponse.json(OperationOutcome); - } - +export async function GET(request: NextRequest) { // Extract id and fhir_server from nextUrl const params = request.nextUrl.searchParams; //deprecated, prefer id @@ -81,32 +44,51 @@ export async function POST(request: NextRequest) { if (!id || !fhir_server) { const OperationOutcome = await handleRequestError(MISSING_API_QUERY_PARAM); - return NextResponse.json(OperationOutcome); + return NextResponse.json(OperationOutcome, { + status: 500, + }); } else if (!Object.values(fhirServers).includes(fhir_server)) { const OperationOutcome = await handleRequestError(INVALID_FHIR_SERVERS); - return NextResponse.json(OperationOutcome); + return NextResponse.json(OperationOutcome, { + status: 500, + }); } const queryResults = await getSavedQueryById(id); if (queryResults === undefined) { const OperationOutcome = await handleRequestError(INVALID_QUERY); - return NextResponse.json(OperationOutcome); + return NextResponse.json(OperationOutcome, { + status: 500, + }); + } + + // try getting params straight from requestBody + const given = params.get("given") ?? ""; + const family = params.get("family") ?? ""; + const dob = params.get("dob") ?? ""; + const mrn = params.get("mrn") ?? ""; + const phone = params.get("phone") ?? ""; + const noParamsDefined = [given, family, dob, mrn, phone].every( + (e) => e === "", + ); + + if (noParamsDefined) { + const OperationOutcome = await handleRequestError( + MISSING_PATIENT_IDENTIFIERS, + ); + return NextResponse.json(OperationOutcome, { status: 400 }); } // Add params & patient identifiers to QueryName const QueryRequest: QueryRequest = { query_name: queryResults.query_name, fhir_server: fhir_server, - ...(PatientIdentifiers.first_name && { - first_name: PatientIdentifiers.first_name, - }), - ...(PatientIdentifiers.last_name && { - last_name: PatientIdentifiers.last_name, - }), - ...(PatientIdentifiers.dob && { dob: PatientIdentifiers.dob }), - ...(PatientIdentifiers.mrn && { mrn: PatientIdentifiers.mrn }), - ...(PatientIdentifiers.phone && { phone: PatientIdentifiers.phone }), + first_name: given, + last_name: family, + dob: dob, + mrn: mrn, + phone: phone, }; const QueryResponse: QueryResponse = await makeFhirQuery(QueryRequest); @@ -114,12 +96,124 @@ export async function POST(request: NextRequest) { // Bundle data const bundle: APIQueryResponse = await createBundle(QueryResponse); - return NextResponse.json(bundle); + return NextResponse.json(bundle, { + status: 200, + }); } -function mapDeprecatedUseCaseToId(use_case: string | null) { - if (use_case === null) return null; - const potentialUseCaseMatch = USE_CASE_DETAILS[use_case as USE_CASES]; - const queryId = potentialUseCaseMatch?.id ?? null; - return queryId; +/** + * @param request A POST request as described by the Swagger docs + * @returns Response with QueryResponse. + */ +export async function POST(request: NextRequest) { + // Extract id and fhir_server from nextUrl + const params = request.nextUrl.searchParams; + //deprecated, prefer id + const use_case_param = params.get("use_case"); + const id_param = params.get("id"); + const fhir_server = params.get("fhir_server"); + const fhirServers = await getFhirServerNames(); + + const id = id_param ? id_param : mapDeprecatedUseCaseToId(use_case_param); + + if (!id || !fhir_server) { + return await handleAndReturnError(MISSING_API_QUERY_PARAM); + } else if (!Object.values(fhirServers).includes(fhir_server)) { + return await handleAndReturnError(INVALID_FHIR_SERVERS); + } + + const queryResults = await getSavedQueryById(id); + if (queryResults === undefined) { + return handleAndReturnError(INVALID_QUERY); + } + + // check message format of body, default to FHIR + const messageFormat = params.get("message_format") ?? "FHIR"; + if (messageFormat !== "FHIR" && messageFormat !== "HL7") { + return await handleAndReturnError(INVALID_MESSAGE_FORMAT); + } + + let QueryRequest: QueryRequest; + if (messageFormat === "HL7") { + try { + let requestText = await request.text(); + + const parsedMessage = new Message({ + text: parseHL7FromRequestBody(requestText), + }); + + const firstName = parsedMessage.get("PID.5.2").toString() ?? ""; + const lastName = parsedMessage.get("PID.5.1").toString() ?? ""; + const dob = parsedMessage.get("PID.7.1").toString() ?? ""; + const mrn = parsedMessage.get("PID.3.1").toString() ?? ""; + const phone = parsedMessage.get("NK1.5.1").toString() ?? ""; + const noPatientIdentifierDefined = [ + firstName, + lastName, + mrn, + phone, + dob, + ].every((e) => e === ""); + console.log(firstName, lastName, mrn, phone, dob); + + if (noPatientIdentifierDefined) { + return await handleAndReturnError(MISSING_PATIENT_IDENTIFIERS, 400); + } + + QueryRequest = { + query_name: queryResults?.query_name, + fhir_server: fhir_server, + first_name: firstName, + last_name: lastName, + dob: dob, + mrn: mrn, + phone: phone, + }; + } catch (error: unknown) { + return await handleAndReturnError(error); + } + } else { + try { + const requestBodyToCheck = await request.json(); + // try extracting patient identifiers out of the request body from a potential + // FHIR message or as raw params + if ( + requestBodyToCheck.resourceType && + requestBodyToCheck.resourceType !== "Patient" + ) { + return await handleAndReturnError( + RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE, + ); + } + + // Parse patient identifiers from a potential FHIR resource + const PatientIdentifiers = parsePatientDemographics(requestBodyToCheck); + + if (Object.keys(PatientIdentifiers).length === 0) { + return await handleAndReturnError(MISSING_PATIENT_IDENTIFIERS, 400); + } + + // Add params & patient identifiers to QueryName + QueryRequest = { + query_name: queryResults.query_name, + fhir_server: fhir_server, + first_name: PatientIdentifiers?.first_name, + last_name: PatientIdentifiers?.last_name, + dob: PatientIdentifiers?.dob, + mrn: PatientIdentifiers?.mrn, + phone: PatientIdentifiers?.phone, + }; + } catch (error: unknown) { + return await handleAndReturnError(error); + } + } + + const QueryResponse: QueryResponse = await makeFhirQuery(QueryRequest); + + // Bundle data + const bundle: APIQueryResponse = await createBundle(QueryResponse); + + return NextResponse.json(bundle, { + status: 200, + }); } diff --git a/query-connector/src/app/api/route.ts b/query-connector/src/app/api/route.ts new file mode 100644 index 000000000..d92594425 --- /dev/null +++ b/query-connector/src/app/api/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; + +/** + * Health check endpoint + * @returns An indication for the health of the API + */ +export async function GET() { + return NextResponse.json({ status: "OK" }, { status: 200 }); +} diff --git a/query-connector/src/app/shared/constants.ts b/query-connector/src/app/shared/constants.ts index e89f4b451..7f0fdb291 100644 --- a/query-connector/src/app/shared/constants.ts +++ b/query-connector/src/app/shared/constants.ts @@ -242,6 +242,10 @@ export const INVALID_FHIR_SERVERS = `Invalid fhir_server. Please provide a valid export const RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE = "Request body is not a Patient resource."; export const MISSING_API_QUERY_PARAM = "Missing id or fhir_server."; +export const INVALID_MESSAGE_FORMAT = + "Invalid message format. Format parameter needs to be either 'HL7' or 'FHIR'"; +export const HL7_BODY_MISFORMAT = + "Invalid HL7 request. Please add your HL7 message to the request body in between curly braces like so - { YOUR MESSAGE HERE } "; export const MISSING_PATIENT_IDENTIFIERS = "No patient identifiers to parse from requestBody."; diff --git a/query-connector/src/app/shared/database-service.ts b/query-connector/src/app/shared/database-service.ts index 7b4622c4c..ae98bd39d 100644 --- a/query-connector/src/app/shared/database-service.ts +++ b/query-connector/src/app/shared/database-service.ts @@ -921,7 +921,7 @@ export async function updateFhirServer( if (existingServer.rows.length > 0) { const existingHeaders = existingServer.rows[0].headers || {}; // Remove Authorization if it exists when switching to no auth - const { Authorization, ...restHeaders } = existingHeaders; + const { _, ...restHeaders } = existingHeaders; headers = restHeaders; } } diff --git a/query-connector/src/app/tests/integration/api-query.test.ts b/query-connector/src/app/tests/integration/api-query.test.ts index fda3b1b3f..a9fe56c23 100644 --- a/query-connector/src/app/tests/integration/api-query.test.ts +++ b/query-connector/src/app/tests/integration/api-query.test.ts @@ -1,14 +1,20 @@ import { Bundle, BundleEntry, Patient } from "fhir/r4"; -import { GET, POST } from "../../api/query/route"; import { readJsonFile } from "../shared_utils/readJsonFile"; import { INVALID_FHIR_SERVERS, + INVALID_MESSAGE_FORMAT, MISSING_API_QUERY_PARAM, MISSING_PATIENT_IDENTIFIERS, RESPONSE_BODY_IS_NOT_PATIENT_RESOURCE, USE_CASE_DETAILS, } from "@/app/shared/constants"; import { NextRequest } from "next/server"; +import { POST } from "@/app/api/query/route"; +import { GET } from "@/app/api/route"; +import { + PATIENT_HL7_MESSAGE, + PATIENT_HL7_MESSAGE_NO_IDENTIFIERS, +} from "./fixtures"; // Utility function to create a minimal NextRequest-like object function createNextRequest( @@ -17,6 +23,7 @@ function createNextRequest( ): NextRequest { return { json: async () => body, + text: async () => body, nextUrl: { searchParams }, method: "POST", headers: new Headers(), @@ -42,11 +49,22 @@ describe("GET Health Check", () => { }); describe("POST Query FHIR Server", () => { + beforeEach(() => { + // supress the console warns for the error endpoints + jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + const SYPHILIS_QUERY_ID = USE_CASE_DETAILS.syphilis.id; it("should return an OperationOutcome if the request body is not a Patient resource", async () => { const request = createNextRequest( { resourceType: "Observation" }, - new URLSearchParams(), + new URLSearchParams( + `id=${SYPHILIS_QUERY_ID}&fhir_server=HELIOS Meld: Direct`, + ), ); const response = await POST(request); const body = await response.json(); @@ -59,7 +77,9 @@ describe("POST Query FHIR Server", () => { it("should return an OperationOutcome if there are no patient identifiers to parse from the request body", async () => { const request = createNextRequest( { resourceType: "Patient" }, - new URLSearchParams(), + new URLSearchParams( + `id=${SYPHILIS_QUERY_ID}&fhir_server=HELIOS Meld: Direct`, + ), ); const response = await POST(request); const body = await response.json(); @@ -85,6 +105,30 @@ describe("POST Query FHIR Server", () => { expect(body.resourceType).toBe("OperationOutcome"); expect(body.issue[0].diagnostics).toBe(INVALID_FHIR_SERVERS); }); + it("should return an OperationOutcome if the message type is not valid", async () => { + const request = createNextRequest( + PatientResource, + new URLSearchParams( + "use_case=syphilis&fhir_server=HELIOS Meld: Direct&message_format=invalid", + ), + ); + const response = await POST(request); + const body = await response.json(); + expect(body.resourceType).toBe("OperationOutcome"); + expect(body.issue[0].diagnostics).toBe(INVALID_MESSAGE_FORMAT); + }); + it("should return a 400 Patient identifier error if HL7 message doesn't have identifiers", async () => { + const request = createNextRequest( + PATIENT_HL7_MESSAGE_NO_IDENTIFIERS, + new URLSearchParams( + `id=${SYPHILIS_QUERY_ID}&fhir_server=HELIOS Meld: Direct&message_format=HL7`, + ), + ); + const response = await POST(request); + const body = await response.json(); + expect(body.resourceType).toBe("OperationOutcome"); + expect(body.issue[0].diagnostics).toBe(MISSING_PATIENT_IDENTIFIERS); + }); // Delete this test once we've messaged out the deprecation of use_case and // partners have switched over to using id it("should return a legitimate FHIR bundle if it uses the deprecated use_case param", async () => { @@ -106,5 +150,27 @@ describe("POST Query FHIR Server", () => { const response = await POST(request); const body = await response.json(); expect(body.resourceType).toBe("Bundle"); + + // should also work if FHIR format type is explicitly specified + const explicitFhirRequest = createNextRequest( + PatientResource, + new URLSearchParams( + `id=${SYPHILIS_QUERY_ID}&fhir_server=HELIOS Meld: Direct&message_format=FHIR`, + ), + ); + const explicitFhirResponse = await POST(explicitFhirRequest); + const explicitFhirBody = await explicitFhirResponse.json(); + expect(explicitFhirBody.resourceType).toBe("Bundle"); + }); + it("should return a FHIR bundle if HL7 message is provided in the query body", async () => { + const request = createNextRequest( + PATIENT_HL7_MESSAGE, + new URLSearchParams( + `id=${SYPHILIS_QUERY_ID}&fhir_server=HELIOS Meld: Direct&message_format=HL7`, + ), + ); + const response = await POST(request); + const body = await response.json(); + expect(body.resourceType).toBe("Bundle"); }); }); diff --git a/query-connector/src/app/tests/integration/fixtures.ts b/query-connector/src/app/tests/integration/fixtures.ts new file mode 100644 index 000000000..3de6019cf --- /dev/null +++ b/query-connector/src/app/tests/integration/fixtures.ts @@ -0,0 +1,8 @@ +export const PATIENT_HL7_MESSAGE = `{ + MSH|^~\&#|ELIS.SC.STAG^2.16.840.1.114222.4.3.4.40.1.2^ISO|Hospital C^2.16.840.1.114222.4.1.171355^ISO|SCION_TEST^2.16.840.1.114222.4.3.2.2.1^ISO|CDC^2.16.840.1.114222.4.3.2.2.1.175.1^ISO|20240715133627.353-0500||ORU^R01^ORU_R01|OE715241T20240715133627|T|2.5.1|||AL|NE|USA||||PHLabReport-Ack^^2.16.840.1.113883.9.11^ISO + PID|1||8692756^^^ELIS.SC.STAG&2.16.840.1.114222.4.3.4.40.1.2&ISO^PI||Shaw^Lee^C^^^^L||1975-12-06|M||2106-3^White^CDCREC^^^^^^White|0759 Sharp Corner^^Pamelaland^CT^^USA^H|||||U^Unknown^HL70002^^^^2.5.1||||||2135-2^Hispanic or Latino^CDCREC^^^^^^Hispanic + }`; +export const PATIENT_HL7_MESSAGE_NO_IDENTIFIERS = `{ + MSH|^~\&#|ELIS.SC.STAG^2.16.840.1.114222.4.3.4.40.1.2^ISO|Hospital C^2.16.840.1.114222.4.1.171355^ISO|SCION_TEST^2.16.840.1.114222.4.3.2.2.1^ISO|CDC^2.16.840.1.114222.4.3.2.2.1.175.1^ISO|20240715133627.353-0500||ORU^R01^ORU_R01|OE715241T20240715133627|T|2.5.1|||AL|NE|USA||||PHLabReport-Ack^^2.16.840.1.113883.9.11^ISO + PID|1|||U^Unknown^HL70002^^^^2.5.1||||||2135-2^Hispanic or Latino^CDCREC^^^^^^Hispanic + }`; diff --git a/query-connector/src/auth.ts b/query-connector/src/auth.ts index 698ae4600..8924e38e3 100644 --- a/query-connector/src/auth.ts +++ b/query-connector/src/auth.ts @@ -63,7 +63,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ // Ensure user is in the database **only on first login** try { await addUserIfNotExists(userToken); - } catch (error) {} + } catch (error) { + console.error("Something went wrong in generating user token", error); + } if (userToken.username !== "") { if (isAuthDisabled()) {