@compas/store
Version:
Postgres & S3-compatible wrappers for common things
153 lines (140 loc) • 4.14 kB
JavaScript
import { environment, isNil, isProduction, merge } from "@compas/stdlib";
import postgres from "postgres";
/**
* @param {import("postgres").Options|undefined} opts
* @returns {import("postgres").Options}
*/
export function buildAndCheckOpts(opts) {
const finalOpts = /** @type {postgres.Options} */ merge(
{
connection: {
application_name: environment.APP_NAME,
},
no_prepare: true,
ssl: isProduction() ? "require" : "prefer",
database:
environment.POSTGRES_DATABASE ??
environment.PGDATABASE ??
environment.APP_NAME,
user:
environment.POSTGRES_USER ??
environment.PGUSERNAME ??
environment.PGUSER,
password: environment.POSTGRES_PASSWORD ?? environment.PGPASSWORD,
host: environment.POSTGRES_HOST ?? environment.PGHOST,
port: environment.POSTGRES_PORT ?? environment.PGPORT,
max: 15,
types: {
// Used by
// `T.date().dateOnly()`
// and
// `T.date().timeOnly()`
dateOrTimeOnly: {
to: 25,
from: [1082, 1083],
serialize: (x) => x,
parse: (x) => x,
},
jsonb: {
// PG oid for jsonb
to: 3802,
from: [3802],
serialize: (value) => value,
parse: (x) => (x ? JSON.parse(x) : undefined),
},
},
},
opts,
);
if (isNil(finalOpts.host) && isNil(environment.POSTGRES_URI)) {
throw new Error(
`One of 'host' option, 'POSTGRES_HOST' environment variable or the 'POSTGRES_URI' environment variable is required.`,
);
}
return finalOpts;
}
/**
* Create a new postgres connection, using the default environment variables.
* A database may be created using the provided credentials.
*
* Note that by default we add a 'dateOrTimeOnly' type, which serializes and parses
* 'date' and 'time' columns, used by `T.date().timeOnly()` and `T.date().dateOnly()', as
* strings.
*
* With `createIfNotExists`, Compas will try to connect to Postgres and first check if
* the database exists before establishing the requested connection. Postgres will
* default to connect to a database with the same name as the provided user, but you can
* manually specify the 'maintenanceDatabase' for the temporary connection.
*
* @since 0.1.0
*
* @param {import("postgres").Options & {
* createIfNotExists?: boolean,
* maintenanceDatabaase?: string,
* }} [opts]
* @returns {Promise<import("postgres").Sql<{}>>}
*/
export async function newPostgresConnection(opts) {
const connectionOpts = buildAndCheckOpts(opts);
if (opts?.createIfNotExists) {
const oldConnection = await createDatabaseIfNotExists(
undefined,
connectionOpts.database,
opts.maintenanceDatabaase,
undefined,
connectionOpts,
);
setImmediate(() => oldConnection.end());
}
return postgres(environment.POSTGRES_URI ?? connectionOpts, connectionOpts);
}
/**
* @param sql
* @param databaseName
* @param maintenanceDatabase
* @param template
* @param connectionOptions
* @returns {Promise<import("postgres").Sql<{}>>}
*/
export async function createDatabaseIfNotExists(
sql,
databaseName,
maintenanceDatabase,
template,
connectionOptions,
) {
if (isNil(databaseName)) {
throw new Error(
"The 'database' option, 'POSTGRES_DATABASE' environment variable or 'APP_NAME' environment variable is required.",
);
}
// Don't connect to a database
const opts = {
...connectionOptions,
database: maintenanceDatabase ?? undefined,
};
if (!sql) {
sql = postgres(environment.POSTGRES_URI ?? opts, opts);
}
const [db] = await sql`
SELECT datname
FROM pg_database
WHERE
datname = ${databaseName}
`;
if (!db?.datname) {
if (template) {
await sql`
CREATE DATABASE ${sql(databaseName)} WITH TEMPLATE ${sql(
template,
)} OWNER ${sql(sql.options.user)}
`;
} else {
await sql`
CREATE DATABASE ${sql(databaseName)} WITH OWNER ${sql(sql.options.user)}
`;
}
}
return sql;
}
export { postgres };