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/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..fe9966ec --- /dev/null +++ b/backend/src/Migrations/2023_07_03_Initial.ts @@ -0,0 +1,58 @@ +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() + + 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() + + 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('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 new file mode 100644 index 00000000..9e7c1ece --- /dev/null +++ b/backend/src/Models/Database.ts @@ -0,0 +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/Models/ElectionRolls.ts b/backend/src/Models/ElectionRolls.ts index 4601a841..df213100 100644 --- a/backend/src/Models/ElectionRolls.ts +++ b/backend/src/Models/ElectionRolls.ts @@ -1,237 +1,148 @@ -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 { Expression, Kysely } from 'kysely' +import { Database } from './Database'; +import { ElectionRoll } from '../../../domain_model/ElectionRoll'; +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)`); - }); - } - - 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}`); - }); + async dropTable(ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.dropTable`); + return this._postgresClient.schema.dropTable(tableName).execute() } - 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); + submitElectionRoll(electionRolls: ElectionRoll[], ctx: ILoggingContext, reason: string): Promise { + Logger.debug(ctx, `${tableName}.submit`); - 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 + .insertInto(tableName) + .values(electionRolls) + .execute().then((res) => { return true }) } - getByVoterID(election_id: string, voter_id: string, ctx:ILoggingContext): Promise { - Logger.debug(ctx, `ElectionRollDB.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); + getRollsByElectionID(election_id: string, ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.getByElectionID ${election_id}`); - 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) + .selectAll() + .execute() } - 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 - }); + getByVoterID(election_id: string, voter_id: string, ctx: ILoggingContext): Promise { + Logger.debug(ctx, `${tableName}.getByVoterID election:${election_id}, voter:${voter_id}`); + + 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, `${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}`); - let sqlString = `SELECT * FROM ${this._tableName} WHERE election_id = $1 AND ( `; - 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 - }); + 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}`); + + return this._postgresClient + .selectFrom(tableName) + .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 eb.or(ors) + }) + .selectAll() + .execute() + .then((rolls) => { + if (rolls.length == 0) return null + return rolls + }) + .catch(((reason: any) => { + console.log('aaaaahhhhhh') + Logger.debug(ctx, reason); + return null + })) } - update(election_roll: ElectionRoll, ctx:ILoggingContext, reason:string): Promise { - Logger.debug(ctx, `ElectionRollDB.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); + update(election_roll: ElectionRoll, ctx: ILoggingContext, reason: string): Promise { + Logger.debug(ctx, `${tableName}.updateRoll`); 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(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, `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); - - 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; + let deletedRoll = this._postgresClient + .deleteFrom(tableName) + .where('election_id', '=', election_roll.election_id) + .where('voter_id', '=', election_roll.voter_id) + .returningAll() + .execute() + + return deletedRoll.then((roll) => { + if (roll) { + return true + } else { + return false } - return false; - }); + }) } } \ No newline at end of file diff --git a/backend/src/Models/Elections.ts b/backend/src/Models/Elections.ts index 29d68d76..06172c96 100644 --- a/backend/src/Models/Elections.ts +++ b/backend/src/Models/Elections.ts @@ -1,247 +1,133 @@ -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 { Election } from '../../../domain_model/Election'; +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)`); - }); + Logger.debug(ctx, `${tableName}.dropTable`); + 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 - ] - }); - - 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; - }); + Logger.debug(ctx, `${tableName}.createElection`, election); + + const newElection = this._postgresClient + .insertInto(tableName) + .values(election) + .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); - 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(election) + .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/IElectionRollStore.ts b/backend/src/Models/IElectionRollStore.ts index ecd13213..5e2e7dd3 100644 --- a/backend/src/Models/IElectionRollStore.ts +++ b/backend/src/Models/IElectionRollStore.ts @@ -22,7 +22,7 @@ export interface IElectionRollStore { email: string|null, ip_address: string|null, ctx:ILoggingContext - ) => Promise<[ElectionRoll] | null>; + ) => Promise; update: ( election_roll: ElectionRoll, ctx: ILoggingContext, diff --git a/backend/src/Models/serialize-parameters/serialize-parameters-plugin.ts b/backend/src/Models/serialize-parameters/serialize-parameters-plugin.ts new file mode 100644 index 00000000..ff3ceff9 --- /dev/null +++ b/backend/src/Models/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/Models/serialize-parameters/serialize-parameters-transformer.ts b/backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts new file mode 100644 index 00000000..5da0766b --- /dev/null +++ b/backend/src/Models/serialize-parameters/serialize-parameters-transformer.ts @@ -0,0 +1,114 @@ +import { + ColumnUpdateNode, + OperationNodeTransformer, + OperatorNode, + PrimitiveValueListNode, + ValueListNode, + ValueNode, + ValuesNode, + OperationNode +} 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 + } + + const { value, ...item } = listNodeItem as ValueNode + + 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) + } + + const { value, ...item } = valueNode as 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/Models/serialize-parameters/serialize-parameters.ts b/backend/src/Models/serialize-parameters/serialize-parameters.ts new file mode 100644 index 00000000..7b1bb910 --- /dev/null +++ b/backend/src/Models/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/backend/src/ServiceLocator.ts b/backend/src/ServiceLocator.ts index 6d2ad887..5c366a1a 100644 --- a/backend/src/ServiceLocator.ts +++ b/backend/src/ServiceLocator.ts @@ -10,31 +10,64 @@ 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"; +import { SerializeParametersPlugin } from "./Models/serialize-parameters/serialize-parameters-plugin"; + 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, + plugins: [ + new SerializeParametersPlugin({ + serializer: (value) => { + if (value !== null && typeof value === 'object') { + return JSON.stringify(value) + } + return value + } + }), + ], + }) + } 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 +84,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 +98,55 @@ async function eventQueue():Promise { return _eventQueue; } -function ballotsDb():IBallotStore { - if (_ballotsDb == null){ - _ballotsDb = new BallotsDB(postgres()); +function ballotsDb(): IBallotStore { + if (_ballotsDb == null) { + _ballotsDb = new BallotsDB(database()); } 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){ - _electionRollDb = new ElectionRollDB(postgres()); +function electionRollDb(): ElectionRollDB { + if (_electionRollDb == null) { + _electionRollDb = new ElectionRollDB(database()); } 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/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', 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": {