diff --git a/containers/postgres.ts b/containers/postgres.ts index 9eb2268..f453504 100644 --- a/containers/postgres.ts +++ b/containers/postgres.ts @@ -19,18 +19,27 @@ */ import { delay } from "@std/async/delay"; +import { getAvailablePort } from "@std/net"; import psql, { type Sql } from "postgres"; import type { Container } from "../docker/libraries/container.ts"; -import getPort from "../docker/libraries/port.ts"; import { docker } from "../mod.ts"; +/** + * Provides a simplified utility layer for starting, operating, and shutting down a + * postgres docker container. + * + * Will automatically pull the requested docker image before starting the container. + */ export class PostgresTestContainer { + readonly #connection: PostgresConnectionInfo; + private constructor( readonly container: Container, - readonly port: number, - readonly config: Config, - ) {} + connection: PostgresConnectionInfo, + ) { + this.#connection = connection; + } /* |-------------------------------------------------------------------------------- @@ -39,15 +48,31 @@ export class PostgresTestContainer { */ /** - * Connection info for the Postgres container. + * PostgreSQL container host. */ - get connectionInfo(): PostgresConnectionInfo { - return { - host: "127.0.0.1", - port: this.port, - user: this.config.username, - pass: this.config.password, - }; + get host(): string { + return this.#connection.host; + } + + /** + * PostgreSQL container port. + */ + get port(): number { + return this.#connection.port; + } + + /** + * PostgreSQL username applied to the container. + */ + get username(): string { + return this.#connection.user; + } + + /** + * PostgreSQL password applied to the container. + */ + get password(): string { + return this.#connection.pass; } /** @@ -68,8 +93,8 @@ export class PostgresTestContainer { * * @param config - Options for the Postgres container. */ - static async start(image: string, config: Partial = {}): Promise { - const port = getPort(); + static async start(image: string, config: Partial = {}): Promise { + const port = getAvailablePort({ preferredPort: config.port }); if (port === undefined) { throw new Error("Unable to assign to a random port"); } @@ -78,7 +103,7 @@ export class PostgresTestContainer { const container = await docker.createContainer({ Image: image, - Env: [`POSTGRES_USER=${config.username ?? "postgres"}`, `POSTGRES_PASSWORD=${config.password ?? "postgres"}`], + Env: [`POSTGRES_USER=${config.user ?? "postgres"}`, `POSTGRES_PASSWORD=${config.pass ?? "postgres"}`], ExposedPorts: { "5432/tcp": {}, }, @@ -92,9 +117,11 @@ export class PostgresTestContainer { await delay(250); - return new PostgresTestContainer(container, port, { - username: config.username ?? "postgres", - password: config.password ?? "postgres", + return new PostgresTestContainer(container, { + host: config.host ?? "127.0.0.1", + port, + user: config.user ?? "postgres", + pass: config.pass ?? "postgres", }); } @@ -117,7 +144,7 @@ export class PostgresTestContainer { * @param name - Name of the database to create. */ async create(name: string): Promise { - await this.exec(["createdb", `--username=${this.config.username}`, name]); + await this.exec(["createdb", `--username=${this.username}`, name]); } /** @@ -140,8 +167,10 @@ export class PostgresTestContainer { * @param name - Name of the database to connect to. * @param options - Connection options to append to the URL. */ - url(name: string, options?: PostgresConnectionOptions): string { - return getConnectionUrl({ ...this.connectionInfo, name, options }); + url(name: string, options?: PostgresConnectionOptions): PostgresConnectionUrl { + return `postgres://${this.username}:${this.password}@${this.host}:${this.port}/${name}${ + postgresOptionsToString(options) + }`; } } @@ -151,12 +180,6 @@ export class PostgresTestContainer { |-------------------------------------------------------------------------------- */ -function getConnectionUrl( - { host, port, user, pass, name, options }: ConnectionUrlConfig, -): PostgresConnectionUrl { - return `postgres://${user}:${pass}@${host}:${port}/${name}${postgresOptionsToString(options)}`; -} - function postgresOptionsToString(options?: PostgresConnectionOptions) { if (options === undefined) { return ""; @@ -181,18 +204,8 @@ function assertPostgresOptionKey(key: string): asserts key is keyof PostgresConn |-------------------------------------------------------------------------------- */ -type Config = { - username: string; - password: string; -}; - type PostgresConnectionUrl = `postgres://${string}:${string}@${string}:${number}/${string}`; -type ConnectionUrlConfig = { - name: string; - options?: PostgresConnectionOptions; -} & PostgresConnectionInfo; - type PostgresConnectionOptions = { schema?: string; }; diff --git a/deno.json b/deno.json index 8f263ad..d51a161 100644 --- a/deno.json +++ b/deno.json @@ -8,7 +8,8 @@ "imports": { "@std/assert": "jsr:@std/assert@^1.0.0", "@std/async": "jsr:@std/async@1.0.0", - "@std/testing": "jsr:@std/testing@^0.225.3", + "@std/net": "jsr:@std/net@0.224.5", + "@std/testing": "jsr:@std/testing@0.225.3", "postgres": "npm:postgres@3.4.4" }, "exclude": [ diff --git a/deno.lock b/deno.lock index de5e6fd..2c03ded 100644 --- a/deno.lock +++ b/deno.lock @@ -5,7 +5,8 @@ "jsr:@std/assert@^1.0.0": "jsr:@std/assert@1.0.0", "jsr:@std/async@1.0.0": "jsr:@std/async@1.0.0", "jsr:@std/internal@^1.0.1": "jsr:@std/internal@1.0.1", - "jsr:@std/testing@^0.225.3": "jsr:@std/testing@0.225.3", + "jsr:@std/net@0.224.5": "jsr:@std/net@0.224.5", + "jsr:@std/testing@0.225.3": "jsr:@std/testing@0.225.3", "npm:@types/node": "npm:@types/node@18.16.19", "npm:postgres@3.4.4": "npm:postgres@3.4.4" }, @@ -22,6 +23,9 @@ "@std/internal@1.0.1": { "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" }, + "@std/net@0.224.5": { + "integrity": "9c2ae90a5c3dc7771da5ae5e13b6f7d5d0b316c1954c5d53f2bfc1129fb757ff" + }, "@std/testing@0.225.3": { "integrity": "348c24d0479d44ab3dbb4f26170f242e19f24051b45935d4a9e7ca0ab7e37780" } @@ -79,7 +83,8 @@ "dependencies": [ "jsr:@std/assert@^1.0.0", "jsr:@std/async@1.0.0", - "jsr:@std/testing@^0.225.3", + "jsr:@std/net@0.224.5", + "jsr:@std/testing@0.225.3", "npm:postgres@3.4.4" ] } diff --git a/docker/libraries/port.ts b/docker/libraries/port.ts deleted file mode 100644 index dd875df..0000000 --- a/docker/libraries/port.ts +++ /dev/null @@ -1,92 +0,0 @@ -export const MIN_PORT = 1024; -export const MAX_PORT = 65535; - -/** - * Try run listener to check if port is open. - * - * @param options - Deno listen options. - */ -export function checkPort(options: Deno.ListenOptions): CheckedPort { - const { port } = options; - try { - Deno.listen(options).close(); - return { valid: true, port: port }; - } catch (e) { - if (e.name !== "AddrInUse") { - throw e; - } - return { valid: false, port: port }; - } -} - -/** - * Create an array of number by min and max. - * - * @param from - Must be between 1024 and 65535 - * @param to - Must be between 1024 and 65535 and greater than from - */ -export function makeRange(from: number, to: number): number[] { - if (!(from > MIN_PORT || from < MAX_PORT)) { - throw new RangeError("`from` must be between 1024 and 65535"); - } - if (!(to > MIN_PORT || to < MAX_PORT)) { - throw new RangeError("`to` must be between 1024 and 65536"); - } - if (!(to > from)) { - throw new RangeError("`to` must be greater than or equal to `from`"); - } - const ports = []; - for (let port = from; port <= to; port++) { - ports.push(port); - } - return ports; -} - -/** - * Return a random port between 1024 and 65535. - * - * @param hostname - Hostname to check for port availability under. (Optional) - */ -export function randomPort(hostname?: string): number { - const port = Math.ceil(Math.random() * ((MAX_PORT - 1) - MIN_PORT + 1) + MIN_PORT + 1); - const result: CheckedPort = checkPort({ hostname, port }); - if (result.valid) { - return result.port; - } - return randomPort(hostname); -} - -/** - * Return available port. - * - * @param port - Wanted port, or list of ports. (Optional) - * @param hostname - Hostname to check for availability under. (Optional) - */ -export default function getPort(port?: number | number[], hostname?: string): number { - const listenOptions: Deno.ListenOptions = { - hostname: hostname || "0.0.0.0", - port: (port && !Array.isArray(port)) ? port : 0, - }; - - if (!port || Array.isArray(port)) { - const ports: number[] = (Array.isArray(port)) ? port : makeRange(MIN_PORT + 1, MAX_PORT - 1); - for (const port of ports) { - const result: CheckedPort = checkPort({ ...listenOptions, port }); - if (result.valid) return result.port; - } - return getPort(ports[ports.length - 1]); - } - - const result: CheckedPort = checkPort(listenOptions); - if (!result.valid) { - const range = makeRange(result.port + 1, MAX_PORT - 1); - return getPort(range); - } - - return result.port; -} - -export type CheckedPort = { - valid: boolean; - port: number; -}; diff --git a/mod.ts b/mod.ts index 660e25f..bb74a76 100644 --- a/mod.ts +++ b/mod.ts @@ -5,6 +5,5 @@ export type { Docker } from "./docker/libraries/docker.ts"; export type { Exec } from "./docker/libraries/exec.ts"; export type { Image } from "./docker/libraries/image.ts"; export { modem } from "./docker/libraries/modem.ts"; -export * from "./docker/libraries/port.ts"; export const docker: Docker = new Docker();