Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Design] Add Postgres client management code #5801

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/design/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"build:self": "tsc --build tsconfig.build.json",
"clean": "pnpm --filter $npm_package_name... clean:self",
"clean:self": "rm -rf build && tsc --build --clean tsconfig.build.json",
"db:reset": "./scripts/db_reset_dev.sh",
"format": "prettier '**/*.+(css|graphql|json|less|md|mdx|sass|scss|yaml|yml)' --write",
"lint": "pnpm type-check && eslint .",
"lint:fix": "pnpm type-check && eslint . --fix",
Expand Down Expand Up @@ -47,6 +48,7 @@
"fs-extra": "11.1.1",
"js-sha256": "^0.9.0",
"jszip": "^3.9.1",
"pg": "^8.13.1",
"react": "18.3.1",
"uuid": "9.0.1",
"zod": "3.23.5"
Expand All @@ -61,6 +63,7 @@
"@types/jest-image-snapshot": "^6.4.0",
"@types/lodash.get": "^4.4.9",
"@types/node": "20.16.0",
"@types/pg": "^8.11.10",
"@types/react": "18.3.3",
"@types/tmp": "0.2.4",
"@types/uuid": "9.0.5",
Expand Down
39 changes: 39 additions & 0 deletions apps/design/backend/schema.old.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
create table elections (
id text primary key,
election_data text not null,
system_settings_data text not null,
precinct_data text not null,
created_at timestamp not null default current_timestamp,
election_package_task_id text,
election_package_url text,
foreign key (election_package_task_id) references background_tasks(id)
on delete set null
);

create table background_tasks (
id text primary key,
task_name text not null,
payload text not null,
created_at timestamp not null default current_timestamp,
started_at timestamp,
completed_at timestamp,
error text
);

create table translation_cache (
source_text text not null,
target_language_code text not null,
translated_text text not null
);

create unique index idx_translation_cache on translation_cache (
source_text,
target_language_code
);

create table speech_synthesis_cache (
language_code text not null,
source_text text not null,
audio_clip_base64 text not null,
primary key (language_code, source_text)
);
23 changes: 11 additions & 12 deletions apps/design/backend/schema.sql
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
create table elections (
id text primary key,
election_data text not null,
system_settings_data text not null,
precinct_data text not null,
created_at timestamp not null default current_timestamp,
election_package_task_id text,
election_package_url text,
foreign key (election_package_task_id) references background_tasks(id)
on delete set null
);

create table background_tasks (
id text primary key,
task_name text not null,
Expand All @@ -20,6 +8,17 @@ create table background_tasks (
error text
);

create table elections (
id text primary key,
election_data text not null,
system_settings_data text not null,
precinct_data text not null,
created_at timestamp not null default current_timestamp,
election_package_task_id text
constraint fk_background_tasks references background_tasks(id) on delete set null,
election_package_url text
);

create table translation_cache (
source_text text not null,
target_language_code text not null,
Expand Down
22 changes: 22 additions & 0 deletions apps/design/backend/scripts/db_reset_dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

# [TODO] Move schema management to migration tool
# (e.g. https://salsita.github.io/node-pg-migrate/)

SCRIPT_DIR="$(dirname "$0")"

if [[ -z $(which psql) ]]; then
echo "🔴 [ERROR] psql not found - you may need to run install postgres first:"
echo " > sudo apt install postgresql"
echo ""
exit 1
fi

sudo systemctl start postgresql

sudo -u postgres psql -c "drop database design;"
sudo -u postgres psql -c "drop user design;"

sudo -u postgres psql -c "create user design password 'design';" &&
sudo -u postgres psql -c "create database design with owner design;" &&
sudo -u postgres psql -d design -f "${SCRIPT_DIR}/../schema.sql"
87 changes: 87 additions & 0 deletions apps/design/backend/src/db/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* istanbul ignore file - [TODO] need to update CI image to include postgres. @preserve */

import { Buffer } from 'node:buffer';
import * as pg from 'pg';

/**
* Types supported for query value substitution.
*/
export type Bindable = string | number | bigint | Buffer | null;

/**
* Manages a client connection to a PostgreSQL database.
*/
export class Client {
constructor(private readonly conn: pg.PoolClient) {}

/**
* Usage:
* ```
* const kind = 'dog';
* const name = 'Scooby';
*
* const res = await client.query(
* 'select id, age from pets where kind = $1 and name = $2',
* kind,
* name,
* );
*
* for (row of res.rows) {
* console.log(`${row.id}: ${row.age}`);
* }
* ```
*/
query(
sql: string,
...values: pg.QueryConfigValues<Bindable[]>
): Promise<pg.QueryResult> {
return this.conn.query(sql, values);
}

/**
* Runs the given query as a prepared statement identified by the given
* `name`. Provides improved performance for frequently used queries.
*
* Usage:
* ```
* const kind = 'dog';
* const name = 'Scooby';
*
* const res = await client.query({
* name: 'petsByKindAndName',
* text: 'select id, age from pets where kind = $1 and name = $2',
* values: [kind, name],
* });
*
* for (row of res.rows) {
* console.log(`${row.id}: ${row.age}`);
* }
* ```
*/
queryPrepared(config: pg.QueryConfig<Bindable[]>): Promise<pg.QueryResult> {
return this.conn.query(config);
}

async withTransaction(fn: () => Promise<boolean>): Promise<boolean> {
await this.query('begin');

let successful = false;

try {
successful = await fn();
return successful;
} catch (error) {
await this.query('rollback').catch((errRollback) => {
throw errRollback;
});

throw error;
} finally {
if (successful) {
await this.query('commit');
} else {
await this.query('rollback');
}
}
}
}
49 changes: 49 additions & 0 deletions apps/design/backend/src/db/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* istanbul ignore file - [TODO] need to update CI image to include postgres. @preserve */

// [TODO] Move to separate libs/ package once it's stable/cleaned up.

import {
BaseLogger,
LogDispositionStandardTypes,
LogEventId,
} from '@votingworks/logging';
import makeDebug from 'debug';
import * as pg from 'pg';
import { Client } from './client';

const debug = makeDebug('pg-client');

/**
* Manages a pool of connections to a PostgreSQL database.
*/
export class Db {
private readonly pool: pg.Pool;

constructor(private readonly logger: BaseLogger) {
this.pool = new pg.Pool({
ssl: { rejectUnauthorized: false },
});
this.pool.on('error', (error) => {
void this.logger.log(
LogEventId.UnknownError, // [TODO] Figure out logging/reporting
'system',
{
disposition: LogDispositionStandardTypes.Failure,
message: `Postgres client error: ${error}`,
},
debug
);
});
}

async withClient<T>(fn: (client: Client) => Promise<T>): Promise<T> {
const poolClient = await this.pool.connect();
const client = new Client(await this.pool.connect());

try {
return await fn(client);
} finally {
poolClient.release();
}
}
}
2 changes: 1 addition & 1 deletion apps/design/backend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ function hydrateElection(row: {
};
}

const SchemaPath = join(__dirname, '../schema.sql');
const SchemaPath = join(__dirname, '../schema.old.sql');

export class Store {
private constructor(private readonly client: DbClient) {}
Expand Down
Loading