From 353d78e9a1772190c8523bb487aa37225381862e Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Sun, 2 Jul 2023 09:38:19 -0700 Subject: [PATCH 01/10] Adding express types to all controllers --- .../Controllers/addElectionRollController.ts | 6 ++++-- .../Controllers/archiveElectionController.ts | 4 +++- backend/src/Controllers/castVoteController.ts | 6 ++++-- .../changeElectionRollController.ts | 20 ++++++++++--------- .../Controllers/createElectionController.ts | 5 +++-- .../Controllers/deleteElectionController.ts | 6 ++++-- .../src/Controllers/editElectionController.ts | 4 +++- .../editElectionRolesController.ts | 6 ++++-- .../Controllers/editElectionRollController.ts | 6 ++++-- .../src/Controllers/elections.controllers.ts | 10 ++++++---- .../Controllers/finalizeElectionController.ts | 4 +++- .../src/Controllers/getBallotByBallotID.ts | 6 +++--- .../getBallotsByElectionIDController.ts | 6 +++--- .../getElectionResultsController.ts | 6 ++++-- .../Controllers/getElectionRollController.ts | 11 +++++----- .../src/Controllers/getElectionsController.ts | 5 +++-- .../Controllers/registerVoterController.ts | 16 +++++++++------ backend/src/Controllers/sandboxController.ts | 3 ++- .../src/Controllers/sendInvitesController.ts | 4 +++- .../Controllers/setPublicResultsController.ts | 4 +++- .../src/Controllers/uploadImageController.ts | 7 ++++++- backend/src/IRequest.ts | 15 ++++++++++++++ domain_model/VoterAuth.ts | 6 +++++- 23 files changed, 111 insertions(+), 55 deletions(-) diff --git a/backend/src/Controllers/addElectionRollController.ts b/backend/src/Controllers/addElectionRollController.ts index 52680840..39190e41 100644 --- a/backend/src/Controllers/addElectionRollController.ts +++ b/backend/src/Controllers/addElectionRollController.ts @@ -6,12 +6,14 @@ import { hasPermission, permission, permissions } from '../../../domain_model/pe import { expectPermission } from "./controllerUtils"; import { BadRequest, InternalServerError } from "@curveball/http-errors"; import { randomUUID } from "crypto"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; const ElectionRollModel = ServiceLocator.electionRollDb(); const className = "VoterRolls.Controllers"; -const addElectionRoll = async (req: any, res: any, next: any) => { +const addElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { expectPermission(req.user_auth.roles, permissions.canAddToElectionRoll) Logger.info(req, `${className}.addElectionRoll ${req.election.election_id}`); const history = [{ @@ -52,7 +54,7 @@ const addElectionRoll = async (req: any, res: any, next: any) => { throw new InternalServerError(msg); } - res.status('200').json({ election: req.election, newElectionRoll }); + res.status(200).json({ election: req.election, newElectionRoll }); return next() } diff --git a/backend/src/Controllers/archiveElectionController.ts b/backend/src/Controllers/archiveElectionController.ts index 18e37ca4..cfea67ee 100644 --- a/backend/src/Controllers/archiveElectionController.ts +++ b/backend/src/Controllers/archiveElectionController.ts @@ -4,12 +4,14 @@ import { permissions } from '../../../domain_model/permissions'; import { expectPermission } from "./controllerUtils"; import { BadRequest, InternalServerError } from "@curveball/http-errors"; import { Election } from '../../../domain_model/Election'; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); const className = "election.Controllers"; -const archiveElection = async (req: any, res: any, next: any) => { +const archiveElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.archive ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canEditElectionState) diff --git a/backend/src/Controllers/castVoteController.ts b/backend/src/Controllers/castVoteController.ts index f584e838..fcf5fc06 100644 --- a/backend/src/Controllers/castVoteController.ts +++ b/backend/src/Controllers/castVoteController.ts @@ -10,6 +10,8 @@ import { randomUUID } from "crypto"; import { Uid } from "../../../domain_model/Uid"; import { Receipt } from "../Services/Email/EmailTemplates" import { getOrCreateElectionRoll, checkForMissingAuthenticationData, getVoterAuthorization } from "./voterRollUtils" +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; const ElectionsModel = ServiceLocator.electionsDb(); const ElectionRollModel = ServiceLocator.electionRollDb(); @@ -26,7 +28,7 @@ type CastVoteEvent = { const castVoteEventQueue = "castVoteEvent"; -async function castVoteController(req: IRequest, res: any, next: any) { +async function castVoteController(req: IElectionRequest, res: Response, next: NextFunction) { Logger.info(req, "Cast Vote Controller"); const targetElection = req.election; @@ -104,7 +106,7 @@ async function castVoteController(req: IRequest, res: any, next: any) { } await (await EventQueue).publish(castVoteEventQueue, event); - res.status("200").json({ ballot: inputBallot} ); + res.status(200).json({ ballot: inputBallot} ); Logger.debug(req, "CastVoteController done, saved event to store", event); }; diff --git a/backend/src/Controllers/changeElectionRollController.ts b/backend/src/Controllers/changeElectionRollController.ts index f76f0d4f..eb33e715 100644 --- a/backend/src/Controllers/changeElectionRollController.ts +++ b/backend/src/Controllers/changeElectionRollController.ts @@ -7,34 +7,36 @@ const ElectionRollModel = ServiceLocator.electionRollDb(); import { hasPermission, permission, permissions } from '../../../domain_model/permissions'; import { expectPermission } from "./controllerUtils"; import { InternalServerError, Unauthorized } from "@curveball/http-errors"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; const className = "VoterRollState.Controllers"; -const approveElectionRoll = async (req: any, res: any, next: any) => { +const approveElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.approveElectionRoll ${req.params.id}`); changeElectionRollState(req, ElectionRollState.approved, [ElectionRollState.registered, ElectionRollState.flagged], permissions.canApproveElectionRoll) - res.status('200').json({}) + res.status(200).json({}) } -const flagElectionRoll = async (req: any, res: any, next: any) => { +const flagElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.flagElectionRoll ${req.params.id}`); changeElectionRollState(req, ElectionRollState.flagged, [ElectionRollState.approved, ElectionRollState.registered, ElectionRollState.invalid], permissions.canFlagElectionRoll) - res.status('200').json({}) + res.status(200).json({}) } -const invalidateElectionRoll = async (req: any, res: any, next: any) => { +const invalidateElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.flagElectionRoll ${req.params.id}`); changeElectionRollState(req, ElectionRollState.invalid, [ElectionRollState.flagged], permissions.canInvalidateBallot) - res.status('200').json({}) + res.status(200).json({}) } -const uninvalidateElectionRoll = async (req: any, res: any, next: any) => { +const uninvalidateElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.flagElectionRoll ${req.params.id}`); changeElectionRollState(req, ElectionRollState.flagged, [ElectionRollState.invalid], permissions.canInvalidateBallot) - res.status('200').json({}) + res.status(200).json({}) } -const changeElectionRollState = async (req: any, newState: ElectionRollState, validStates: ElectionRollState[], permission: permission) => { +const changeElectionRollState = async (req: IElectionRequest, newState: ElectionRollState, validStates: ElectionRollState[], permission: permission) => { expectPermission(req.user_auth.roles, permission) const roll = await ElectionRollModel.getByVoterID(req.election.election_id, req.body.electionRollEntry.voter_id, req) if (!roll) { diff --git a/backend/src/Controllers/createElectionController.ts b/backend/src/Controllers/createElectionController.ts index dc07ca00..ca9ad04e 100644 --- a/backend/src/Controllers/createElectionController.ts +++ b/backend/src/Controllers/createElectionController.ts @@ -6,18 +6,19 @@ import Logger from "../Services/Logging/Logger"; import { InternalServerError } from "@curveball/http-errors"; import { ILoggingContext } from "../Services/Logging/ILogger"; import { expectValidElectionFromRequest, catchAndRespondError, expectPermission } from "./controllerUtils"; +import { Response, NextFunction } from "express"; var ElectionsModel = ServiceLocator.electionsDb(); const className = "createElectionController"; const failMsgPrfx = "CATCH: create error electio err: "; -async function createElectionController(req: IRequest, res: any, next: any) { +async function createElectionController(req: IRequest, res: Response, next: NextFunction) { Logger.info(req, "Create Election Controller"); const inputElection = expectValidElectionFromRequest(req); const resElection = await createAndCheckElection(inputElection, req); - res.status("200").json({ election: resElection }); + res.status(200).json({ election: resElection }); }; const createAndCheckElection = async ( diff --git a/backend/src/Controllers/deleteElectionController.ts b/backend/src/Controllers/deleteElectionController.ts index 7356ed35..5437bce7 100644 --- a/backend/src/Controllers/deleteElectionController.ts +++ b/backend/src/Controllers/deleteElectionController.ts @@ -5,11 +5,13 @@ import { IRequest } from '../IRequest'; import { hasPermission, permissions } from '../../../domain_model/permissions'; import { expectPermission } from "./controllerUtils"; import { BadRequest } from "@curveball/http-errors"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); const className = "Elections.Controllers"; -const deleteElection = async (req: any, res: any, next: any) => { +const deleteElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { expectPermission(req.user_auth.roles, permissions.canDeleteElection) const electionId = req.election.election_id; Logger.info(req, `${className}.deleteElection ${electionId}`) @@ -21,7 +23,7 @@ const deleteElection = async (req: any, res: any, next: any) => { throw new BadRequest(msg) } Logger.info(req, `Deleted election ${electionId}`); - return res.status('200') + return res.status(200) } module.exports = { diff --git a/backend/src/Controllers/editElectionController.ts b/backend/src/Controllers/editElectionController.ts index 0e865c73..e3d2561c 100644 --- a/backend/src/Controllers/editElectionController.ts +++ b/backend/src/Controllers/editElectionController.ts @@ -5,11 +5,13 @@ import { responseErr } from '../Util'; import { expectPermission } from "./controllerUtils"; import { permissions } from '../../../domain_model/permissions'; import { BadRequest } from "@curveball/http-errors"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); -const editElection = async (req: any, res: any, next: any) => { +const editElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { const inputElection = req.body.Election; Logger.info(req, `editElection: ${req.election.election_id}`) diff --git a/backend/src/Controllers/editElectionRolesController.ts b/backend/src/Controllers/editElectionRolesController.ts index c0cf527c..353bc8c1 100644 --- a/backend/src/Controllers/editElectionRolesController.ts +++ b/backend/src/Controllers/editElectionRolesController.ts @@ -5,11 +5,13 @@ import { responseErr } from '../Util'; import { expectPermission } from "./controllerUtils"; import { permissions } from '../../../domain_model/permissions'; import { BadRequest } from "@curveball/http-errors"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); -const editElectionRoles = async (req: any, res: any, next: any) => { +const editElectionRoles = async (req: IElectionRequest, res: Response, next: NextFunction) => { const inputElection = req.body.Election; Logger.info(req, `editElectionRoles: ${req.election.election_id}`) @@ -33,7 +35,7 @@ const editElectionRoles = async (req: any, res: any, next: any) => { req.election = updatedElection Logger.debug(req, `editElectionRoles succeeds for ${updatedElection.election_id}`); - res.status('200').json({election: req.election}) + res.status(200).json({election: req.election}) } module.exports = { diff --git a/backend/src/Controllers/editElectionRollController.ts b/backend/src/Controllers/editElectionRollController.ts index 6dd2a8e4..214c9777 100644 --- a/backend/src/Controllers/editElectionRollController.ts +++ b/backend/src/Controllers/editElectionRollController.ts @@ -5,12 +5,14 @@ import { responseErr } from "../Util"; import { hasPermission, permissions } from '../../../domain_model/permissions'; import { expectPermission } from "./controllerUtils"; import { BadRequest } from "@curveball/http-errors"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; const ElectionRollModel = ServiceLocator.electionRollDb(); const className = "VoterRolls.Controllers"; -const editElectionRoll = async (req: any, res: any, next: any) => { +const editElectionRoll = async (req: IElectionRequest, res: Response, next: NextFunction) => { expectPermission(req.user_auth.roles, permissions.canEditElectionRoll) const electinoRollInput = req.body.electionRollEntry; Logger.info(req, `${className}.editElectionRoll`, { electionRollEntry: electinoRollInput }); @@ -28,7 +30,7 @@ const editElectionRoll = async (req: any, res: any, next: any) => { Logger.info(req, msg); throw new BadRequest(msg) } - res.status('200').json(electionRollEntry) + res.status(200).json(electionRollEntry) } module.exports = { diff --git a/backend/src/Controllers/elections.controllers.ts b/backend/src/Controllers/elections.controllers.ts index 4cdeb355..16d435c6 100644 --- a/backend/src/Controllers/elections.controllers.ts +++ b/backend/src/Controllers/elections.controllers.ts @@ -2,7 +2,7 @@ import { Election, removeHiddenFields } from '../../../domain_model/Election'; import ServiceLocator from '../ServiceLocator'; import Logger from '../Services/Logging/Logger'; import { responseErr } from '../Util'; -import { IRequest } from '../IRequest'; +import { IElectionRequest, IRequest } from '../IRequest'; import { roles } from "../../../domain_model/roles" import { getPermissions } from '../../../domain_model/permissions'; import { getOrCreateElectionRoll, checkForMissingAuthenticationData, getVoterAuthorization } from "./voterRollUtils" @@ -50,7 +50,7 @@ const electionSpecificAuth = async (req: IRequest, res: any, next: any) => { return next(); } -const electionPostAuthMiddleware = async (req: any, res: any, next: any) => { +const electionPostAuthMiddleware = async (req: IElectionRequest, res: any, next: any) => { Logger.info(req, `${className}.electionPostAuthMiddleware ${req.params.id}`); try { // Update Election State @@ -64,8 +64,10 @@ const electionPostAuthMiddleware = async (req: any, res: any, next: any) => { req.election = election; - req.user_auth = {} - req.user_auth.roles = [] + req.user_auth = { + roles: [], + permissions: [] + } if (req.user && req.election && req.user.typ != 'TEMP_ID'){ if (req.user.sub === req.election.owner_id){ req.user_auth.roles.push(roles.owner) diff --git a/backend/src/Controllers/finalizeElectionController.ts b/backend/src/Controllers/finalizeElectionController.ts index 423c3125..44a8e294 100644 --- a/backend/src/Controllers/finalizeElectionController.ts +++ b/backend/src/Controllers/finalizeElectionController.ts @@ -5,13 +5,15 @@ import { expectPermission } from "./controllerUtils"; import { BadRequest } from "@curveball/http-errors"; import { ElectionRoll } from '../../../domain_model/ElectionRoll'; const { sendBatchEmailInvites } = require('./sendInvitesController') +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); var ElectionRollModel = ServiceLocator.electionRollDb(); const className = "election.Controllers"; -const finalizeElection = async (req: any, res: any, next: any) => { +const finalizeElection = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.finalize ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canEditElectionState) if (req.election.state !== 'draft') { diff --git a/backend/src/Controllers/getBallotByBallotID.ts b/backend/src/Controllers/getBallotByBallotID.ts index 95bbb6cf..66886b06 100644 --- a/backend/src/Controllers/getBallotByBallotID.ts +++ b/backend/src/Controllers/getBallotByBallotID.ts @@ -1,12 +1,12 @@ import ServiceLocator from "../ServiceLocator"; import Logger from "../Services/Logging/Logger"; import { BadRequest } from "@curveball/http-errors"; -import { expectPermission } from "./controllerUtils"; -import { permissions } from '../../../domain_model/permissions'; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; const BallotModel = ServiceLocator.ballotsDb(); -const getBallotByBallotID = async (req: any, res: any, next: any) => { +const getBallotByBallotID = async (req: IElectionRequest, res: Response, next: NextFunction) => { var electionId = req.election.election_id; var ballot_id = req.params.ballot_id if (!ballot_id) { diff --git a/backend/src/Controllers/getBallotsByElectionIDController.ts b/backend/src/Controllers/getBallotsByElectionIDController.ts index 302acdf5..ac60f075 100644 --- a/backend/src/Controllers/getBallotsByElectionIDController.ts +++ b/backend/src/Controllers/getBallotsByElectionIDController.ts @@ -1,14 +1,14 @@ import ServiceLocator from "../ServiceLocator"; import Logger from "../Services/Logging/Logger"; import { BadRequest } from "@curveball/http-errors"; -import { Ballot } from '../../../domain_model/Ballot'; -import { Score } from '../../../domain_model/Score'; import { expectPermission } from "./controllerUtils"; import { permissions } from '../../../domain_model/permissions'; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; const BallotModel = ServiceLocator.ballotsDb(); -const getBallotsByElectionID = async (req: any, res: any, next: any) => { +const getBallotsByElectionID = async (req: IElectionRequest, res: Response, next: NextFunction) => { var electionId = req.election.election_id; Logger.debug(req, "getBallotsByElectionID: " + electionId); diff --git a/backend/src/Controllers/getElectionResultsController.ts b/backend/src/Controllers/getElectionResultsController.ts index 42f17a4c..50ddf1cc 100644 --- a/backend/src/Controllers/getElectionResultsController.ts +++ b/backend/src/Controllers/getElectionResultsController.ts @@ -5,11 +5,13 @@ import { Ballot } from '../../../domain_model/Ballot'; import { Score } from '../../../domain_model/Score'; import { expectPermission } from "./controllerUtils"; import { permissions } from '../../../domain_model/permissions'; -import { VotingMethods } from '../Tabulators/VotingMethodSelecter' +import { VotingMethods } from '../Tabulators/VotingMethodSelecter'; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; const BallotModel = ServiceLocator.ballotsDb(); -const getElectionResults = async (req: any, res: any, next: any) => { +const getElectionResults = async (req: IElectionRequest, res: Response, next: NextFunction) => { var electionId = req.election.election_id; Logger.info(req, `getElectionResults: ${electionId}`); diff --git a/backend/src/Controllers/getElectionRollController.ts b/backend/src/Controllers/getElectionRollController.ts index b1d5f1ed..a5bf59a6 100644 --- a/backend/src/Controllers/getElectionRollController.ts +++ b/backend/src/Controllers/getElectionRollController.ts @@ -1,16 +1,16 @@ -import { ElectionRoll, ElectionRollState } from "../../../domain_model/ElectionRoll"; import ServiceLocator from "../ServiceLocator"; import Logger from "../Services/Logging/Logger"; -import { responseErr } from "../Util"; -import { hasPermission, permissions } from '../../../domain_model/permissions'; +import { permissions } from '../../../domain_model/permissions'; import { expectPermission } from "./controllerUtils"; import { BadRequest } from "@curveball/http-errors"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; const ElectionRollModel = ServiceLocator.electionRollDb(); const className = "VoterRolls.Controllers"; -const getRollsByElectionID = async (req: any, res: any, next: any) => { +const getRollsByElectionID = async (req: IElectionRequest, res: Response, next: NextFunction) => { expectPermission(req.user_auth.roles, permissions.canViewElectionRoll) const electionId = req.election.election_id; Logger.info(req, `${className}.getRollsByElectionID ${electionId}`); @@ -24,12 +24,11 @@ const getRollsByElectionID = async (req: any, res: any, next: any) => { } Logger.debug(req, `Got Election: ${req.params.id}`, electionRoll); - req.electionRoll = electionRoll Logger.info(req, `${className}.returnRolls ${req.params.id}`); res.json({ election: req.election, electionRoll: electionRoll }); } -const getByVoterID = async (req: any, res: any, next: any) => { +const getByVoterID = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.getByVoterID ${req.election.election_id} ${req.params.voter_id}`) expectPermission(req.user_auth.roles, permissions.canViewElectionRoll) const electionRollEntry = await ElectionRollModel.getByVoterID(req.election.election_id, req.params.voter_id, req) diff --git a/backend/src/Controllers/getElectionsController.ts b/backend/src/Controllers/getElectionsController.ts index 464f1acf..f9aace05 100644 --- a/backend/src/Controllers/getElectionsController.ts +++ b/backend/src/Controllers/getElectionsController.ts @@ -1,14 +1,15 @@ import ServiceLocator from '../ServiceLocator'; import Logger from '../Services/Logging/Logger'; -import { responseErr } from '../Util'; import { BadRequest } from "@curveball/http-errors"; import { Election, removeHiddenFields } from '../../../domain_model/Election'; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); var ElectionRollModel = ServiceLocator.electionRollDb(); -const getElections = async (req: any, res: any, next: any) => { +const getElections = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `getElections`); // var filter = (req.query.filter == undefined) ? "" : req.query.filter; const email = req.user?.email || '' diff --git a/backend/src/Controllers/registerVoterController.ts b/backend/src/Controllers/registerVoterController.ts index 04b16fd8..be6d467b 100644 --- a/backend/src/Controllers/registerVoterController.ts +++ b/backend/src/Controllers/registerVoterController.ts @@ -8,8 +8,10 @@ import { getOrCreateElectionRoll, checkForMissingAuthenticationData, getVoterAut import { BadRequest, InternalServerError, Unauthorized } from "@curveball/http-errors"; import { Election } from "../../../domain_model/Election"; import { randomUUID } from "crypto"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; -const registerVoter = async (req: any, res: any, next: any) => { +const registerVoter = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.registerVoter ${req.election.election_id}`); const targetElection: Election | null = req.election; @@ -33,19 +35,21 @@ const registerVoter = async (req: any, res: any, next: any) => { if (missingAuthData !== null) { throw new Unauthorized(missingAuthData); } - - let roll = await getOrCreateElectionRoll(req, targetElection, req); - if (roll === null) { + let roll: ElectionRoll + let tempRoll = await getOrCreateElectionRoll(req, targetElection, req); + if (tempRoll == null) { roll = { voter_id: randomUUID(), election_id: req.election.election_id, email: req.user?.email, submitted: false, - ip_address: targetElection.settings.voter_authentication.ip_address ? req.ip : null, + ip_address: targetElection.settings.voter_authentication.ip_address ? req.ip : undefined, state: ElectionRollState.registered, history: [], registration: req.body.registration, } + } else { + roll = tempRoll } const history = { action_type: 'registered', @@ -61,7 +65,7 @@ const registerVoter = async (req: any, res: any, next: any) => { throw new InternalServerError(msg) } - res.status('200').json(JSON.stringify({ election: req.election, NewElectionRoll })) + res.status(200).json(JSON.stringify({ election: req.election, NewElectionRoll })) return next() } diff --git a/backend/src/Controllers/sandboxController.ts b/backend/src/Controllers/sandboxController.ts index fb6b457d..03ee513e 100644 --- a/backend/src/Controllers/sandboxController.ts +++ b/backend/src/Controllers/sandboxController.ts @@ -1,8 +1,9 @@ import Logger from '../Services/Logging/Logger'; const className = "Elections.Controllers"; import { VotingMethods } from '../Tabulators/VotingMethodSelecter' +import { Request, Response, NextFunction } from 'express'; -const getSandboxResults = async (req: any, res: any, next: any) => { +const getSandboxResults = async (req: Request, res: Response, next: NextFunction) => { Logger.info(req, `${className}.getSandboxResults`); const candidateNames = req.body.candidates; diff --git a/backend/src/Controllers/sendInvitesController.ts b/backend/src/Controllers/sendInvitesController.ts index 31c40258..2e4fa8a5 100644 --- a/backend/src/Controllers/sendInvitesController.ts +++ b/backend/src/Controllers/sendInvitesController.ts @@ -8,6 +8,8 @@ import { ElectionRoll } from '../../../domain_model/ElectionRoll'; import { Uid } from "../../../domain_model/Uid"; import { Election } from '../../../domain_model/Election'; import { randomUUID } from "crypto"; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; var ElectionRollModel = ServiceLocator.electionRollDb(); var EmailService = ServiceLocator.emailService(); @@ -25,7 +27,7 @@ export type SendInviteEvent = { sender: string, } -const sendInvitationsController = async (req: any, res: any, next: any) => { +const sendInvitationsController = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.sendInvitations ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canSendEmails) diff --git a/backend/src/Controllers/setPublicResultsController.ts b/backend/src/Controllers/setPublicResultsController.ts index 715c8e67..dbdef083 100644 --- a/backend/src/Controllers/setPublicResultsController.ts +++ b/backend/src/Controllers/setPublicResultsController.ts @@ -4,12 +4,14 @@ import { permissions } from '../../../domain_model/permissions'; import { expectPermission } from "./controllerUtils"; import { BadRequest, InternalServerError } from "@curveball/http-errors"; import { Election } from '../../../domain_model/Election'; +import { IElectionRequest } from "../IRequest"; +import { Response, NextFunction } from 'express'; var ElectionsModel = ServiceLocator.electionsDb(); const className = "election.Controllers"; -const setPublicResults = async (req: any, res: any, next: any) => { +const setPublicResults = async (req: IElectionRequest, res: Response, next: NextFunction) => { Logger.info(req, `${className}.finalize ${req.election.election_id}`); expectPermission(req.user_auth.roles, permissions.canEditElectionState) const election: Election = req.election diff --git a/backend/src/Controllers/uploadImageController.ts b/backend/src/Controllers/uploadImageController.ts index e9d7d440..b4e18083 100644 --- a/backend/src/Controllers/uploadImageController.ts +++ b/backend/src/Controllers/uploadImageController.ts @@ -1,5 +1,6 @@ import { InternalServerError } from "@curveball/http-errors"; import { randomUUID } from "crypto"; +import { Request, Response, NextFunction } from 'express'; const S3 = require("aws-sdk/clients/s3"); const ID = process.env.S3_ID; const SECRET = process.env.S3_SECRET; @@ -30,8 +31,12 @@ const upload = multer({ limits: { fileSize: 1000000000, files: 1 }, }); +interface ImageRequest extends Request { + file: any +} -const uploadImageController = async (req: any, res: any, next: any) => { +// TODO: add multer file and S3 types +const uploadImageController = async (req: ImageRequest, res: Response, next: NextFunction) => { const file = req.file const params = { diff --git a/backend/src/IRequest.ts b/backend/src/IRequest.ts index bc273b42..3a405335 100644 --- a/backend/src/IRequest.ts +++ b/backend/src/IRequest.ts @@ -1,6 +1,10 @@ import { randomUUID } from 'crypto'; import { Request } from 'express'; import { Election } from '../../domain_model/Election'; +import { roles } from '../../domain_model/roles'; +import { permission, permissions } from '../../domain_model/permissions'; + +type p = keyof typeof permissions export interface IRequest extends Request { contextId?: string; @@ -9,6 +13,17 @@ export interface IRequest extends Request { user?: any; } +export interface IElectionRequest extends IRequest { + election: Election; + user_auth: { + roles: roles[]; + permissions: p[] + } + authorized_voter?: Boolean; + has_voted?: Boolean; + +} + export function reqIdSuffix(req:IRequest):string { return ` (${req.contextId})`; } diff --git a/domain_model/VoterAuth.ts b/domain_model/VoterAuth.ts index a6e2ece0..0cbf6760 100644 --- a/domain_model/VoterAuth.ts +++ b/domain_model/VoterAuth.ts @@ -1,6 +1,10 @@ +import { permissions } from "./permissions"; +import { roles } from "./roles"; + export interface VoterAuth { authorized_voter: boolean; required: string; has_voted: boolean; - permissions: string[] + permissions: (keyof typeof permissions)[]; + roles: roles[]; } \ No newline at end of file From 9eebb5f665cb205aabd916feead4966feece426a Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Sun, 2 Jul 2023 09:43:42 -0700 Subject: [PATCH 02/10] Adding types to user token controller --- backend/src/Controllers/getUserTokenController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/Controllers/getUserTokenController.ts b/backend/src/Controllers/getUserTokenController.ts index 5d448c89..be323078 100644 --- a/backend/src/Controllers/getUserTokenController.ts +++ b/backend/src/Controllers/getUserTokenController.ts @@ -1,9 +1,9 @@ -import { response } from "express"; +import { Request, Response, NextFunction } from 'express'; import ServiceLocator from "../ServiceLocator"; const AccountService = ServiceLocator.accountService() -const getUserToken = async (req: any, res: any, next: any) => { +const getUserToken = async (req: Request, res: Response, next: NextFunction) => { const data = await AccountService.getToken(req) res.json(data) } From a57762d662de5bd716af6188fbe0a13b4526ed03 Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Mon, 3 Jul 2023 16:15:27 -0400 Subject: [PATCH 03/10] Adding kysely for election db --- backend/package-lock.json | 299 +++++++++++++++---- backend/package.json | 7 +- backend/src/Migrations/2023_07_03_Initial.ts | 28 ++ backend/src/Models/Database.ts | 6 + backend/src/Models/Elections.ts | 287 ++++++------------ backend/src/Models/IElection.ts | 26 ++ backend/src/ServiceLocator.ts | 92 +++--- backend/src/migrate-to-latest.ts | 46 +++ domain_model/Election.ts | 37 +-- 9 files changed, 523 insertions(+), 305 deletions(-) create mode 100644 backend/src/Migrations/2023_07_03_Initial.ts create mode 100644 backend/src/Models/Database.ts create mode 100644 backend/src/Models/IElection.ts create mode 100644 backend/src/migrate-to-latest.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index e42be125..020285b9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,6 +17,7 @@ "@types/jest": "^27.4.1", "@types/luxon": "^3.3.0", "@types/node": "^16.11.26", + "@types/pg": "^8.10.2", "aws-sdk": "^2.1277.0", "axios": "^0.24.0", "cookie-parser": "^1.4.6", @@ -27,6 +28,7 @@ "express-async-handler": "^1.2.0", "fraction.js": "^4.2.0", "jsonwebtoken": "^9.0.0", + "kysely": "^0.25.0", "luxon": "^3.3.0", "multer": "^1.4.5-lts.1", "node-fetch": "^3.2.3", @@ -1240,6 +1242,68 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==" }, + "node_modules/@types/pg": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.2.tgz", + "integrity": "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "engines": { + "node": ">=12" + } + }, "node_modules/@types/prettier": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.4.tgz", @@ -2490,11 +2554,14 @@ } }, "node_modules/dotenv": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", - "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/duplexer3": { @@ -4657,6 +4724,14 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.25.0.tgz", + "integrity": "sha512-srn0efIMu5IoEBk0tBmtGnoUss4uwvxtbFQWG/U2MosfqIace1l43IFP1PmEpHRDp+Z79xIcKEqmHH3dAvQdQA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", @@ -4819,13 +4894,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" @@ -4892,9 +4967,12 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/mkdirp": { "version": "0.5.6", @@ -5101,6 +5179,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -5312,23 +5395,26 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "node_modules/pg": { - "version": "8.7.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz", - "integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==", + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.1.tgz", + "integrity": "sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==", "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "^2.5.0", - "pg-pool": "^3.5.1", - "pg-protocol": "^1.5.0", + "pg-connection-string": "^2.6.1", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "engines": { "node": ">= 8.0.0" }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, "peerDependencies": { - "pg-native": ">=2.0.0" + "pg-native": ">=3.0.1" }, "peerDependenciesMeta": { "pg-native": { @@ -5361,10 +5447,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, "node_modules/pg-connection-string": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", - "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, "node_modules/pg-format": { "version": "1.0.4", @@ -5382,18 +5474,26 @@ "node": ">=4.0.0" } }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "engines": { + "node": ">=4" + } + }, "node_modules/pg-pool": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz", - "integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", - "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -5425,9 +5525,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "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" @@ -5492,6 +5592,11 @@ "node": ">=0.10.0" } }, + "node_modules/postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==" + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -8001,6 +8106,55 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==" }, + "@types/pg": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.2.tgz", + "integrity": "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==", + "requires": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + }, + "dependencies": { + "pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "requires": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + } + }, + "postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==" + }, + "postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "requires": { + "obuf": "~1.1.2" + } + }, + "postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==" + }, + "postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==" + } + } + }, "@types/prettier": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.4.tgz", @@ -8986,9 +9140,9 @@ } }, "dotenv": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", - "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==" + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" }, "duplexer3": { "version": "0.1.4", @@ -10609,6 +10763,11 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "kysely": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.25.0.tgz", + "integrity": "sha512-srn0efIMu5IoEBk0tBmtGnoUss4uwvxtbFQWG/U2MosfqIace1l43IFP1PmEpHRDp+Z79xIcKEqmHH3dAvQdQA==" + }, "latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", @@ -10737,13 +10896,13 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, "mime": { @@ -10786,9 +10945,9 @@ } }, "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "mkdirp": { "version": "0.5.6", @@ -10936,6 +11095,11 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -11092,15 +11256,16 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "pg": { - "version": "8.7.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz", - "integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==", + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.1.tgz", + "integrity": "sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "^2.5.0", - "pg-pool": "^3.5.1", - "pg-protocol": "^1.5.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.1", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", "pg-types": "^2.1.0", "pgpass": "1.x" } @@ -11126,10 +11291,16 @@ } } }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, "pg-connection-string": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", - "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, "pg-format": { "version": "1.0.4", @@ -11141,16 +11312,21 @@ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, + "pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==" + }, "pg-pool": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz", - "integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", "requires": {} }, "pg-protocol": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", - "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" }, "pg-types": { "version": "2.2.0", @@ -11179,9 +11355,9 @@ "dev": true }, "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pirates": { @@ -11222,6 +11398,11 @@ "xtend": "^4.0.0" } }, + "postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==" + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", diff --git a/backend/package.json b/backend/package.json index 78eea72f..63f6b661 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,8 @@ "test": "jest --forceExit --detectOpenHandles", "start": "npm run-script build && node ./build/backend/src/index.js", "dev": "nodemon ./src/index.ts", - "build": "tsc --project ./" + "build": "tsc --project ./", + "migrate:latest": "node ./build/backend/src/migrate-to-latest.js" }, "keywords": [], "author": "", @@ -18,9 +19,10 @@ "@types/cookie-parser": "^1.4.2", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/jest": "^27.4.1", "@types/luxon": "^3.3.0", "@types/node": "^16.11.26", - "@types/jest": "^27.4.1", + "@types/pg": "^8.10.2", "aws-sdk": "^2.1277.0", "axios": "^0.24.0", "cookie-parser": "^1.4.6", @@ -31,6 +33,7 @@ "express-async-handler": "^1.2.0", "fraction.js": "^4.2.0", "jsonwebtoken": "^9.0.0", + "kysely": "^0.25.0", "luxon": "^3.3.0", "multer": "^1.4.5-lts.1", "node-fetch": "^3.2.3", diff --git a/backend/src/Migrations/2023_07_03_Initial.ts b/backend/src/Migrations/2023_07_03_Initial.ts new file mode 100644 index 00000000..bcdf9c00 --- /dev/null +++ b/backend/src/Migrations/2023_07_03_Initial.ts @@ -0,0 +1,28 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('electiondb') + .addColumn('election_id', 'varchar', (col) => col.primaryKey()) + .addColumn('title', 'varchar') + .addColumn('description', 'text') + .addColumn('frontend_url', 'varchar') + .addColumn('start_time', 'varchar') + .addColumn('end_time', 'varchar') + .addColumn('support_email', 'varchar') + .addColumn('owner_id', 'varchar') + .addColumn('audit_ids', 'json') + .addColumn('admin_ids', 'json') + .addColumn('credential_ids', 'json') + .addColumn('state', 'varchar') + .addColumn('races', 'json', (col) => col.notNull()) + .addColumn('settings', 'json') + .addColumn('auth_key', 'varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('electiondb').execute() + await db.schema.dropTable('person').execute() +} + diff --git a/backend/src/Models/Database.ts b/backend/src/Models/Database.ts new file mode 100644 index 00000000..d2d0dacb --- /dev/null +++ b/backend/src/Models/Database.ts @@ -0,0 +1,6 @@ +import { ElectionTable } from "./IElection"; + + +export interface Database { + electiondb: ElectionTable +} \ No newline at end of file diff --git a/backend/src/Models/Elections.ts b/backend/src/Models/Elections.ts index 29d68d76..90140a76 100644 --- a/backend/src/Models/Elections.ts +++ b/backend/src/Models/Elections.ts @@ -1,247 +1,150 @@ -import { Election } from '../../../domain_model/Election'; import { Uid } from '../../../domain_model/Uid'; +import { Database } from './Database'; import { ILoggingContext } from '../Services/Logging/ILogger'; import Logger from '../Services/Logging/Logger'; -const className = 'ElectionsDB'; +import { Kysely, sql } from 'kysely' +import { NewElection, Election, UpdatedElection } from './IElection'; +const tableName = 'electiondb'; export default class ElectionsDB { - _postgresClient; - _tableName: string; + _tableName: string = tableName; - constructor(postgresClient: any) { + constructor(postgresClient: Kysely) { this._postgresClient = postgresClient; - this._tableName = "electionDB"; this.init() } async init(): Promise { var appInitContext = Logger.createContext("appInit"); Logger.debug(appInitContext, "-> ElectionsDB.init") - - //await this.dropTable(appInitContext); - - var query = ` - CREATE TABLE IF NOT EXISTS ${this._tableName} ( - election_id VARCHAR PRIMARY KEY, - title VARCHAR, - description TEXT, - frontend_url VARCHAR, - start_time VARCHAR, - end_time VARCHAR, - support_email VARCHAR, - owner_id VARCHAR, - audit_ids json, - admin_ids json, - credential_ids json, - state VARCHAR, - races json NOT NULL, - settings json, - auth_key VARCHAR - ); - `; - - Logger.debug(appInitContext, query); - var p = this._postgresClient.query(query); - return p.then((_: any) => { - //This will add the new field to the live DB in prod. Once that's done we can remove this - var credentialQuery = ` - ALTER TABLE ${this._tableName} ADD COLUMN IF NOT EXISTS auth_key VARCHAR - `; - return this._postgresClient.query(credentialQuery).catch((err: any) => { - console.log("err adding credential_ids column to DB: " + err.message); - return err; - }); - }).then((_: any) => { - return this; - }); + return this; } async dropTable(ctx: ILoggingContext): Promise { - var query = `DROP TABLE IF EXISTS ${this._tableName};`; - var p = this._postgresClient.query({ - text: query, - }); - return p.then((_: any) => { - Logger.debug(ctx, `Dropped it (like its hot)`); - }); + return this._postgresClient.schema.dropTable(tableName).execute() } createElection(election: Election, ctx: ILoggingContext, reason: string): Promise { - Logger.debug(ctx, `${className}.createElection`, election); - - var sqlString = `INSERT INTO ${this._tableName} (election_id, title,description,frontend_url,start_time,end_time,support_email,owner_id,audit_ids,admin_ids,credential_ids,state,races,settings,auth_key) - VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *;`; - Logger.debug(ctx, sqlString); - - var p = this._postgresClient.query({ - text: sqlString, - values: [ - election.election_id, - election.title, - election.description, - election.frontend_url, - election.start_time, - election.end_time, - election.support_email, - election.owner_id, - JSON.stringify(election.audit_ids), - JSON.stringify(election.admin_ids), - JSON.stringify(election.credential_ids), - election.state, - JSON.stringify(election.races), - JSON.stringify(election.settings), - election.auth_key - ] - }); + Logger.debug(ctx, `${tableName}.createElection`, election); + + const stringifiedElection: NewElection = { + ...election, + settings: JSON.stringify(election.settings), + races: JSON.stringify(election.races), + admin_ids: JSON.stringify(election.admin_ids), + audit_ids: JSON.stringify(election.audit_ids), + credential_ids: JSON.stringify(election.credential_ids), + } - return p.then((res: any) => { - const newElection = res.rows[0]; - Logger.state(ctx, `Election created:`, { reason: reason, election: newElection }); - return newElection; - }).catch((err: any) => { - Logger.error(ctx, "Error with postgres createElection: " + err.message); - throw err; - }); + const newElection = this._postgresClient + .insertInto(tableName) + .values(stringifiedElection) + .returningAll() + .executeTakeFirstOrThrow() + return newElection } updateElection(election: Election, ctx: ILoggingContext, reason: string): Promise { - Logger.debug(ctx, `${className}.updateElection`, election); - var sqlString = `UPDATE ${this._tableName} SET (title,description,frontend_url,start_time,end_time,support_email,owner_id,audit_ids,admin_ids,credential_ids,state,races,settings,auth_key) = - ($2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) WHERE election_id = $1 RETURNING *;`; - Logger.debug(ctx, sqlString); - - var p = this._postgresClient.query({ - text: sqlString, - values: [ - election.election_id, - election.title, - election.description, - election.frontend_url, - election.start_time, - election.end_time, - election.support_email, - election.owner_id, - JSON.stringify(election.audit_ids), - JSON.stringify(election.admin_ids), - JSON.stringify(election.credential_ids), - election.state, - JSON.stringify(election.races), - JSON.stringify(election.settings), - election.auth_key - ] - }); + Logger.debug(ctx, `${tableName}.updateElection`, election); + + const stringifiedElection: UpdatedElection = { + ...election, + settings: JSON.stringify(election.settings), + races: JSON.stringify(election.races), + admin_ids: JSON.stringify(election.admin_ids), + audit_ids: JSON.stringify(election.audit_ids), + credential_ids: JSON.stringify(election.credential_ids), + } - return p.then((res: any) => { - const updatedElection = res.rows[0]; - Logger.state(ctx, `Election Updated:`, { reason: reason, election: updatedElection }); - return updatedElection; - }); + const updatedElection = this._postgresClient + .updateTable(tableName) + .set(stringifiedElection) + .where('election_id', '=', election.election_id) + .returningAll() + .executeTakeFirstOrThrow() + + return updatedElection } async getOpenElections(ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.getOpenElections`); // Returns all elections where settings.voter_access == open and state == open // TODO: The filter is pretty inefficient for now since I don't think there's a way to include on settings.voter_access in the query - - // All elections with state=open - var sqlString = `SELECT * FROM ${this._tableName} WHERE state=$1`; - let values = ['open'] - - // Do Query - var elections = await this._postgresClient.query({ - text: sqlString, - values: values - }).then((response: any) => { - return (response.rows.length == 0)? [] as Election[] : response.rows; - }); - - // Filter for settings.voter_access = open - return elections.filter( (election : Election, index : any, array : any) => { + const openElections = await this._postgresClient + .selectFrom(tableName) + .where('state', '=', 'open') + .selectAll() + .execute() + + // // Filter for settings.voter_access = open + return openElections.filter((election: Election, index: any, array: any) => { return election.settings.voter_access == 'open'; }); } getElections(id: string, email: string, ctx: ILoggingContext): Promise { // When I filter in trello it adds "filter=member:arendpetercastelein,overdue:true" to the URL, I'm following the same pattern here - Logger.debug(ctx, `${className}.getAll ${id} ${email}`); + Logger.debug(ctx, `${tableName}.getAll ${id} ${email}`); - var sqlString = `SELECT * FROM ${this._tableName}`; - let values: any[] = [] + let querry = this._postgresClient + .selectFrom(tableName) + .selectAll() if (id !== '' || email !== '') { - sqlString += ' WHERE owner_id=$1 OR admin_ids::jsonb ? $2 OR audit_ids::jsonb ? $3 OR credential_ids::jsonb ? $4 ' - values = [id,email,email,email] + querry = querry.where(({ or, cmpr }) => + or([ + cmpr('owner_id', '=', id), + cmpr(sql`admin_ids::jsonb`, '?', email), + cmpr(sql`audit_ids::jsonb`, '?', email), + cmpr(sql`credential_ids::jsonb`, '?', email) + ])) } - Logger.debug(ctx, sqlString); + const elections = querry.execute() - var p = this._postgresClient.query({ - text: sqlString, - values: values - }); - return p.then((response: any) => { - var rows = response.rows; - if (rows.length == 0) { - console.log(".get null"); - return [] as Election[]; - } - return rows - }); + return elections } getElectionByID(election_id: Uid, ctx: ILoggingContext): Promise { - Logger.debug(ctx, `${className}.getElectionByID ${election_id}`); - var sqlString = `SELECT * FROM ${this._tableName} WHERE election_id = $1`; - Logger.debug(ctx, sqlString); + Logger.debug(ctx, `${tableName}.getElectionByID ${election_id}`); - var p = this._postgresClient.query({ - text: sqlString, - values: [election_id] - }); - return p.then((response: any) => { - var rows = response.rows; - if (rows.length == 0) { - Logger.debug(ctx, `.get null`); - return null; - } - return rows[0] as Election; - }); + const election = this._postgresClient + .selectFrom(tableName) + .where('election_id', '=', election_id) + .selectAll() + .executeTakeFirstOrThrow() + + return election } getElectionByIDs(election_ids: Uid[], ctx: ILoggingContext): Promise { - Logger.debug(ctx, `${className}.getElectionByIDs ${election_ids.join(',')}`); - var sqlString = `SELECT * FROM ${this._tableName} WHERE election_id IN ('${election_ids.join(',')}')`; - Logger.debug(ctx, sqlString); + Logger.debug(ctx, `${tableName}.getElectionByIDs ${election_ids.join(',')}`); - var p = this._postgresClient.query({ - text: sqlString, - values: [] - }); - return p.then((response: any) => { - var rows = response.rows; - if (rows.length == 0) { - Logger.debug(ctx, `.get null`); - return null; - } - return rows as Election[]; - }); + const elections = this._postgresClient + .selectFrom(tableName) + .where('election_id', 'in', election_ids) + .selectAll() + .execute() + + return elections } delete(election_id: Uid, ctx: ILoggingContext, reason: string): Promise { - Logger.debug(ctx, `${className}.delete ${election_id}`); - var sqlString = `DELETE FROM ${this._tableName} WHERE election_id = $1`; - Logger.debug(ctx, sqlString); - - var p = this._postgresClient.query({ - rowMode: 'array', - text: sqlString, - values: [election_id] - }); - return p.then((response: any) => { - if (response.rowCount == 1) { - Logger.state(ctx, `Election Deleted:`, { reason: reason, electionId: election_id }); - return true; + Logger.debug(ctx, `${tableName}.delete ${election_id}`); + + const deletedElection = this._postgresClient + .deleteFrom(tableName) + .where('election_id', '=', election_id) + .returningAll() + .executeTakeFirst() + + return deletedElection.then((election) => { + if (election) { + return true + } else { + return false } - return false; - }); + } + ) } } \ No newline at end of file diff --git a/backend/src/Models/IElection.ts b/backend/src/Models/IElection.ts new file mode 100644 index 00000000..78e7ee65 --- /dev/null +++ b/backend/src/Models/IElection.ts @@ -0,0 +1,26 @@ +import { ElectionSettings } from "../../../domain_model/ElectionSettings"; +import { Race } from "../../../domain_model/Race"; +import { Uid } from "../../../domain_model/Uid"; +import { ColumnType, Insertable, Selectable, Updateable } from 'kysely' + +export interface ElectionTable { + election_id: Uid; // identifier assigned by the system + title: string; // one-line election title + description?: string; // mark-up text describing the election + frontend_url: string; // base URL for the frontend + start_time?: Date | string; // when the election starts + end_time?: Date | string; // when the election ends + support_email?: string; // email available to voters to request support + owner_id: Uid; // user_id of owner of election + audit_ids?: ColumnType; // user_id of account with audit access + admin_ids?: ColumnType; // user_id of account with admin access + credential_ids?:ColumnType; // user_id of account with credentialling access + state: 'draft' | 'finalized' | 'open' | 'closed' | 'archived'; // State of election, In development, finalized, etc + races: ColumnType; // one or more race definitions + settings: ColumnType; + auth_key?: string; + } + + export type NewElection = Insertable + export type UpdatedElection = Updateable + export type Election = Selectable \ No newline at end of file diff --git a/backend/src/ServiceLocator.ts b/backend/src/ServiceLocator.ts index 6d2ad887..c230e061 100644 --- a/backend/src/ServiceLocator.ts +++ b/backend/src/ServiceLocator.ts @@ -10,31 +10,53 @@ import PGBossEventQueue from "./Services/EventQueue/PGBossEventQueue"; import AccountService from "./Services/Account/AccountService" import GlobalData from "./Services/GlobalData"; +import { Kysely, PostgresDialect } from 'kysely' +import { Database } from "./Models/Database"; + const { Pool } = require('pg'); -var _postgresClient:any; +var _postgresClient: any; +var _DB: Kysely var _appInitContext = Logger.createContext("appInit"); -var _ballotsDb:IBallotStore; -var _electionsDb:ElectionsDB; -var _electionRollDb:ElectionRollDB; -var _castVoteStore:CastVoteStore; -var _emailService:EmailService -var _eventQueue:IEventQueue; - -var _emailService:EmailService; -var _accountService:AccountService; -var _globalData:GlobalData; - -function postgres():any { - if (_postgresClient == null){ +var _ballotsDb: IBallotStore; +var _electionsDb: ElectionsDB; +var _electionRollDb: ElectionRollDB; +var _castVoteStore: CastVoteStore; +var _emailService: EmailService +var _eventQueue: IEventQueue; + +var _emailService: EmailService; +var _accountService: AccountService; +var _globalData: GlobalData; + + +function postgres(): any { + if (_postgresClient == null) { var connectionConfig = pgConnectionObject(); Logger.debug(_appInitContext, `Postgres Config: ${JSON.stringify(connectionConfig)}}`); - _postgresClient = new Pool(connectionConfig); + _postgresClient = new Pool(connectionConfig); + + const dialect = new PostgresDialect({ + pool: _postgresClient + }) + + _DB = new Kysely({ + dialect, + }) + } return _postgresClient; } -function pgConnectionObject():any { +function database(): Kysely { + console.log('starting database') + if (_DB == null){ + postgres() + } + return _DB +} + +function pgConnectionObject(): any { var connectionStr = pgConnectionString(); var devDB = process.env.DEV_DATABASE; if (devDB === 'TRUE') { @@ -51,12 +73,12 @@ function pgConnectionObject():any { }; } -function pgConnectionString():string { +function pgConnectionString(): string { return process.env.DATABASE_URL || 'postgresql://postgres:password@localhost:5432/postgres'; } -async function eventQueue():Promise { - if (_eventQueue == null){ +async function eventQueue(): Promise { + if (_eventQueue == null) { const eq = new PGBossEventQueue(); await eq.init(pgConnectionObject(), Logger.createContext("appInit")); _eventQueue = eq; @@ -65,55 +87,55 @@ async function eventQueue():Promise { return _eventQueue; } -function ballotsDb():IBallotStore { - if (_ballotsDb == null){ +function ballotsDb(): IBallotStore { + if (_ballotsDb == null) { _ballotsDb = new BallotsDB(postgres()); } return _ballotsDb; } -function electionsDb():ElectionsDB { - if (_electionsDb == null){ - _electionsDb = new ElectionsDB(postgres()); +function electionsDb(): ElectionsDB { + if (_electionsDb == null) { + _electionsDb = new ElectionsDB(database()); } return _electionsDb; } -function electionRollDb():ElectionRollDB { - if (_electionRollDb == null){ +function electionRollDb(): ElectionRollDB { + if (_electionRollDb == null) { _electionRollDb = new ElectionRollDB(postgres()); } return _electionRollDb; } -function castVoteStore():CastVoteStore { - if (_castVoteStore == null){ +function castVoteStore(): CastVoteStore { + if (_castVoteStore == null) { _castVoteStore = new CastVoteStore(postgres()); } return _castVoteStore; } -function emailService():EmailService { - if (_emailService == null){ +function emailService(): EmailService { + if (_emailService == null) { _emailService = new EmailService(); } return _emailService; } -function accountService():AccountService { - if (_accountService == null){ +function accountService(): AccountService { + if (_accountService == null) { _accountService = new AccountService(); } return _accountService; } -function globalData():GlobalData { - if (_globalData == null){ +function globalData(): GlobalData { + if (_globalData == null) { _globalData = new GlobalData(); } return _globalData; } -export default { ballotsDb, electionsDb, electionRollDb, emailService, accountService, castVoteStore, globalData, eventQueue}; +export default { ballotsDb, electionsDb, electionRollDb, emailService, accountService, castVoteStore, globalData, eventQueue, database }; diff --git a/backend/src/migrate-to-latest.ts b/backend/src/migrate-to-latest.ts new file mode 100644 index 00000000..a4ec0abd --- /dev/null +++ b/backend/src/migrate-to-latest.ts @@ -0,0 +1,46 @@ +require('dotenv').config() +import * as path from 'path' +import { Pool } from 'pg' +import { promises as fs } from 'fs' +import { + Kysely, + Migrator, + PostgresDialect, + FileMigrationProvider, +} from 'kysely' + +import servicelocator from './ServiceLocator' + +async function migrateToLatest() { + const db = servicelocator.database() + + const migrator = new Migrator({ + db, + provider: new FileMigrationProvider({ + fs, + path, + // This needs to be an absolute path. + migrationFolder: path.join(__dirname, './Migrations'), + }), + }) + + const { error, results } = await migrator.migrateToLatest() + + results?.forEach((it) => { + if (it.status === 'Success') { + console.log(`migration "${it.migrationName}" was executed successfully`) + } else if (it.status === 'Error') { + console.error(`failed to execute migration "${it.migrationName}"`) + } + }) + + if (error) { + console.error('failed to migrate') + console.error(error) + process.exit(1) + } + + await db.destroy() +} + +migrateToLatest() \ No newline at end of file diff --git a/domain_model/Election.ts b/domain_model/Election.ts index a929b5d1..5543c24b 100644 --- a/domain_model/Election.ts +++ b/domain_model/Election.ts @@ -2,24 +2,27 @@ import { ElectionRoll } from "./ElectionRoll"; import { ElectionSettings } from "./ElectionSettings"; import { Race } from "./Race"; import { Uid } from "./Uid"; +import { Election as IElection } from "../backend/src/Models/IElection"; -export interface Election { - election_id: Uid; // identifier assigned by the system - title: string; // one-line election title - description?: string; // mark-up text describing the election - frontend_url: string; // base URL for the frontend - start_time?: Date | string; // when the election starts - end_time?: Date | string; // when the election ends - support_email?: string; // email available to voters to request support - owner_id: Uid; // user_id of owner of election - audit_ids?: Uid[]; // user_id of account with audit access - admin_ids?: Uid[]; // user_id of account with admin access - credential_ids?:Uid[]; // user_id of account with credentialling access - state: 'draft' | 'finalized' | 'open' | 'closed' | 'archived'; // State of election, In development, finalized, etc - races: Race[]; // one or more race definitions - settings: ElectionSettings; - auth_key?: string; - } +// Temp forwarding of election type +export type Election = IElection + + // election_id: Uid; // identifier assigned by the system + // title: string; // one-line election title + // description?: string; // mark-up text describing the election + // frontend_url: string; // base URL for the frontend + // start_time?: Date | string; // when the election starts + // end_time?: Date | string; // when the election ends + // support_email?: string; // email available to voters to request support + // owner_id: Uid; // user_id of owner of election + // audit_ids?: Uid[]; // user_id of account with audit access + // admin_ids?: Uid[]; // user_id of account with admin access + // credential_ids?:Uid[]; // user_id of account with credentialling access + // state: 'draft' | 'finalized' | 'open' | 'closed' | 'archived'; // State of election, In development, finalized, etc + // races: Race[]; // one or more race definitions + // settings: ElectionSettings; + // auth_key?: string; + // } export function electionValidation(obj:Election): string | null { From a9ae1401332adb9bdd23cb12b6895dd61fc421c5 Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Wed, 5 Jul 2023 00:15:00 -0400 Subject: [PATCH 04/10] adding electionroll and stringify plugin --- backend/src/Migrations/2023_07_03_Initial.ts | 18 +- backend/src/Models/Database.ts | 4 +- backend/src/Models/ElectionRolls.ts | 239 ++++++++---------- backend/src/Models/Elections.ts | 1 + backend/src/Models/IElectionRoll.ts | 37 +++ backend/src/Models/IElectionRollStore.ts | 4 +- backend/src/ServiceLocator.ts | 2 +- .../serialize-parameters-plugin.ts | 139 ++++++++++ .../serialize-parameters-transformer.ts | 114 +++++++++ .../serialize-parameters.ts | 20 ++ domain_model/ElectionRoll.ts | 2 - 11 files changed, 441 insertions(+), 139 deletions(-) create mode 100644 backend/src/Models/IElectionRoll.ts create mode 100644 backend/src/serialize-parameters/serialize-parameters-plugin.ts create mode 100644 backend/src/serialize-parameters/serialize-parameters-transformer.ts create mode 100644 backend/src/serialize-parameters/serialize-parameters.ts diff --git a/backend/src/Migrations/2023_07_03_Initial.ts b/backend/src/Migrations/2023_07_03_Initial.ts index bcdf9c00..efbbca6a 100644 --- a/backend/src/Migrations/2023_07_03_Initial.ts +++ b/backend/src/Migrations/2023_07_03_Initial.ts @@ -19,10 +19,26 @@ export async function up(db: Kysely): Promise { .addColumn('settings', 'json') .addColumn('auth_key', 'varchar') .execute() + + await db.schema + .createTable('electionRollDB') + .addColumn('voter_id', 'varchar', (col) => col.primaryKey().notNull()) + .addColumn('election_id', 'varchar', (col) => col.notNull()) + .addColumn('email', 'varchar') + .addColumn('submitted', 'boolean', (col) => col.notNull()) + .addColumn('ballot_id', 'varchar') + .addColumn('ip_address', 'varchar') + .addColumn('address', 'varchar') + .addColumn('state', 'varchar', (col) => col.notNull()) + .addColumn('history', 'json') + .addColumn('registration', 'json') + .addColumn('precinct', 'varchar') + .addColumn('email_data', 'json') + .execute() } export async function down(db: Kysely): Promise { await db.schema.dropTable('electiondb').execute() - await db.schema.dropTable('person').execute() + await db.schema.dropTable('electionRollDB').execute() } diff --git a/backend/src/Models/Database.ts b/backend/src/Models/Database.ts index d2d0dacb..eebe1b7b 100644 --- a/backend/src/Models/Database.ts +++ b/backend/src/Models/Database.ts @@ -1,6 +1,8 @@ import { ElectionTable } from "./IElection"; +import { ElectionRollTable } from "./IElectionRoll"; export interface Database { - electiondb: ElectionTable + electiondb: ElectionTable, + electionRollDB: ElectionRollTable } \ No newline at end of file diff --git a/backend/src/Models/ElectionRolls.ts b/backend/src/Models/ElectionRolls.ts index 4601a841..c9f7d683 100644 --- a/backend/src/Models/ElectionRolls.ts +++ b/backend/src/Models/ElectionRolls.ts @@ -1,160 +1,124 @@ -import { ElectionRoll, ElectionRollAction } from '../../../domain_model/ElectionRoll'; +// import { ElectionRoll, ElectionRollAction } from '../../../domain_model/ElectionRoll'; import { ILoggingContext } from '../Services/Logging/ILogger'; import Logger from '../Services/Logging/Logger'; import { IElectionRollStore } from './IElectionRollStore'; var format = require('pg-format'); +import { Kysely, SqlBool, SqlBool, sql } from 'kysely' +import { NewElectionRoll, ElectionRoll, UpdatedElectionRoll } from './IElectionRoll'; +import { Database } from './Database'; +const tableName = 'electionRollDB'; -export default class ElectionRollDB implements IElectionRollStore{ +export default class ElectionRollDB implements IElectionRollStore { _postgresClient; - _tableName: string; + _tableName: string = tableName; - constructor(postgresClient:any) { + constructor(postgresClient: Kysely) { this._postgresClient = postgresClient; - this._tableName = "electionRollDB"; this.init() } async init(): Promise { var appInitContext = Logger.createContext("appInit"); Logger.debug(appInitContext, "-> ElectionRollDB.init"); - //await this.dropTable(appInitContext); - var query = ` - CREATE TABLE IF NOT EXISTS ${this._tableName} ( - voter_id VARCHAR NOT NULL PRIMARY KEY, - election_id VARCHAR NOT NULL, - email VARCHAR, - submitted BOOLEAN NOT NULL, - ballot_id VARCHAR, - ip_address VARCHAR, - address VARCHAR, - state VARCHAR NOT NULL, - history json, - registration json, - precinct VARCHAR, - email_data json - ); - `; - Logger.debug(appInitContext, query); - var p = this._postgresClient.query(query); - return p.then((_: any) => { - //removes pgboss archive, only use in dev - // var cleanupQuerry = ` - // DELETE FROM pgboss.archive`; - // return this._postgresClient.query(cleanupQuerry).catch((err:any) => { - // console.log("err cleaning up db: " + err.message); - // return err; - // }); - var credentialQuery = ` - ALTER TABLE ${this._tableName} ADD COLUMN IF NOT EXISTS email_data json - `; - return this._postgresClient.query(credentialQuery).catch((err: any) => { - console.log("err adding email_data column to DB: " + err.message); - return err; - }); - }).then((_:any)=> { - return this; - }); + return this; } - async dropTable(ctx:ILoggingContext):Promise{ - var query = `DROP TABLE IF EXISTS ${this._tableName};`; - var p = this._postgresClient.query({ - text: query, - }); - return p.then((_: any) => { - Logger.debug(ctx, `Dropped it (like its hot)`); - }); + async dropTable(ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.dropTable`); + return this._postgresClient.schema.dropTable(tableName).execute() } - - submitElectionRoll(electionRolls: ElectionRoll[], ctx:ILoggingContext,reason:string): Promise { - Logger.debug(ctx, `ElectionRollDB.submit`); - var values = electionRolls.map((electionRoll) => ([ - electionRoll.voter_id, - electionRoll.election_id, - electionRoll.email, - electionRoll.ip_address, - electionRoll.submitted, - electionRoll.state, - JSON.stringify(electionRoll.history), - JSON.stringify(electionRoll.registration), - electionRoll.precinct, - JSON.stringify(electionRoll.email_data)])) - var sqlString = format(`INSERT INTO ${this._tableName} (voter_id,election_id,email,ip_address,submitted,state,history,registration,precinct,email_data) - VALUES %L;`, values); - Logger.debug(ctx, sqlString) - Logger.debug(ctx, values) - var p = this._postgresClient.query(sqlString); - return p.then((res: any) => { - Logger.state(ctx, `Submit Election Roll: `, {reason: reason, electionRoll: electionRolls}); - return true; - }).catch((err:any)=>{ - Logger.error(ctx, `Error with postgres submitElectionRoll: ${err.message}`); - }); + + submitElectionRoll(electionRolls: ElectionRoll[], ctx: ILoggingContext, reason: string): Promise { + Logger.debug(ctx, `${tableName}.submit`); + + var stringifiedRolls: NewElectionRoll[] = electionRolls.map((electionRoll) => ( + stringifyRoll(electionRoll))) + + return this._postgresClient + .insertInto(tableName) + .values(stringifiedRolls) + .execute().then((res) => { return true }) } - getRollsByElectionID(election_id: string, ctx:ILoggingContext): Promise { - Logger.debug(ctx, `ElectionRollDB.getByElectionID`); - var sqlString = `SELECT * FROM ${this._tableName} WHERE election_id = $1`; - Logger.debug(ctx, sqlString); + getRollsByElectionID(election_id: string, ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.getByElectionID ${election_id}`); - var p = this._postgresClient.query({ - text: sqlString, - values: [election_id] - }); - return p.then((response: any) => { - const resRolls = response.rows; - Logger.debug(ctx, "", resRolls); - if (resRolls.length == 0) { - Logger.debug(ctx, ".get null"); - return []; - } - return resRolls - }); + return this._postgresClient + .selectFrom(tableName) + .where('election_id', '=', election_id) + .selectAll() + .execute() } - getByVoterID(election_id: string, voter_id: string, ctx:ILoggingContext): Promise { - Logger.debug(ctx, `ElectionRollDB.getByVoterID election:${election_id}, voter:${voter_id}`); + getByVoterID(election_id: string, voter_id: string, ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.getByVoterID election:${election_id}, voter:${voter_id}`); var sqlString = `SELECT * FROM ${this._tableName} WHERE election_id = $1 AND voter_id = $2`; Logger.debug(ctx, sqlString); - var p = this._postgresClient.query({ - text: sqlString, - values: [election_id, voter_id] - }); - return p.then((response: any) => { - var rows = response.rows; - Logger.debug(ctx, rows[0]) - if (rows.length == 0) { - Logger.debug(ctx, ".get null"); - return null; - } - return rows[0] - }); + return this._postgresClient + .selectFrom(tableName) + .where('election_id', '=', election_id) + .where('voter_id', '=', voter_id) + .selectAll() + .executeTakeFirstOrThrow() + .catch(((reason: any) => { + Logger.debug(ctx, reason); + return null + })) } - getByEmail(email: string, ctx:ILoggingContext): Promise { - Logger.debug(ctx, `ElectionRollDB.getByEmail email:${email}`); - var sqlString = `SELECT * FROM ${this._tableName} WHERE email = $1`; - Logger.debug(ctx, sqlString); - - var p = this._postgresClient.query({ - text: sqlString, - values: [email] - }); - return p.then((response: any) => { - var rows = response.rows; - Logger.debug(ctx, rows[0]) - if (rows.length == 0) { - Logger.debug(ctx, ".get null"); - return null; - } - return rows - }); + getByEmail(email: string, ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.getByEmail email:${email}`); + + return this._postgresClient + .selectFrom(tableName) + .where('email', '=', email) + .selectAll() + .execute() + .catch(((reason: any) => { + Logger.debug(ctx, reason); + return null + })) } - getElectionRoll(election_id: string, voter_id: string|null, email: string|null, ip_address: string|null, ctx:ILoggingContext): Promise<[ElectionRoll] | null> { - Logger.debug(ctx, `ElectionRollDB.get election:${election_id}, voter:${voter_id}`); + getElectionRoll(election_id: string, voter_id: string | null, email: string | null, ip_address: string | null, ctx: ILoggingContext): Promise<[ElectionRoll] | null> { + Logger.debug(ctx, `${tableName}.get election:${election_id}, voter:${voter_id}`); let sqlString = `SELECT * FROM ${this._tableName} WHERE election_id = $1 AND ( `; + + let query = this._postgresClient + .selectFrom(tableName) + .where(({ or, cmpr }) => + or( + ) + ) + + if (voter_id) { + values.push(voter_id) + sqlString += `voter_id = $${values.length}` + } + if (email) { + if (voter_id) { + sqlString += ' OR ' + } + values.push(email) + sqlString += `email = $${values.length}` + } + if (ip_address) { + if (voter_id || email) { + sqlString += ' OR ' + } + values.push(ip_address) + sqlString += `ip_address = $${values.length}` + } + + + return query.selectAll() + .execute() + .catch(((reason: any) => { + Logger.debug(ctx, reason); + return null + })) + let values = [election_id] if (voter_id) { values.push(voter_id) @@ -192,8 +156,8 @@ export default class ElectionRollDB implements IElectionRollStore{ }); } - update(election_roll: ElectionRoll, ctx:ILoggingContext, reason:string): Promise { - Logger.debug(ctx, `ElectionRollDB.updateRoll`); + update(election_roll: ElectionRoll, ctx: ILoggingContext, reason: string): Promise { + Logger.debug(ctx, `${tableName}.updateRoll`); var sqlString = `UPDATE ${this._tableName} SET ballot_id=$1, submitted=$2, state=$3, history=$4, registration=$5, email_data=$6 WHERE election_id = $7 AND voter_id=$8`; Logger.debug(ctx, sqlString); Logger.debug(ctx, "", election_roll) @@ -211,13 +175,13 @@ export default class ElectionRollDB implements IElectionRollStore{ return [] as ElectionRoll[]; } const newElectionRoll = rows; - Logger.state(ctx, `Update Election Roll: `, {reason: reason, electionRoll:newElectionRoll }); + Logger.state(ctx, `Update Election Roll: `, { reason: reason, electionRoll: newElectionRoll }); return newElectionRoll; }); } - delete(election_roll: ElectionRoll, ctx:ILoggingContext, reason:string): Promise { - Logger.debug(ctx, `ElectionRollDB.delete`); + delete(election_roll: ElectionRoll, ctx: ILoggingContext, reason: string): Promise { + Logger.debug(ctx, `${tableName}.delete`); var sqlString = `DELETE FROM ${this._tableName} WHERE election_id = $1 AND voter_id=$2`; Logger.debug(ctx, sqlString); @@ -228,10 +192,19 @@ export default class ElectionRollDB implements IElectionRollStore{ }); return p.then((response: any) => { if (response.rowCount == 1) { - Logger.state(ctx, `Delete ElectionRoll`, {reason:reason, electionId: election_roll.election_id}); + Logger.state(ctx, `Delete ElectionRoll`, { reason: reason, electionId: election_roll.election_id }); return true; } return false; }); } +} + +function stringifyRoll(electionRoll: ElectionRoll) { + return { + ...electionRoll, + history: JSON.stringify(electionRoll.history), + registration: JSON.stringify(electionRoll.registration), + email_data: JSON.stringify(electionRoll.email_data) + } } \ No newline at end of file diff --git a/backend/src/Models/Elections.ts b/backend/src/Models/Elections.ts index 90140a76..6bc25229 100644 --- a/backend/src/Models/Elections.ts +++ b/backend/src/Models/Elections.ts @@ -22,6 +22,7 @@ export default class ElectionsDB { } async dropTable(ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.dropTable`); return this._postgresClient.schema.dropTable(tableName).execute() } diff --git a/backend/src/Models/IElectionRoll.ts b/backend/src/Models/IElectionRoll.ts new file mode 100644 index 00000000..48db4520 --- /dev/null +++ b/backend/src/Models/IElectionRoll.ts @@ -0,0 +1,37 @@ +import { Uid } from "../../../domain_model/Uid"; +import { ColumnType, Insertable, Selectable, Updateable } from 'kysely' + +export interface ElectionRollTable { + voter_id: Uid; //Unique ID of voter who cast ballot + election_id: Uid; //ID of election ballot is cast in + email?: string; // Email address of voter + submitted: boolean; //has ballot been submitted + ballot_id?: Uid; //ID of ballot, unsure if this is needed + ip_address?: string; //IP Address of voter + address?: string; // Address of voter + state: ColumnType; //state of election roll + history?: ColumnType;// history of changes to election roll + registration?: any; //Registration data for voter + precinct?: string; // Precint of voter + email_data?: ColumnType<{ + inviteResponse?: any, + reminderResponse?: any, + }, string, string>; +} + +export interface ElectionRollAction { + action_type: string; + actor: Uid; + timestamp: number; +} + +export enum ElectionRollState { + approved = 'approved', + flagged = 'flagged', + registered = 'registered', + invalid = 'invalid' +} + +export type NewElectionRoll = Insertable +export type UpdatedElectionRoll = Updateable +export type ElectionRoll = Selectable \ No newline at end of file diff --git a/backend/src/Models/IElectionRollStore.ts b/backend/src/Models/IElectionRollStore.ts index ecd13213..51b81bbd 100644 --- a/backend/src/Models/IElectionRollStore.ts +++ b/backend/src/Models/IElectionRollStore.ts @@ -1,5 +1,7 @@ -import { ElectionRoll } from "../../../domain_model/ElectionRoll"; +// import { ElectionRoll } from "../../../domain_model/ElectionRoll"; + import { ILoggingContext } from "../Services/Logging/ILogger"; +import { ElectionRoll } from "./IElectionRoll"; export interface IElectionRollStore { submitElectionRoll: ( diff --git a/backend/src/ServiceLocator.ts b/backend/src/ServiceLocator.ts index c230e061..3acf338a 100644 --- a/backend/src/ServiceLocator.ts +++ b/backend/src/ServiceLocator.ts @@ -103,7 +103,7 @@ function electionsDb(): ElectionsDB { function electionRollDb(): ElectionRollDB { if (_electionRollDb == null) { - _electionRollDb = new ElectionRollDB(postgres()); + _electionRollDb = new ElectionRollDB(database()); } return _electionRollDb; } diff --git a/backend/src/serialize-parameters/serialize-parameters-plugin.ts b/backend/src/serialize-parameters/serialize-parameters-plugin.ts new file mode 100644 index 00000000..ff3ceff9 --- /dev/null +++ b/backend/src/serialize-parameters/serialize-parameters-plugin.ts @@ -0,0 +1,139 @@ +import { + KyselyPlugin, + PluginTransformQueryArgs, + PluginTransformResultArgs, + UnknownRow, + RootOperationNode, + QueryResult +} from 'kysely' +import { SerializeParametersTransformer } from './serialize-parameters-transformer.js' +import { Caster, Serializer } from './serialize-parameters.js' + +export interface SerializeParametersPluginOptions { + /** + * Function responsible for casting of serialized parameters. + * + * E.g. Postgres `::jsonb` casting of parameters in sql query. + */ + caster?: Caster + /** + * Function responsible for serialization of parameters. + * + * Defaults to `JSON.stringify` of objects and arrays. + */ + serializer?: Serializer +} + +/** + * A plugin that serializes query parameters so you don't have to. + * + * The following example will return an error when using Postgres or Mysql dialects, unless using this plugin: + * + * ```ts + * interface Person { + * firstName: string + * lastName: string + * tags: string[] // json or jsonb data type in database + * } + * + * interface Database { + * person: Person + * } + * + * const db = new Kysely({ + * dialect: new PostgresDialect({ + * database: 'kysel_test', + * host: 'localhost', + * }), + * plugins: [ + * new SerializeParametersPlugin(), + * ], + * }) + * + * await db.insertInto('person') + * .values([{ + * firstName: 'Jennifer', + * lastName: 'Aniston', + * tags: ['celebrity', 'actress'], + * }]) + * .execute() + * ``` + * + * + * You can also provide a custom serializer function: + * + * ```ts + * const db = new Kysely({ + * dialect: new PostgresDialect({ + * database: 'kysel_test', + * host: 'localhost', + * }), + * plugins: [ + * new SerializeParametersPlugin({ + * serializer: (value) => { + * if (value instanceof Date) { + * return formatDatetime(value) + * } + * + * if (value !== null && typeof value === 'object') { + * return JSON.stringify(value) + * } + * + * return value + * } + * }), + * ], + * }) + * ``` + * + * + * Casting serialized parameters is also supported: + * + * ```ts + * const db = new Kysely({ + * dialect: new PostgresDialect({ + * database: 'kysel_test', + * host: 'localhost', + * }), + * plugins: [ + * new SerializeParametersPlugin({ + * caster: (serializedValue) => sql`${serializedValue}::jsonb` + * }), + * ], + * }) + * + * await db.insertInto('person') + * .values([{ + * firstName: 'Jennifer', + * lastName: 'Aniston', + * tags: ['celebrity', 'actress'], + * }]) + * .execute() + * ``` + * + * Compiled sql query (Postgres): + * + * ```sql + * insert into "person" ("firstName", "lastName", "tags") values ($1, $2, $3::jsonb) + * ``` + */ +export class SerializeParametersPlugin implements KyselyPlugin { + readonly #serializeParametersTransformer: SerializeParametersTransformer + + constructor(opt: SerializeParametersPluginOptions = {}) { + this.#serializeParametersTransformer = new SerializeParametersTransformer( + opt.serializer, + opt.caster + ) + } + + transformQuery(args: PluginTransformQueryArgs): RootOperationNode { + return this.#serializeParametersTransformer.transformNode(args.node) + } + + async transformResult( + args: PluginTransformResultArgs + ): Promise> { + return args.result + } +} \ No newline at end of file diff --git a/backend/src/serialize-parameters/serialize-parameters-transformer.ts b/backend/src/serialize-parameters/serialize-parameters-transformer.ts new file mode 100644 index 00000000..9f89f681 --- /dev/null +++ b/backend/src/serialize-parameters/serialize-parameters-transformer.ts @@ -0,0 +1,114 @@ +import { + ColumnUpdateNode, + OperationNodeTransformer, + OperatorNode, + PrimitiveValueListNode, + ValueListNode, + ValueNode, + ValuesNode } from 'kysely' +import { + Caster, + defaultSerializer, + Serializer, +} from './serialize-parameters.js' + +export class SerializeParametersTransformer extends OperationNodeTransformer { + readonly #caster: Caster | undefined + readonly #serializer: Serializer + + constructor(serializer: Serializer | undefined, caster: Caster | undefined) { + super() + this.#caster = caster + this.#serializer = serializer || defaultSerializer + } + + protected override transformValues(node: ValuesNode): ValuesNode { + if (!this.#caster) { + return super.transformValues(node) + } + + return super.transformValues({ + ...node, + values: node.values.map((valueItemNode) => { + if (valueItemNode.kind !== 'PrimitiveValueListNode') { + return valueItemNode + } + + return { + kind: 'ValueListNode', + values: valueItemNode.values.map( + (value) => + ({ + kind: 'ValueNode', + value, + } as ValueNode) + ), + } as ValueListNode + }), + }) + } + + protected override transformValueList(node: ValueListNode): ValueListNode { + if (!this.#caster) { + return super.transformValueList(node) + } + + return super.transformValueList({ + ...node, + values: node.values.map((listNodeItem) => { + + if (listNodeItem.kind !== 'ValueNode') { + return listNodeItem + } + return listNodeItem + // const { value } = listNodeItem + + // const serializedValue = this.#serializer(value) + + // if (value === serializedValue) { + // return listNodeItem + // } + + // return this.#caster!(serializedValue, value).toOperationNode() + }), + }) + } + + protected override transformPrimitiveValueList( + node: PrimitiveValueListNode + ): PrimitiveValueListNode { + return { + ...node, + values: node.values.map(this.#serializer), + } + } + + protected transformColumnUpdate(node: ColumnUpdateNode): ColumnUpdateNode { + const { value: valueNode } = node + if (!this.#caster || valueNode.kind !== 'ValueNode') { + return super.transformColumnUpdate(node) + } + + return super.transformColumnUpdate(node) + + // const { value } = valueNode + + // const serializedValue = this.#serializer(value) + + // if (value === serializedValue) { + // return super.transformColumnUpdate(node) + // } + + // return super.transformColumnUpdate({ + // ...node, + // value: this.#caster(serializedValue, value).toOperationNode(), + // }) + } + + protected override transformValue(node: ValueNode): ValueNode { + return { + ...node, + value: this.#serializer(node.value), + } + } +} \ No newline at end of file diff --git a/backend/src/serialize-parameters/serialize-parameters.ts b/backend/src/serialize-parameters/serialize-parameters.ts new file mode 100644 index 00000000..7b1bb910 --- /dev/null +++ b/backend/src/serialize-parameters/serialize-parameters.ts @@ -0,0 +1,20 @@ +import { ColumnDataType, RawBuilder, sql } from 'kysely' + +export type Caster = ( + serializedValue: unknown, + value: unknown +) => RawBuilder +export type Serializer = (parameter: unknown) => unknown + +export const createDefaultPostgresCaster: (castTo: ColumnDataType) => Caster = + (castTo = 'jsonb') => + (serializedValue) => + sql`${serializedValue}::${sql.raw(castTo)}` + +export const defaultSerializer: Serializer = (parameter) => { + if (parameter && typeof parameter === 'object') { + return JSON.stringify(parameter) + } + + return parameter +} \ No newline at end of file diff --git a/domain_model/ElectionRoll.ts b/domain_model/ElectionRoll.ts index ceb7b7a1..3ba737a3 100644 --- a/domain_model/ElectionRoll.ts +++ b/domain_model/ElectionRoll.ts @@ -25,8 +25,6 @@ export interface ElectionRollAction { timestamp:number; } -export const ElectionStates = {} - export enum ElectionRollState { approved= 'approved', flagged = 'flagged', From 3d98274a5663f996a88600efbda4d33c8c077061 Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Wed, 5 Jul 2023 01:21:53 -0400 Subject: [PATCH 05/10] finishing other election roll queries --- backend/src/Models/ElectionRolls.ts | 136 +++++++---------------- backend/src/Models/IElectionRollStore.ts | 2 +- 2 files changed, 42 insertions(+), 96 deletions(-) diff --git a/backend/src/Models/ElectionRolls.ts b/backend/src/Models/ElectionRolls.ts index c9f7d683..af6b7558 100644 --- a/backend/src/Models/ElectionRolls.ts +++ b/backend/src/Models/ElectionRolls.ts @@ -2,8 +2,7 @@ import { ILoggingContext } from '../Services/Logging/ILogger'; import Logger from '../Services/Logging/Logger'; import { IElectionRollStore } from './IElectionRollStore'; -var format = require('pg-format'); -import { Kysely, SqlBool, SqlBool, sql } from 'kysely' +import { Expression, Kysely, SqlBool, sql } from 'kysely' import { NewElectionRoll, ElectionRoll, UpdatedElectionRoll } from './IElectionRoll'; import { Database } from './Database'; const tableName = 'electionRollDB'; @@ -53,8 +52,6 @@ export default class ElectionRollDB implements IElectionRollStore { getByVoterID(election_id: string, voter_id: string, ctx: ILoggingContext): Promise { Logger.debug(ctx, `${tableName}.getByVoterID election:${election_id}, voter:${voter_id}`); - var sqlString = `SELECT * FROM ${this._tableName} WHERE election_id = $1 AND voter_id = $2`; - Logger.debug(ctx, sqlString); return this._postgresClient .selectFrom(tableName) @@ -81,122 +78,71 @@ export default class ElectionRollDB implements IElectionRollStore { })) } - getElectionRoll(election_id: string, voter_id: string | null, email: string | null, ip_address: string | null, ctx: ILoggingContext): Promise<[ElectionRoll] | null> { + getElectionRoll(election_id: string, voter_id: string | null, email: string | null, ip_address: string | null, ctx: ILoggingContext): Promise { Logger.debug(ctx, `${tableName}.get election:${election_id}, voter:${voter_id}`); - let sqlString = `SELECT * FROM ${this._tableName} WHERE election_id = $1 AND ( `; - let query = this._postgresClient + return this._postgresClient .selectFrom(tableName) - .where(({ or, cmpr }) => - or( - ) - ) - - if (voter_id) { - values.push(voter_id) - sqlString += `voter_id = $${values.length}` - } - if (email) { - if (voter_id) { - sqlString += ' OR ' - } - values.push(email) - sqlString += `email = $${values.length}` - } - if (ip_address) { - if (voter_id || email) { - sqlString += ' OR ' - } - values.push(ip_address) - sqlString += `ip_address = $${values.length}` - } + .where((eb) => { + const ors: Expression[] = [] + + if (voter_id) { + ors.push(eb.cmpr('voter_id', '=', voter_id)) + } + + if (email) { + ors.push(eb.cmpr('email', '=', email)) + } + if (ip_address) { + ors.push(eb.cmpr('ip_address', '=', ip_address)) + } - return query.selectAll() + return eb.or(ors) + }) + .selectAll() .execute() .catch(((reason: any) => { Logger.debug(ctx, reason); return null })) - - let values = [election_id] - if (voter_id) { - values.push(voter_id) - sqlString += `voter_id = $${values.length}` - } - if (email) { - if (voter_id) { - sqlString += ' OR ' - } - values.push(email) - sqlString += `email = $${values.length}` - } - if (ip_address) { - if (voter_id || email) { - sqlString += ' OR ' - } - values.push(ip_address) - sqlString += `ip_address = $${values.length}` - } - sqlString += ')' - Logger.debug(ctx, sqlString); - - var p = this._postgresClient.query({ - text: sqlString, - values: values - }); - return p.then((response: any) => { - var rows = response.rows; - Logger.debug(ctx, rows[0]) - if (rows.length == 0) { - Logger.debug(ctx, ".get null"); - return null; - } - return rows - }); } update(election_roll: ElectionRoll, ctx: ILoggingContext, reason: string): Promise { Logger.debug(ctx, `${tableName}.updateRoll`); - var sqlString = `UPDATE ${this._tableName} SET ballot_id=$1, submitted=$2, state=$3, history=$4, registration=$5, email_data=$6 WHERE election_id = $7 AND voter_id=$8`; - Logger.debug(ctx, sqlString); Logger.debug(ctx, "", election_roll) - var p = this._postgresClient.query({ - text: sqlString, - - values: [election_roll.ballot_id, election_roll.submitted, election_roll.state, JSON.stringify(election_roll.history), JSON.stringify(election_roll.registration), JSON.stringify(election_roll.email_data), election_roll.election_id, election_roll.voter_id] - }); - return p.then((response: any) => { - var rows = response.rows; - Logger.debug(ctx, "", response); - if (rows.length == 0) { + return this._postgresClient + .updateTable(tableName) + .where('election_id', '=', election_roll.election_id) + .where('voter_id', '=', election_roll.voter_id) + .set(stringifyRoll(election_roll)) + .returningAll() + .executeTakeFirstOrThrow() + .catch((reason: any) => { Logger.debug(ctx, ".get null"); - return [] as ElectionRoll[]; - } - const newElectionRoll = rows; - Logger.state(ctx, `Update Election Roll: `, { reason: reason, electionRoll: newElectionRoll }); - return newElectionRoll; - }); + return null; + }) } delete(election_roll: ElectionRoll, ctx: ILoggingContext, reason: string): Promise { Logger.debug(ctx, `${tableName}.delete`); var sqlString = `DELETE FROM ${this._tableName} WHERE election_id = $1 AND voter_id=$2`; Logger.debug(ctx, sqlString); + let deletedRoll = this._postgresClient + .deleteFrom(tableName) + .where('election_id', '=', election_roll.election_id) + .where('voter_id', '=', election_roll.voter_id) + .returningAll() + .execute() - var p = this._postgresClient.query({ - rowMode: 'array', - text: sqlString, - values: [election_roll.election_id, election_roll.voter_id] - }); - return p.then((response: any) => { - if (response.rowCount == 1) { - Logger.state(ctx, `Delete ElectionRoll`, { reason: reason, electionId: election_roll.election_id }); - return true; + return deletedRoll.then((roll) => { + if (roll) { + return true + } else { + return false } - return false; - }); + }) } } diff --git a/backend/src/Models/IElectionRollStore.ts b/backend/src/Models/IElectionRollStore.ts index 51b81bbd..f95aa226 100644 --- a/backend/src/Models/IElectionRollStore.ts +++ b/backend/src/Models/IElectionRollStore.ts @@ -24,7 +24,7 @@ export interface IElectionRollStore { email: string|null, ip_address: string|null, ctx:ILoggingContext - ) => Promise<[ElectionRoll] | null>; + ) => Promise; update: ( election_roll: ElectionRoll, ctx: ILoggingContext, From 653f91ebde7ac379aec8180cf996454e3d01ab0c Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:40:32 -0400 Subject: [PATCH 06/10] moving plugin, fixing bug --- .../serialize-parameters-plugin.ts | 0 .../serialize-parameters-transformer.ts | 50 +++++++++---------- .../serialize-parameters.ts | 0 3 files changed, 25 insertions(+), 25 deletions(-) rename backend/src/{ => Models}/serialize-parameters/serialize-parameters-plugin.ts (100%) rename backend/src/{ => Models}/serialize-parameters/serialize-parameters-transformer.ts (71%) rename backend/src/{ => Models}/serialize-parameters/serialize-parameters.ts (100%) diff --git a/backend/src/serialize-parameters/serialize-parameters-plugin.ts b/backend/src/Models/serialize-parameters/serialize-parameters-plugin.ts similarity index 100% rename from backend/src/serialize-parameters/serialize-parameters-plugin.ts rename to backend/src/Models/serialize-parameters/serialize-parameters-plugin.ts diff --git a/backend/src/serialize-parameters/serialize-parameters-transformer.ts b/backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts similarity index 71% rename from backend/src/serialize-parameters/serialize-parameters-transformer.ts rename to backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts index 9f89f681..5da0766b 100644 --- a/backend/src/serialize-parameters/serialize-parameters-transformer.ts +++ b/backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts @@ -1,11 +1,13 @@ -import { +import { ColumnUpdateNode, OperationNodeTransformer, OperatorNode, PrimitiveValueListNode, ValueListNode, ValueNode, - ValuesNode } from 'kysely' + ValuesNode, + OperationNode +} from 'kysely' import { Caster, defaultSerializer, @@ -38,10 +40,10 @@ export class SerializeParametersTransformer extends OperationNodeTransformer { kind: 'ValueListNode', values: valueItemNode.values.map( (value) => - ({ - kind: 'ValueNode', - value, - } as ValueNode) + ({ + kind: 'ValueNode', + value, + } as ValueNode) ), } as ValueListNode }), @@ -56,20 +58,20 @@ export class SerializeParametersTransformer extends OperationNodeTransformer { return super.transformValueList({ ...node, values: node.values.map((listNodeItem) => { - + if (listNodeItem.kind !== 'ValueNode') { return listNodeItem } - return listNodeItem - // const { value } = listNodeItem - // const serializedValue = this.#serializer(value) + const { value, ...item } = listNodeItem as ValueNode + + const serializedValue = this.#serializer(value) - // if (value === serializedValue) { - // return listNodeItem - // } + if (value === serializedValue) { + return listNodeItem + } - // return this.#caster!(serializedValue, value).toOperationNode() + return this.#caster!(serializedValue, value).toOperationNode() }), }) } @@ -89,20 +91,18 @@ export class SerializeParametersTransformer extends OperationNodeTransformer { return super.transformColumnUpdate(node) } - return super.transformColumnUpdate(node) + const { value, ...item } = valueNode as ValueNode - // const { value } = valueNode + const serializedValue = this.#serializer(value) - // const serializedValue = this.#serializer(value) - - // if (value === serializedValue) { - // return super.transformColumnUpdate(node) - // } + if (value === serializedValue) { + return super.transformColumnUpdate(node) + } - // return super.transformColumnUpdate({ - // ...node, - // value: this.#caster(serializedValue, value).toOperationNode(), - // }) + return super.transformColumnUpdate({ + ...node, + value: this.#caster(serializedValue, value).toOperationNode(), + }) } protected override transformValue(node: ValueNode): ValueNode { diff --git a/backend/src/serialize-parameters/serialize-parameters.ts b/backend/src/Models/serialize-parameters/serialize-parameters.ts similarity index 100% rename from backend/src/serialize-parameters/serialize-parameters.ts rename to backend/src/Models/serialize-parameters/serialize-parameters.ts From 72ccd81daccb6649e06ca5a36851b3c7f9f79dc3 Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Wed, 5 Jul 2023 17:21:23 -0400 Subject: [PATCH 07/10] Implement plugin to auto stringify objects, revert other changes --- backend/src/Models/Database.ts | 9 +++--- backend/src/Models/ElectionRolls.ts | 26 ++++++----------- backend/src/Models/Elections.ts | 24 ++------------- backend/src/Models/IElection.ts | 26 ----------------- backend/src/Models/IElectionRoll.ts | 37 ------------------------ backend/src/Models/IElectionRollStore.ts | 4 +-- backend/src/ServiceLocator.ts | 13 ++++++++- domain_model/Election.ts | 37 +++++++++++------------- 8 files changed, 46 insertions(+), 130 deletions(-) delete mode 100644 backend/src/Models/IElection.ts delete mode 100644 backend/src/Models/IElectionRoll.ts diff --git a/backend/src/Models/Database.ts b/backend/src/Models/Database.ts index eebe1b7b..95bbc417 100644 --- a/backend/src/Models/Database.ts +++ b/backend/src/Models/Database.ts @@ -1,8 +1,7 @@ -import { ElectionTable } from "./IElection"; -import { ElectionRollTable } from "./IElectionRoll"; - +import { Election } from "../../../domain_model/Election"; +import { ElectionRoll } from "../../../domain_model/ElectionRoll"; export interface Database { - electiondb: ElectionTable, - electionRollDB: ElectionRollTable + electiondb: Election, + electionRollDB: ElectionRoll } \ No newline at end of file diff --git a/backend/src/Models/ElectionRolls.ts b/backend/src/Models/ElectionRolls.ts index af6b7558..df213100 100644 --- a/backend/src/Models/ElectionRolls.ts +++ b/backend/src/Models/ElectionRolls.ts @@ -1,10 +1,9 @@ -// import { ElectionRoll, ElectionRollAction } from '../../../domain_model/ElectionRoll'; import { ILoggingContext } from '../Services/Logging/ILogger'; import Logger from '../Services/Logging/Logger'; import { IElectionRollStore } from './IElectionRollStore'; -import { Expression, Kysely, SqlBool, sql } from 'kysely' -import { NewElectionRoll, ElectionRoll, UpdatedElectionRoll } from './IElectionRoll'; +import { Expression, Kysely } from 'kysely' import { Database } from './Database'; +import { ElectionRoll } from '../../../domain_model/ElectionRoll'; const tableName = 'electionRollDB'; export default class ElectionRollDB implements IElectionRollStore { @@ -31,12 +30,9 @@ export default class ElectionRollDB implements IElectionRollStore { submitElectionRoll(electionRolls: ElectionRoll[], ctx: ILoggingContext, reason: string): Promise { Logger.debug(ctx, `${tableName}.submit`); - var stringifiedRolls: NewElectionRoll[] = electionRolls.map((electionRoll) => ( - stringifyRoll(electionRoll))) - return this._postgresClient .insertInto(tableName) - .values(stringifiedRolls) + .values(electionRolls) .execute().then((res) => { return true }) } @@ -102,7 +98,12 @@ export default class ElectionRollDB implements IElectionRollStore { }) .selectAll() .execute() + .then((rolls) => { + if (rolls.length == 0) return null + return rolls + }) .catch(((reason: any) => { + console.log('aaaaahhhhhh') Logger.debug(ctx, reason); return null })) @@ -116,7 +117,7 @@ export default class ElectionRollDB implements IElectionRollStore { .updateTable(tableName) .where('election_id', '=', election_roll.election_id) .where('voter_id', '=', election_roll.voter_id) - .set(stringifyRoll(election_roll)) + .set(election_roll) .returningAll() .executeTakeFirstOrThrow() .catch((reason: any) => { @@ -144,13 +145,4 @@ export default class ElectionRollDB implements IElectionRollStore { } }) } -} - -function stringifyRoll(electionRoll: ElectionRoll) { - return { - ...electionRoll, - history: JSON.stringify(electionRoll.history), - registration: JSON.stringify(electionRoll.registration), - email_data: JSON.stringify(electionRoll.email_data) - } } \ No newline at end of file diff --git a/backend/src/Models/Elections.ts b/backend/src/Models/Elections.ts index 6bc25229..e5a490b9 100644 --- a/backend/src/Models/Elections.ts +++ b/backend/src/Models/Elections.ts @@ -3,7 +3,7 @@ import { Database } from './Database'; import { ILoggingContext } from '../Services/Logging/ILogger'; import Logger from '../Services/Logging/Logger'; import { Kysely, sql } from 'kysely' -import { NewElection, Election, UpdatedElection } from './IElection'; +import { Election } from '../../../domain_model/Election'; const tableName = 'electiondb'; export default class ElectionsDB { @@ -29,18 +29,9 @@ export default class ElectionsDB { createElection(election: Election, ctx: ILoggingContext, reason: string): Promise { Logger.debug(ctx, `${tableName}.createElection`, election); - const stringifiedElection: NewElection = { - ...election, - settings: JSON.stringify(election.settings), - races: JSON.stringify(election.races), - admin_ids: JSON.stringify(election.admin_ids), - audit_ids: JSON.stringify(election.audit_ids), - credential_ids: JSON.stringify(election.credential_ids), - } - const newElection = this._postgresClient .insertInto(tableName) - .values(stringifiedElection) + .values(election) .returningAll() .executeTakeFirstOrThrow() return newElection @@ -49,18 +40,9 @@ export default class ElectionsDB { updateElection(election: Election, ctx: ILoggingContext, reason: string): Promise { Logger.debug(ctx, `${tableName}.updateElection`, election); - const stringifiedElection: UpdatedElection = { - ...election, - settings: JSON.stringify(election.settings), - races: JSON.stringify(election.races), - admin_ids: JSON.stringify(election.admin_ids), - audit_ids: JSON.stringify(election.audit_ids), - credential_ids: JSON.stringify(election.credential_ids), - } - const updatedElection = this._postgresClient .updateTable(tableName) - .set(stringifiedElection) + .set(election) .where('election_id', '=', election.election_id) .returningAll() .executeTakeFirstOrThrow() diff --git a/backend/src/Models/IElection.ts b/backend/src/Models/IElection.ts deleted file mode 100644 index 78e7ee65..00000000 --- a/backend/src/Models/IElection.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ElectionSettings } from "../../../domain_model/ElectionSettings"; -import { Race } from "../../../domain_model/Race"; -import { Uid } from "../../../domain_model/Uid"; -import { ColumnType, Insertable, Selectable, Updateable } from 'kysely' - -export interface ElectionTable { - election_id: Uid; // identifier assigned by the system - title: string; // one-line election title - description?: string; // mark-up text describing the election - frontend_url: string; // base URL for the frontend - start_time?: Date | string; // when the election starts - end_time?: Date | string; // when the election ends - support_email?: string; // email available to voters to request support - owner_id: Uid; // user_id of owner of election - audit_ids?: ColumnType; // user_id of account with audit access - admin_ids?: ColumnType; // user_id of account with admin access - credential_ids?:ColumnType; // user_id of account with credentialling access - state: 'draft' | 'finalized' | 'open' | 'closed' | 'archived'; // State of election, In development, finalized, etc - races: ColumnType; // one or more race definitions - settings: ColumnType; - auth_key?: string; - } - - export type NewElection = Insertable - export type UpdatedElection = Updateable - export type Election = Selectable \ No newline at end of file diff --git a/backend/src/Models/IElectionRoll.ts b/backend/src/Models/IElectionRoll.ts deleted file mode 100644 index 48db4520..00000000 --- a/backend/src/Models/IElectionRoll.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Uid } from "../../../domain_model/Uid"; -import { ColumnType, Insertable, Selectable, Updateable } from 'kysely' - -export interface ElectionRollTable { - voter_id: Uid; //Unique ID of voter who cast ballot - election_id: Uid; //ID of election ballot is cast in - email?: string; // Email address of voter - submitted: boolean; //has ballot been submitted - ballot_id?: Uid; //ID of ballot, unsure if this is needed - ip_address?: string; //IP Address of voter - address?: string; // Address of voter - state: ColumnType; //state of election roll - history?: ColumnType;// history of changes to election roll - registration?: any; //Registration data for voter - precinct?: string; // Precint of voter - email_data?: ColumnType<{ - inviteResponse?: any, - reminderResponse?: any, - }, string, string>; -} - -export interface ElectionRollAction { - action_type: string; - actor: Uid; - timestamp: number; -} - -export enum ElectionRollState { - approved = 'approved', - flagged = 'flagged', - registered = 'registered', - invalid = 'invalid' -} - -export type NewElectionRoll = Insertable -export type UpdatedElectionRoll = Updateable -export type ElectionRoll = Selectable \ No newline at end of file diff --git a/backend/src/Models/IElectionRollStore.ts b/backend/src/Models/IElectionRollStore.ts index f95aa226..5e2e7dd3 100644 --- a/backend/src/Models/IElectionRollStore.ts +++ b/backend/src/Models/IElectionRollStore.ts @@ -1,7 +1,5 @@ -// import { ElectionRoll } from "../../../domain_model/ElectionRoll"; - +import { ElectionRoll } from "../../../domain_model/ElectionRoll"; import { ILoggingContext } from "../Services/Logging/ILogger"; -import { ElectionRoll } from "./IElectionRoll"; export interface IElectionRollStore { submitElectionRoll: ( diff --git a/backend/src/ServiceLocator.ts b/backend/src/ServiceLocator.ts index 3acf338a..2e47c88d 100644 --- a/backend/src/ServiceLocator.ts +++ b/backend/src/ServiceLocator.ts @@ -12,6 +12,7 @@ import AccountService from "./Services/Account/AccountService" import GlobalData from "./Services/GlobalData"; import { Kysely, PostgresDialect } from 'kysely' import { Database } from "./Models/Database"; +import { SerializeParametersPlugin } from "./Models/serialize-parameters/serialize-parameters-plugin"; const { Pool } = require('pg'); @@ -42,6 +43,16 @@ function postgres(): any { _DB = new Kysely({ dialect, + plugins: [ + new SerializeParametersPlugin({ + serializer: (value) => { + if (value !== null && typeof value === 'object') { + return JSON.stringify(value) + } + return value + } + }), + ], }) } @@ -50,7 +61,7 @@ function postgres(): any { function database(): Kysely { console.log('starting database') - if (_DB == null){ + if (_DB == null) { postgres() } return _DB diff --git a/domain_model/Election.ts b/domain_model/Election.ts index 5543c24b..a929b5d1 100644 --- a/domain_model/Election.ts +++ b/domain_model/Election.ts @@ -2,27 +2,24 @@ import { ElectionRoll } from "./ElectionRoll"; import { ElectionSettings } from "./ElectionSettings"; import { Race } from "./Race"; import { Uid } from "./Uid"; -import { Election as IElection } from "../backend/src/Models/IElection"; -// Temp forwarding of election type -export type Election = IElection - - // election_id: Uid; // identifier assigned by the system - // title: string; // one-line election title - // description?: string; // mark-up text describing the election - // frontend_url: string; // base URL for the frontend - // start_time?: Date | string; // when the election starts - // end_time?: Date | string; // when the election ends - // support_email?: string; // email available to voters to request support - // owner_id: Uid; // user_id of owner of election - // audit_ids?: Uid[]; // user_id of account with audit access - // admin_ids?: Uid[]; // user_id of account with admin access - // credential_ids?:Uid[]; // user_id of account with credentialling access - // state: 'draft' | 'finalized' | 'open' | 'closed' | 'archived'; // State of election, In development, finalized, etc - // races: Race[]; // one or more race definitions - // settings: ElectionSettings; - // auth_key?: string; - // } +export interface Election { + election_id: Uid; // identifier assigned by the system + title: string; // one-line election title + description?: string; // mark-up text describing the election + frontend_url: string; // base URL for the frontend + start_time?: Date | string; // when the election starts + end_time?: Date | string; // when the election ends + support_email?: string; // email available to voters to request support + owner_id: Uid; // user_id of owner of election + audit_ids?: Uid[]; // user_id of account with audit access + admin_ids?: Uid[]; // user_id of account with admin access + credential_ids?:Uid[]; // user_id of account with credentialling access + state: 'draft' | 'finalized' | 'open' | 'closed' | 'archived'; // State of election, In development, finalized, etc + races: Race[]; // one or more race definitions + settings: ElectionSettings; + auth_key?: string; + } export function electionValidation(obj:Election): string | null { From 1b8ccbf785bb0ab80b6d2ffa165d31dfe1cced7e Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Wed, 5 Jul 2023 17:32:49 -0400 Subject: [PATCH 08/10] Fixing election db table name --- backend/src/Migrations/2023_07_03_Initial.ts | 2 +- backend/src/Models/Database.ts | 2 +- backend/src/Models/Elections.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/Migrations/2023_07_03_Initial.ts b/backend/src/Migrations/2023_07_03_Initial.ts index efbbca6a..1becb4a4 100644 --- a/backend/src/Migrations/2023_07_03_Initial.ts +++ b/backend/src/Migrations/2023_07_03_Initial.ts @@ -2,7 +2,7 @@ import { Kysely } from 'kysely' export async function up(db: Kysely): Promise { await db.schema - .createTable('electiondb') + .createTable('electionDB') .addColumn('election_id', 'varchar', (col) => col.primaryKey()) .addColumn('title', 'varchar') .addColumn('description', 'text') diff --git a/backend/src/Models/Database.ts b/backend/src/Models/Database.ts index 95bbc417..6c06d422 100644 --- a/backend/src/Models/Database.ts +++ b/backend/src/Models/Database.ts @@ -2,6 +2,6 @@ import { Election } from "../../../domain_model/Election"; import { ElectionRoll } from "../../../domain_model/ElectionRoll"; export interface Database { - electiondb: Election, + electionDB: Election, electionRollDB: ElectionRoll } \ No newline at end of file diff --git a/backend/src/Models/Elections.ts b/backend/src/Models/Elections.ts index e5a490b9..06172c96 100644 --- a/backend/src/Models/Elections.ts +++ b/backend/src/Models/Elections.ts @@ -4,7 +4,7 @@ import { ILoggingContext } from '../Services/Logging/ILogger'; import Logger from '../Services/Logging/Logger'; import { Kysely, sql } from 'kysely' import { Election } from '../../../domain_model/Election'; -const tableName = 'electiondb'; +const tableName = 'electionDB'; export default class ElectionsDB { _postgresClient; From 4d391d9d19e2a193aa6a57bc2bf9cdc0d95d63e5 Mon Sep 17 00:00:00 2001 From: mikefranze <41272412+mikefranze@users.noreply.github.com> Date: Wed, 5 Jul 2023 18:24:46 -0400 Subject: [PATCH 09/10] Converting ballot db to use kysely --- backend/src/Migrations/2023_07_03_Initial.ts | 16 +- backend/src/Models/Ballots.ts | 167 ++++++------------- backend/src/Models/Database.ts | 2 + backend/src/ServiceLocator.ts | 2 +- 4 files changed, 72 insertions(+), 115 deletions(-) diff --git a/backend/src/Migrations/2023_07_03_Initial.ts b/backend/src/Migrations/2023_07_03_Initial.ts index 1becb4a4..fe9966ec 100644 --- a/backend/src/Migrations/2023_07_03_Initial.ts +++ b/backend/src/Migrations/2023_07_03_Initial.ts @@ -35,10 +35,24 @@ export async function up(db: Kysely): Promise { .addColumn('precinct', 'varchar') .addColumn('email_data', 'json') .execute() + + await db.schema + .createTable('ballotDB') + .addColumn('ballot_id', 'varchar', (col) => col.primaryKey().notNull()) + .addColumn('election_id', 'varchar') + .addColumn('user_id', 'varchar') + .addColumn('status', 'varchar') + .addColumn('date_submitted', 'varchar') + .addColumn('ip_address', 'varchar') + .addColumn('votes', 'json', (col) => col.notNull()) + .addColumn('history', 'json') + .addColumn('precinct', 'varchar') + .execute() } export async function down(db: Kysely): Promise { - await db.schema.dropTable('electiondb').execute() + await db.schema.dropTable('electionDB').execute() await db.schema.dropTable('electionRollDB').execute() + await db.schema.dropTable('ballotDB').execute() } diff --git a/backend/src/Models/Ballots.ts b/backend/src/Models/Ballots.ts index a0fdf097..086d79b2 100644 --- a/backend/src/Models/Ballots.ts +++ b/backend/src/Models/Ballots.ts @@ -3,146 +3,87 @@ import { Uid } from '../../../domain_model/Uid'; import { ILoggingContext } from '../Services/Logging/ILogger'; import Logger from '../Services/Logging/Logger'; import { IBallotStore } from './IBallotStore'; -const className = 'BallotsDB'; +import { Kysely, sql } from 'kysely'; +import { Database } from './Database'; +import { InternalServerError } from '@curveball/http-errors'; -export default class BallotsDB implements IBallotStore { +const tableName = 'ballotDB'; +export default class BallotsDB implements IBallotStore { _postgresClient; - _tableName: string; + _tableName: string = tableName; - constructor(postgresClient:any) { - this._tableName = "ballotDB"; + constructor(postgresClient: Kysely) { this._postgresClient = postgresClient; - this.init(); + this.init() } async init(): Promise { var appInitContext = Logger.createContext("appInit"); Logger.debug(appInitContext, "BallotsDB.init"); - //await this.dropTable(appInitContext); - var query = ` - CREATE TABLE IF NOT EXISTS ${this._tableName} ( - ballot_id VARCHAR PRIMARY KEY, - election_id VARCHAR, - user_id VARCHAR, - status VARCHAR, - date_submitted VARCHAR, - ip_address VARCHAR, - votes json NOT NULL, - history json, - precinct VARCHAR - ); - `; - Logger.debug(appInitContext, query); - var p = this._postgresClient.query(query); - return p.then((_: any) => { - //This will add the new field to the live DB in prod. Once that's done we can remove this - var historyQuery = ` - ALTER TABLE ${this._tableName} ADD COLUMN IF NOT EXISTS precinct VARCHAR - `; - return this._postgresClient.query(historyQuery).catch((err:any) => { - Logger.error(appInitContext, "err adding precinct column to DB: " + err.message); - return err; - }); - }).then((_:any)=> { - return this; - }); + return this; } - async dropTable(ctx:ILoggingContext):Promise{ - var query = `DROP TABLE IF EXISTS ${this._tableName};`; - var p = this._postgresClient.query({ - text: query, - }); - return p.then((_: any) => { - Logger.debug(ctx, `Dropped it (like its hot)`); - }); + async dropTable(ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.dropTable`); + return this._postgresClient.schema.dropTable(tableName).execute() } - submitBallot(ballot: Ballot, ctx:ILoggingContext, reason:string): Promise { - Logger.debug(ctx, `${className}.submit`, ballot); - var sqlString = `INSERT INTO ${this._tableName} (ballot_id,election_id,user_id,status,date_submitted,ip_address,votes,history,precinct) - VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING ballot_id;`; - Logger.debug(ctx, sqlString); - var p = this._postgresClient.query({ - rowMode: 'array', - text: sqlString, - values: [ - ballot.ballot_id, - ballot.election_id, - ballot.user_id, - ballot.status, - ballot.date_submitted, - ballot.ip_address, - JSON.stringify(ballot.votes), - JSON.stringify(ballot.history), - ballot.precinct] - }); + submitBallot(ballot: Ballot, ctx: ILoggingContext, reason: string): Promise { + Logger.debug(ctx, `${tableName}.submit`, ballot); - return p.then((res: any) => { - Logger.debug(ctx, `set response rows:`, res); - ballot.ballot_id = res.rows[0][0]; - Logger.state(ctx, `Ballot submitted`, { ballot: ballot, reason: reason}); - return ballot; - }); + return this._postgresClient + .insertInto(tableName) + .values(ballot) + .returningAll() + .executeTakeFirstOrThrow() + .then((ballot) => { + Logger.state(ctx, `Ballot submitted`, { ballot: ballot, reason: reason }); + return ballot; + }); } - getBallotByID(ballot_id: string, ctx:ILoggingContext): Promise { - Logger.debug(ctx, `${className}.getBallotByID ${ballot_id}`); - var sqlString = `SELECT * FROM ${this._tableName} WHERE ballot_id = $1`; - Logger.debug(ctx, sqlString); + getBallotByID(ballot_id: string, ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.getBallotByID ${ballot_id}`); - var p = this._postgresClient.query({ - text: sqlString, - values: [ballot_id] - }); - return p.then((response: any) => { - var rows = response.rows; - if (rows.length == 0) { - Logger.debug(ctx, `.get null`); + return this._postgresClient + .selectFrom(tableName) + .selectAll() + .where('ballot_id', '=', ballot_id) + .executeTakeFirstOrThrow() + .catch((reason: any) => { + Logger.debug(ctx, `${tableName}.get null`, reason); return null; - } - return rows[0] as Ballot; - }); + }) } - getBallotsByElectionID(election_id: string, ctx:ILoggingContext): Promise { - Logger.debug(ctx, `${className}.getBallotsByElectionID ${election_id}`); - var sqlString = `SELECT * FROM ${this._tableName} WHERE election_id = $1`; - Logger.debug(ctx, sqlString); + getBallotsByElectionID(election_id: string, ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.getBallotsByElectionID ${election_id}`); - var p = this._postgresClient.query({ - text: sqlString, - values: [election_id] - }); - return p.then((response: any) => { - var rows = response.rows; - console.log(rows[0]) - if (rows.length == 0) { - Logger.debug(ctx, `.get null`); - return [] as Ballot[]; - } - return rows - }); + return this._postgresClient + .selectFrom(tableName) + .selectAll() + .where('election_id', '=', election_id) + .execute() } - delete(ballot_id: Uid, ctx:ILoggingContext, reason:string): Promise { - Logger.debug(ctx, `${className}.delete ${ballot_id}`); + delete(ballot_id: Uid, ctx: ILoggingContext, reason: string): Promise { + Logger.debug(ctx, `${tableName}.delete ${ballot_id}`); var sqlString = `DELETE FROM ${this._tableName} WHERE ballot_id = $1`; Logger.debug(ctx, sqlString); - var p = this._postgresClient.query({ - rowMode: 'array', - text: sqlString, - values: [ballot_id] - }); - return p.then((response: any) => { - if (response.rowCount == 1) { - Logger.state(ctx, `Ballot ${ballot_id} deleted:`, {ballotId: ballot_id, reason: reason }); - return true; - } - return false; - }); + return this._postgresClient + .deleteFrom(tableName) + .where('ballot_id', '=', ballot_id) + .returningAll() + .executeTakeFirst() + .then((ballot) => { + if (ballot) { + return true + } else { + return false + } + }) } } \ No newline at end of file diff --git a/backend/src/Models/Database.ts b/backend/src/Models/Database.ts index 6c06d422..9e7c1ece 100644 --- a/backend/src/Models/Database.ts +++ b/backend/src/Models/Database.ts @@ -1,7 +1,9 @@ +import { Ballot } from "../../../domain_model/Ballot"; import { Election } from "../../../domain_model/Election"; import { ElectionRoll } from "../../../domain_model/ElectionRoll"; export interface Database { electionDB: Election, electionRollDB: ElectionRoll + ballotDB: Ballot } \ No newline at end of file diff --git a/backend/src/ServiceLocator.ts b/backend/src/ServiceLocator.ts index 2e47c88d..5c366a1a 100644 --- a/backend/src/ServiceLocator.ts +++ b/backend/src/ServiceLocator.ts @@ -100,7 +100,7 @@ async function eventQueue(): Promise { function ballotsDb(): IBallotStore { if (_ballotsDb == null) { - _ballotsDb = new BallotsDB(postgres()); + _ballotsDb = new BallotsDB(database()); } return _ballotsDb; } From a4e437746830b7b5d1f713d0bc0dad3bf7859673 Mon Sep 17 00:00:00 2001 From: eznarf <41272412+eznarf@users.noreply.github.com> Date: Sat, 4 Nov 2023 13:39:17 -0700 Subject: [PATCH 10/10] changing startup script to run latest migrations --- Dockerfile | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c1ecd82c..7534cd31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,6 +42,6 @@ COPY --chown=node:node --from=build /usr/src/app/domain_model domain_model COPY --chown=node:node --from=build /usr/src/app/frontend/build /usr/src/app/frontend/build COPY --chown=node:node --from=build /usr/src/app/backend/build /usr/src/app/backend/build COPY --chown=node:node --from=build /usr/src/app/backend/node_modules /usr/src/app/backend/node_modules -CMD ["dumb-init", "node", "backend/build/backend/src/index.js"] - +ENTRYPOINT ["dumb-init", "--"] +CMD ["npm", "run", "start"] EXPOSE 5000 diff --git a/package.json b/package.json index 2fcfbf30..b6547ab8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "test": "cd backend && npm install && npm test", - "start": "cd backend && npm start", + "start": "node ./backend/build/backend/src/migrate-to-latest.js && node ./backend/build/backend/src/index.js", "heroku-postbuild": "cd frontend && npm install && npm run build && cd ../backend && npm install" }, "repository": {