From 5ae45cafca7e1e027b7254b4e1fc72e2cf70a3f4 Mon Sep 17 00:00:00 2001 From: Eli Morris-Heft Date: Mon, 12 Feb 2024 21:52:34 -0500 Subject: [PATCH] Move Trackbear over to PostgreSQL --- .env.example | 19 +- Dockerfile | 8 +- README.md | 28 +- docker-compose.yaml | 33 +- docs/env.md | 4 + package-lock.json | 312 ++++++++++++++++++ package.json | 2 + prisma/migrations-sqlite/0_init/migration.sql | 41 +++ .../1702337365_session_table/migration.sql | 0 .../migration.sql | 0 .../20231212023328_salt/migration.sql | 0 .../20231212035324_add_username/migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../20240113043555_enable_wal/migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration_lock.toml | 0 prisma/migrations/0_init/migration.sql | 184 ++++++++++- prisma/schema.prisma | 53 +-- scripts/healthchecks/postgres.sh | 21 ++ .../trackbear.js} | 0 scripts/sqlite2pg.sh | 16 + server/lib/env.ts | 15 + server/lib/queue.ts | 18 +- 39 files changed, 678 insertions(+), 76 deletions(-) create mode 100644 prisma/migrations-sqlite/0_init/migration.sql rename prisma/{migrations => migrations-sqlite}/1702337365_session_table/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231211233144_uuid_unique_indexes/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231212023328_salt/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231212035324_add_username/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231212035410_add_unique_username_index/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231214021837_make_start_date_optional/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231214022923_change_dates_to_strings/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231214022945_change_dates_to_strings_again/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231220002804_add_audit_event_table/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231220004343_add_createdat_to_audit_events/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231223170837_add_leaderboards/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20231228232936_add_updated_at_to_audit_event/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240106041110_add_userauth_table/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240106043622_populate_userauth/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240106050805_remove_password_and_salt_from_user_table/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240111015455_add_password_reset_links/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240112211230_add_email_verification_table/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240112223910_add_new_email_column_to_email_verification_table/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240113024038_add_is_email_verified_column/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240113043555_enable_wal/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240113235914_add_banner_table/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240114001703_add_id_column_to_banner_table/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/20240116003708_remove_previousemail_field/migration.sql (100%) rename prisma/{migrations => migrations-sqlite}/migration_lock.toml (100%) create mode 100755 scripts/healthchecks/postgres.sh rename scripts/{healthcheck.js => healthchecks/trackbear.js} (100%) create mode 100755 scripts/sqlite2pg.sh diff --git a/.env.example b/.env.example index 245b086..0896c1b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ # See docs/env.md for more details. -# Contaner volume paths +## Contaner volume paths LOGS_VOLUME_DIR=./logs CERTS_VOLUME_DIR=./certs DB_VOLUME_DIR=./db @@ -9,22 +9,29 @@ DB_VOLUME_DIR=./db PORT=3000 HAS_PROXY=0 -# HTTPS/TLS +## HTTPS/TLS ENABLE_TLS=0 TLS_KEY_PATH= TLS_CERT_PATH= TLS_ALLOW_SELF_SIGNED=0 -# Logs +## Logs LOG_LEVEL=info -# Database +## Database +DATABASE_USER=postgres +DATABASE_PASSWORD=postgres +DATABASE_NAME=trackbear +DATABASE_HOST=db +# This is used in prisma.schema; don't change it +DATABASE_URL=postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME} + DB_APP_DB_URL=file:/db/trackbear.db -# Session/Cookies +## Session/Cookies COOKIE_SECRET=replace-this-with-random-characters -# Email +## Email ENABLE_EMAIL=1 MAILERSEND_API_KEY= EMAIL_URL_PREFIX= diff --git a/Dockerfile b/Dockerfile index 1438204..9db3790 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,8 @@ # base stage FROM node:lts-slim as base -# Install the latest openssl (maybe we need alpine?) -RUN apt-get update -y && apt-get install -y openssl curl +# Install the latest openssl (for HTTPS), curl, and psql (for debugging) +RUN apt update -y && apt install -y openssl curl postgresql-client # Default NODE_ENV to development (the safest); later stages will override this if needed ENV NODE_ENV=development @@ -58,7 +58,7 @@ ENV DB_APP_DB_URL $DB_APP_DB_URL RUN npx prisma generate # Check every 30s to ensure /api/ping returns HTTP 200 -HEALTHCHECK --interval=30s CMD node ./scripts/healthcheck.js +HEALTHCHECK --interval=30s CMD node ./scripts/healthchecks/trackbear.js # Start the server (this also runs migrations) CMD [ "./entrypoint.sh" ] @@ -81,7 +81,7 @@ RUN npx prisma generate RUN npm run build:client # Check every 30s to ensure /api/ping returns HTTP 200 -HEALTHCHECK --interval=30s CMD node ./scripts/healthcheck.js +HEALTHCHECK --interval=30s CMD node ./scripts/healthchecks/trackbear.js # Start the server (this also runs migrations) CMD [ "./entrypoint.sh" ] diff --git a/README.md b/README.md index f44d519..e6c2f4b 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,33 @@ > TrackBear is... not even in beta. It's in super-mega-alpha mode right now. Please don't use it unless you are willing to be testing an alpha build. -## Installation +## Setup ```sh # install dependencies npm install + +# copy the .env file and then fill it out +cp .env.example .env ``` -## Setup +See [the environment variable documentation](./docs/env.md) for more details on environment variables. + +Then set up the docker stack: ```sh -# copy the .env file -cp .env.example .env +# build the container +docker compose build + +# run the docker compose stack +docker compose up ``` -See [the environment variable documentation](./docs/env.md) for more details on environment variables. +**The first time you set up your docker stack, you will need to manually create the `queue` database.** This is a limitation of the Postgres docker container. Exec into the container (`docker exec -it /bin/bash trackbear-db-1`) and run: + +```sh +createdb -U $POSTGRES_USER queue +``` ## Developing @@ -34,6 +46,10 @@ npm run watch Starting the container in watch mode means that it will either copy in changed files or restart the container (depending on what's needed) as you save files. This enables HMR and other creature comforts. +See [the docker documentation](./docs/docker.md) for more details on specific commands. + +### Production mode + You can also start up the app in a docker container in production mode: ```sh @@ -46,7 +62,7 @@ docker compose -f docker-compose.production.yaml up -d npm run start:prod ``` -See [the docker documentation](./docs/docker.md) for more details on specific commands. +### Outside of a container You *can* start up the app locally (outside of a container) using `npm run local:start:dev` and `npm run local:start:prod` but **these are deprecated and you shouldn't use them**. diff --git a/docker-compose.yaml b/docker-compose.yaml index fcf37bb..0beb590 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,10 +6,9 @@ services: ports: - "${PORT}:${PORT}" # http(s) - 24678:24678 # HMR - # TODO: migrate from sqlite to postgres - # depends_on: - # db: - # condition: service_healthy + depends_on: + db: + condition: service_healthy env_file: - ./.env volumes: @@ -67,13 +66,19 @@ services: action: rebuild target: . - # db: - # image: postgres:alpine - # environment: - # POSTGRES_USER: postgres - # POSTGRES_PASSWORD: postgres - # volumes: - # - ./healthchecks:/healthchecks - # healthcheck: - # test: /healthchecks/postgres-healthcheck - # interval: "5s" + db: + image: postgres:16-alpine + env_file: + - ./.env + environment: + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME} + POSTGRES_INITDB_ARGS: -E UTF8 + volumes: + - ./scripts/healthchecks:/healthchecks + - ${DB_VOLUME_DIR}/postgres:/var/lib/postgresql/data:rw + - ${DB_VOLUME_DIR}:/data:rw + healthcheck: + test: /healthchecks/postgres.sh + interval: "5s" diff --git a/docs/env.md b/docs/env.md index a1f5b54..5871744 100644 --- a/docs/env.md +++ b/docs/env.md @@ -40,6 +40,10 @@ These are only needed for working with the Docker Compose files. ## Database | Variable | Default | Notes | | --- | --- | --- | +| `DATABASE_USER` | | The username to use with the Postgres database | +| `DATABASE_PASSWORD` | | The password to use with the Postgres database | +| `DATABASE_NAME` | | The database name to use | +| `DATABASE_HOST` | | The hostname for the Postgres database | | `DB_APP_DB_URL` | `file:/db/trackbear.db` | This URL is used by the Prisma schema (see *prisma/schema.prisma*) to connect to the database. | | `DB_PATH` | `/db` | The directory to create databases in. Don't set this unless you're running outside a container for some reason. | diff --git a/package-lock.json b/package-lock.json index e2db05c..6c91506 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@vitejs/plugin-vue": "^4.5.0", "autoprefixer": "^10.4.16", "better-queue": "^3.8.12", + "better-queue-sql": "^1.0.6", "better-queue-sqlite": "^1.0.7", "body-parser": "^1.20.2", "chart.js": "^4.4.1", @@ -28,6 +29,7 @@ "markdown-it": "^14.0.0", "material-design-icons-iconfont": "^6.7.0", "morgan": "^1.10.0", + "pg": "^8.11.3", "pinia": "^2.1.7", "postcss": "^8.4.32", "prisma": "^5.8.1", @@ -1980,6 +1982,37 @@ "resolved": "https://registry.npmjs.org/better-queue-memory/-/better-queue-memory-1.0.4.tgz", "integrity": "sha512-SWg5wFIShYffEmJpI6LgbL8/3Dqhku7xI1oEiy6FroP9DbcZlG0ZDjxvPdP9t7hTGW40IpIcC6zVoGT1oxjOuA==" }, + "node_modules/better-queue-sql": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/better-queue-sql/-/better-queue-sql-1.0.6.tgz", + "integrity": "sha512-AHI9qfeAN3JHJYnQqmMLVpzNenYmhSvfMdvUK27v7azdbHnadon2h8/zJ9nDDeWgP+ZxmiGxqbY6GGBqkUlRZQ==", + "dependencies": { + "async": "^2.0.1", + "extend": "^3.0.0", + "knex": "^2.2.0", + "uuid": "^3.0.0" + }, + "peerDependencies": { + "better-queue": "3.x" + } + }, + "node_modules/better-queue-sql/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/better-queue-sql/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/better-queue-sqlite": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/better-queue-sqlite/-/better-queue-sqlite-1.0.7.tgz", @@ -2139,6 +2172,14 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2393,6 +2434,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -3030,6 +3076,14 @@ "node": "*" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3612,6 +3666,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -3985,6 +4052,14 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -4192,6 +4267,72 @@ "json-buffer": "3.0.1" } }, + "node_modules/knex": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz", + "integrity": "sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.1", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/knex/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -5019,6 +5160,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5112,6 +5258,94 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "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": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "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.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-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "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.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", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -5332,6 +5566,41 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -5555,6 +5824,17 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -5973,6 +6253,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sqlite3": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", @@ -6281,6 +6569,14 @@ "node": ">=8" } }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -6311,6 +6607,14 @@ "node": ">=0.8" } }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6881,6 +7185,14 @@ "node": ">=12" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 08fed76..86be2b7 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@vitejs/plugin-vue": "^4.5.0", "autoprefixer": "^10.4.16", "better-queue": "^3.8.12", + "better-queue-sql": "^1.0.6", "better-queue-sqlite": "^1.0.7", "body-parser": "^1.20.2", "chart.js": "^4.4.1", @@ -46,6 +47,7 @@ "markdown-it": "^14.0.0", "material-design-icons-iconfont": "^6.7.0", "morgan": "^1.10.0", + "pg": "^8.11.3", "pinia": "^2.1.7", "postcss": "^8.4.32", "prisma": "^5.8.1", diff --git a/prisma/migrations-sqlite/0_init/migration.sql b/prisma/migrations-sqlite/0_init/migration.sql new file mode 100644 index 0000000..b9ab0e4 --- /dev/null +++ b/prisma/migrations-sqlite/0_init/migration.sql @@ -0,0 +1,41 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "uuid" TEXT NOT NULL, + "email" TEXT NOT NULL, + "displayName" TEXT NOT NULL, + "password" TEXT NOT NULL, + "state" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "Project" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "uuid" TEXT NOT NULL, + "ownerId" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "type" TEXT NOT NULL, + "state" TEXT NOT NULL, + "goal" INTEGER, + "startDate" DATETIME NOT NULL, + "endDate" DATETIME, + "visibility" TEXT NOT NULL, + "starred" BOOLEAN NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Project_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Update" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "projectId" INTEGER NOT NULL, + "date" DATETIME NOT NULL, + "value" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Update_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + diff --git a/prisma/migrations/1702337365_session_table/migration.sql b/prisma/migrations-sqlite/1702337365_session_table/migration.sql similarity index 100% rename from prisma/migrations/1702337365_session_table/migration.sql rename to prisma/migrations-sqlite/1702337365_session_table/migration.sql diff --git a/prisma/migrations/20231211233144_uuid_unique_indexes/migration.sql b/prisma/migrations-sqlite/20231211233144_uuid_unique_indexes/migration.sql similarity index 100% rename from prisma/migrations/20231211233144_uuid_unique_indexes/migration.sql rename to prisma/migrations-sqlite/20231211233144_uuid_unique_indexes/migration.sql diff --git a/prisma/migrations/20231212023328_salt/migration.sql b/prisma/migrations-sqlite/20231212023328_salt/migration.sql similarity index 100% rename from prisma/migrations/20231212023328_salt/migration.sql rename to prisma/migrations-sqlite/20231212023328_salt/migration.sql diff --git a/prisma/migrations/20231212035324_add_username/migration.sql b/prisma/migrations-sqlite/20231212035324_add_username/migration.sql similarity index 100% rename from prisma/migrations/20231212035324_add_username/migration.sql rename to prisma/migrations-sqlite/20231212035324_add_username/migration.sql diff --git a/prisma/migrations/20231212035410_add_unique_username_index/migration.sql b/prisma/migrations-sqlite/20231212035410_add_unique_username_index/migration.sql similarity index 100% rename from prisma/migrations/20231212035410_add_unique_username_index/migration.sql rename to prisma/migrations-sqlite/20231212035410_add_unique_username_index/migration.sql diff --git a/prisma/migrations/20231214021837_make_start_date_optional/migration.sql b/prisma/migrations-sqlite/20231214021837_make_start_date_optional/migration.sql similarity index 100% rename from prisma/migrations/20231214021837_make_start_date_optional/migration.sql rename to prisma/migrations-sqlite/20231214021837_make_start_date_optional/migration.sql diff --git a/prisma/migrations/20231214022923_change_dates_to_strings/migration.sql b/prisma/migrations-sqlite/20231214022923_change_dates_to_strings/migration.sql similarity index 100% rename from prisma/migrations/20231214022923_change_dates_to_strings/migration.sql rename to prisma/migrations-sqlite/20231214022923_change_dates_to_strings/migration.sql diff --git a/prisma/migrations/20231214022945_change_dates_to_strings_again/migration.sql b/prisma/migrations-sqlite/20231214022945_change_dates_to_strings_again/migration.sql similarity index 100% rename from prisma/migrations/20231214022945_change_dates_to_strings_again/migration.sql rename to prisma/migrations-sqlite/20231214022945_change_dates_to_strings_again/migration.sql diff --git a/prisma/migrations/20231220002804_add_audit_event_table/migration.sql b/prisma/migrations-sqlite/20231220002804_add_audit_event_table/migration.sql similarity index 100% rename from prisma/migrations/20231220002804_add_audit_event_table/migration.sql rename to prisma/migrations-sqlite/20231220002804_add_audit_event_table/migration.sql diff --git a/prisma/migrations/20231220004343_add_createdat_to_audit_events/migration.sql b/prisma/migrations-sqlite/20231220004343_add_createdat_to_audit_events/migration.sql similarity index 100% rename from prisma/migrations/20231220004343_add_createdat_to_audit_events/migration.sql rename to prisma/migrations-sqlite/20231220004343_add_createdat_to_audit_events/migration.sql diff --git a/prisma/migrations/20231223170837_add_leaderboards/migration.sql b/prisma/migrations-sqlite/20231223170837_add_leaderboards/migration.sql similarity index 100% rename from prisma/migrations/20231223170837_add_leaderboards/migration.sql rename to prisma/migrations-sqlite/20231223170837_add_leaderboards/migration.sql diff --git a/prisma/migrations/20231228232936_add_updated_at_to_audit_event/migration.sql b/prisma/migrations-sqlite/20231228232936_add_updated_at_to_audit_event/migration.sql similarity index 100% rename from prisma/migrations/20231228232936_add_updated_at_to_audit_event/migration.sql rename to prisma/migrations-sqlite/20231228232936_add_updated_at_to_audit_event/migration.sql diff --git a/prisma/migrations/20240106041110_add_userauth_table/migration.sql b/prisma/migrations-sqlite/20240106041110_add_userauth_table/migration.sql similarity index 100% rename from prisma/migrations/20240106041110_add_userauth_table/migration.sql rename to prisma/migrations-sqlite/20240106041110_add_userauth_table/migration.sql diff --git a/prisma/migrations/20240106043622_populate_userauth/migration.sql b/prisma/migrations-sqlite/20240106043622_populate_userauth/migration.sql similarity index 100% rename from prisma/migrations/20240106043622_populate_userauth/migration.sql rename to prisma/migrations-sqlite/20240106043622_populate_userauth/migration.sql diff --git a/prisma/migrations/20240106050805_remove_password_and_salt_from_user_table/migration.sql b/prisma/migrations-sqlite/20240106050805_remove_password_and_salt_from_user_table/migration.sql similarity index 100% rename from prisma/migrations/20240106050805_remove_password_and_salt_from_user_table/migration.sql rename to prisma/migrations-sqlite/20240106050805_remove_password_and_salt_from_user_table/migration.sql diff --git a/prisma/migrations/20240111015455_add_password_reset_links/migration.sql b/prisma/migrations-sqlite/20240111015455_add_password_reset_links/migration.sql similarity index 100% rename from prisma/migrations/20240111015455_add_password_reset_links/migration.sql rename to prisma/migrations-sqlite/20240111015455_add_password_reset_links/migration.sql diff --git a/prisma/migrations/20240112211230_add_email_verification_table/migration.sql b/prisma/migrations-sqlite/20240112211230_add_email_verification_table/migration.sql similarity index 100% rename from prisma/migrations/20240112211230_add_email_verification_table/migration.sql rename to prisma/migrations-sqlite/20240112211230_add_email_verification_table/migration.sql diff --git a/prisma/migrations/20240112223910_add_new_email_column_to_email_verification_table/migration.sql b/prisma/migrations-sqlite/20240112223910_add_new_email_column_to_email_verification_table/migration.sql similarity index 100% rename from prisma/migrations/20240112223910_add_new_email_column_to_email_verification_table/migration.sql rename to prisma/migrations-sqlite/20240112223910_add_new_email_column_to_email_verification_table/migration.sql diff --git a/prisma/migrations/20240113024038_add_is_email_verified_column/migration.sql b/prisma/migrations-sqlite/20240113024038_add_is_email_verified_column/migration.sql similarity index 100% rename from prisma/migrations/20240113024038_add_is_email_verified_column/migration.sql rename to prisma/migrations-sqlite/20240113024038_add_is_email_verified_column/migration.sql diff --git a/prisma/migrations/20240113043555_enable_wal/migration.sql b/prisma/migrations-sqlite/20240113043555_enable_wal/migration.sql similarity index 100% rename from prisma/migrations/20240113043555_enable_wal/migration.sql rename to prisma/migrations-sqlite/20240113043555_enable_wal/migration.sql diff --git a/prisma/migrations/20240113235914_add_banner_table/migration.sql b/prisma/migrations-sqlite/20240113235914_add_banner_table/migration.sql similarity index 100% rename from prisma/migrations/20240113235914_add_banner_table/migration.sql rename to prisma/migrations-sqlite/20240113235914_add_banner_table/migration.sql diff --git a/prisma/migrations/20240114001703_add_id_column_to_banner_table/migration.sql b/prisma/migrations-sqlite/20240114001703_add_id_column_to_banner_table/migration.sql similarity index 100% rename from prisma/migrations/20240114001703_add_id_column_to_banner_table/migration.sql rename to prisma/migrations-sqlite/20240114001703_add_id_column_to_banner_table/migration.sql diff --git a/prisma/migrations/20240116003708_remove_previousemail_field/migration.sql b/prisma/migrations-sqlite/20240116003708_remove_previousemail_field/migration.sql similarity index 100% rename from prisma/migrations/20240116003708_remove_previousemail_field/migration.sql rename to prisma/migrations-sqlite/20240116003708_remove_previousemail_field/migration.sql diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations-sqlite/migration_lock.toml similarity index 100% rename from prisma/migrations/migration_lock.toml rename to prisma/migrations-sqlite/migration_lock.toml diff --git a/prisma/migrations/0_init/migration.sql b/prisma/migrations/0_init/migration.sql index b9ab0e4..7a7ffbe 100644 --- a/prisma/migrations/0_init/migration.sql +++ b/prisma/migrations/0_init/migration.sql @@ -1,41 +1,195 @@ +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sid" TEXT NOT NULL, + "data" TEXT NOT NULL, + "expiresAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditEvent" ( + "id" SERIAL NOT NULL, + "agentId" INTEGER NOT NULL, + "patientId" INTEGER, + "goalId" INTEGER, + "eventType" TEXT NOT NULL, + "auxInfo" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "AuditEvent_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" SERIAL NOT NULL, "uuid" TEXT NOT NULL, - "email" TEXT NOT NULL, + "state" TEXT NOT NULL, + "username" TEXT NOT NULL, "displayName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "isEmailVerified" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserAuth" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, "password" TEXT NOT NULL, + "salt" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "UserAuth_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PendingEmailVerification" ( + "uuid" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "newEmail" TEXT NOT NULL, + "expiresAt" TIMESTAMPTZ(3) NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "PendingEmailVerification_pkey" PRIMARY KEY ("uuid") +); + +-- CreateTable +CREATE TABLE "PasswordResetLink" ( + "uuid" TEXT NOT NULL, + "userId" INTEGER NOT NULL, "state" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL + "expiresAt" TIMESTAMPTZ(3) NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "PasswordResetLink_pkey" PRIMARY KEY ("uuid") +); + +-- CreateTable +CREATE TABLE "Banner" ( + "id" SERIAL NOT NULL, + "uuid" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL, + "showUntil" TIMESTAMPTZ(3) NOT NULL, + "message" TEXT NOT NULL, + "color" TEXT NOT NULL DEFAULT 'info', + "icon" TEXT NOT NULL DEFAULT 'campaign', + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Banner_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Project" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" SERIAL NOT NULL, "uuid" TEXT NOT NULL, "ownerId" INTEGER NOT NULL, "title" TEXT NOT NULL, "type" TEXT NOT NULL, "state" TEXT NOT NULL, "goal" INTEGER, - "startDate" DATETIME NOT NULL, - "endDate" DATETIME, + "startDate" TEXT, + "endDate" TEXT, "visibility" TEXT NOT NULL, "starred" BOOLEAN NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "Project_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "Update" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" SERIAL NOT NULL, "projectId" INTEGER NOT NULL, - "date" DATETIME NOT NULL, + "date" TEXT NOT NULL, "value" INTEGER NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "Update_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE RESTRICT ON UPDATE CASCADE + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Update_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Leaderboard" ( + "id" SERIAL NOT NULL, + "uuid" TEXT NOT NULL, + "ownerId" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "type" TEXT NOT NULL, + "state" TEXT NOT NULL, + "goal" INTEGER, + "startDate" TEXT, + "endDate" TEXT, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Leaderboard_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_LeaderboardToProject" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL ); +-- CreateIndex +CREATE UNIQUE INDEX "Session_sid_key" ON "Session"("sid"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_uuid_key" ON "User"("uuid"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserAuth_userId_key" ON "UserAuth"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Banner_uuid_key" ON "Banner"("uuid"); + +-- CreateIndex +CREATE UNIQUE INDEX "Project_uuid_key" ON "Project"("uuid"); + +-- CreateIndex +CREATE UNIQUE INDEX "Leaderboard_uuid_key" ON "Leaderboard"("uuid"); + +-- CreateIndex +CREATE UNIQUE INDEX "_LeaderboardToProject_AB_unique" ON "_LeaderboardToProject"("A", "B"); + +-- CreateIndex +CREATE INDEX "_LeaderboardToProject_B_index" ON "_LeaderboardToProject"("B"); + +-- AddForeignKey +ALTER TABLE "UserAuth" ADD CONSTRAINT "UserAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PendingEmailVerification" ADD CONSTRAINT "PendingEmailVerification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PasswordResetLink" ADD CONSTRAINT "PasswordResetLink_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Update" ADD CONSTRAINT "Update_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Leaderboard" ADD CONSTRAINT "Leaderboard_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_LeaderboardToProject" ADD CONSTRAINT "_LeaderboardToProject_A_fkey" FOREIGN KEY ("A") REFERENCES "Leaderboard"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_LeaderboardToProject" ADD CONSTRAINT "_LeaderboardToProject_B_fkey" FOREIGN KEY ("B") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2cae55a..671157b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -5,16 +5,21 @@ generator client { provider = "prisma-client-js" } +// datasource db { +// provider = "sqlite" +// url = env("DB_APP_DB_URL") +// } + datasource db { - provider = "sqlite" - url = env("DB_APP_DB_URL") + provider = "postgresql" + url = env("DATABASE_URL") } model Session { id String @id sid String @unique data String - expiresAt DateTime + expiresAt DateTime @db.Timestamptz(3) } model AuditEvent { @@ -28,8 +33,8 @@ model AuditEvent { eventType String auxInfo String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt // this should never differ from createdAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt // this should never differ from createdAt } model User { @@ -43,8 +48,8 @@ model User { email String isEmailVerified Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt projects Project[] leaderboards Leaderboard[] @@ -62,8 +67,8 @@ model UserAuth { password String salt String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt } model PendingEmailVerification { @@ -73,10 +78,10 @@ model PendingEmailVerification { userId Int newEmail String - expiresAt DateTime + expiresAt DateTime @db.Timestamptz(3) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt } model PasswordResetLink { @@ -86,10 +91,10 @@ model PasswordResetLink { userId Int state String - expiresAt DateTime + expiresAt DateTime @db.Timestamptz(3) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt } model Banner { @@ -97,14 +102,14 @@ model Banner { uuid String @unique @default(uuid()) enabled Boolean - showUntil DateTime + showUntil DateTime @db.Timestamptz(3) message String color String @default("info") icon String @default("campaign") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt } model Project { @@ -125,8 +130,8 @@ model Project { visibility String starred Boolean - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt updates Update[] leaderboards Leaderboard[] @@ -141,8 +146,8 @@ model Update { date String value Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt } model Leaderboard { @@ -162,6 +167,6 @@ model Leaderboard { projects Project[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @db.Timestamptz(3) @default(now()) + updatedAt DateTime @db.Timestamptz(3) @updatedAt } diff --git a/scripts/healthchecks/postgres.sh b/scripts/healthchecks/postgres.sh new file mode 100755 index 0000000..2994167 --- /dev/null +++ b/scripts/healthchecks/postgres.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -eo pipefail + +host="$(hostname -i || echo '127.0.0.1')" +user="${POSTGRES_USER:-postgres}" +db="${POSTGRES_DB:-$POSTGRES_USER}" +export PGPASSWORD="${POSTGRES_PASSWORD:-}" + +args=( + # force postgres to not use the local unix socket (test "external" connectibility) + --host "$host" + --username "$user" + --dbname "$db" + --quiet --no-align --tuples-only +) + +if select="$(echo 'SELECT 1' | psql "${args[@]}")" && [ "$select" = '1' ]; then + exit 0 +fi + +exit 1 diff --git a/scripts/healthcheck.js b/scripts/healthchecks/trackbear.js similarity index 100% rename from scripts/healthcheck.js rename to scripts/healthchecks/trackbear.js diff --git a/scripts/sqlite2pg.sh b/scripts/sqlite2pg.sh new file mode 100755 index 0000000..b018273 --- /dev/null +++ b/scripts/sqlite2pg.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +sed -E \ + -e 's/^(.*PRAGMA.*)$/-- \1/' \ + -e 's/^INSERT INTO sqlite_sequence VALUES\('\''(.*)'\'',(.*)\);$/SELECT setval('\''"\1_id_seq"'\''::regclass,\2);/' \ + -e 's/(INSERT INTO )([^"]+)( VALUES)/\1"\2"\3/' \ + -e 's/1704516508/1704516508000/g' \ + -e 's/([0-9]{10})([0-9]{3})/to_timestamp(\1.\2)/g' \ + -e 's/^(INSERT INTO "User".*),(0|1),(.*)$/\1,\2 = 1,\3/' \ + -e 's/^(INSERT INTO "Project".*),(0|1),(to_timestamp.*)$/\1,\2 = 1,\3/' \ + -e 's/^(INSERT INTO "Banner".*),(0|1),(to_timestamp.*)$/\1,\2 = 1,\3/' \ + -e 's/^(.*DELETE FROM.*)$/-- \1/' \ + -e 's/^(.*CREATE (UNIQUE )?INDEX.*)$/-- \1/' + +# -e 's/^INSERT INTO sqlite_sequence VALUES\(\x27(.*)\x27,(.*)\);$/SELECT setval(\x27"\1_id_seq"\x27::regclass,\2);/' \ diff --git a/server/lib/env.ts b/server/lib/env.ts index 564dd84..bcc02f0 100644 --- a/server/lib/env.ts +++ b/server/lib/env.ts @@ -15,6 +15,11 @@ type TrackbearCommonEnv = { LOG_PATH: string; LOG_LEVEL: string; + DATABASE_USER: string; + DATABASE_PASSWORD: string; + DATABASE_NAME: string; + DATABASE_HOST: string; + DB_PATH: string; COOKIE_SECRET: string; @@ -83,6 +88,11 @@ async function normalizeEnv(): Promise { if(!['', 'debug', 'info', 'warn', 'error', 'critical'].includes(process.env.LOG_LEVEL)) { throw new Error('LOG_LEVEL should be one of: `debug`, `info`, `warn`, `error`, `critical`'); } process.env.LOG_LEVEL = process.env.LOG_LEVEL || 'info'; + if(!process.env.DATABASE_USER) { throw new Error('Missing DATABASE_USER value in .env'); } + if(!process.env.DATABASE_PASSWORD) { throw new Error('Missing DATABASE_PASSWORD value in .env'); } + if(!process.env.DATABASE_NAME) { throw new Error('Missing DATABASE_NAME value in .env'); } + if(!process.env.DATABASE_HOST) { throw new Error('Missing DATABASE_HOST value in .env'); } + process.env.DB_PATH = process.env.DB_PATH || '/db'; if(!process.env.COOKIE_SECRET) { throw new Error('Missing COOKIE_SECRET value in .env'); } @@ -105,6 +115,11 @@ async function normalizeEnv(): Promise { LOG_PATH: process.env.LOG_PATH, LOG_LEVEL: process.env.LOG_LEVEL, + DATABASE_USER: process.env.DATABASE_USER, + DATABASE_PASSWORD: process.env.DATABASE_PASSWORD, + DATABASE_NAME: process.env.DATABASE_NAME, + DATABASE_HOST: process.env.DATABASE_HOST, + DB_PATH: process.env.DB_PATH, COOKIE_SECRET: process.env.COOKIE_SECRET, diff --git a/server/lib/queue.ts b/server/lib/queue.ts index 6b4669a..9f57dee 100644 --- a/server/lib/queue.ts +++ b/server/lib/queue.ts @@ -1,9 +1,7 @@ -import path from 'path'; import { getNormalizedEnv } from './env.ts'; - import Queue from 'better-queue'; -import SqliteStore from 'better-queue-sqlite'; +// import SqliteStore from 'better-queue-sqlite'; import sendSignupEmailTask from './tasks/send-signup-email.ts'; import sendPwchangeEmailTask from './tasks/send-pwchange-email.ts'; @@ -18,16 +16,22 @@ export type HandlerCallback = (error: Error | null, result: unknown) => void; let q: Queue | null = null; async function initQueue() { const env = await getNormalizedEnv(); - const queueDbPath = path.resolve(env.DB_PATH, './queue.db'); q = new Queue(function(task, cb) { taskHandler(task) .then(result => cb(null, result)) .catch(err => cb(err)); }, { - store: new SqliteStore({ - path: queueDbPath - }), + store: { + type: 'sql', + dialect: 'postgres', + host: env.DATABASE_HOST, + port: 5432, + username: env.DATABASE_USER, + password: env.DATABASE_PASSWORD, + dbname: 'queue', + tableName: 'tasks' + }, cancelIfRunning: true, autoResume: true, batchSize: 1,