feat: use @std/net for port
This commit is contained in:
@@ -19,18 +19,27 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { delay } from "@std/async/delay";
|
import { delay } from "@std/async/delay";
|
||||||
|
import { getAvailablePort } from "@std/net";
|
||||||
import psql, { type Sql } from "postgres";
|
import psql, { type Sql } from "postgres";
|
||||||
|
|
||||||
import type { Container } from "../docker/libraries/container.ts";
|
import type { Container } from "../docker/libraries/container.ts";
|
||||||
import getPort from "../docker/libraries/port.ts";
|
|
||||||
import { docker } from "../mod.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 {
|
export class PostgresTestContainer {
|
||||||
|
readonly #connection: PostgresConnectionInfo;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly container: Container,
|
readonly container: Container,
|
||||||
readonly port: number,
|
connection: PostgresConnectionInfo,
|
||||||
readonly config: Config,
|
) {
|
||||||
) {}
|
this.#connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
@@ -39,15 +48,31 @@ export class PostgresTestContainer {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connection info for the Postgres container.
|
* PostgreSQL container host.
|
||||||
*/
|
*/
|
||||||
get connectionInfo(): PostgresConnectionInfo {
|
get host(): string {
|
||||||
return {
|
return this.#connection.host;
|
||||||
host: "127.0.0.1",
|
}
|
||||||
port: this.port,
|
|
||||||
user: this.config.username,
|
/**
|
||||||
pass: this.config.password,
|
* 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.
|
* @param config - Options for the Postgres container.
|
||||||
*/
|
*/
|
||||||
static async start(image: string, config: Partial<Config> = {}): Promise<PostgresTestContainer> {
|
static async start(image: string, config: Partial<PostgresConnectionInfo> = {}): Promise<PostgresTestContainer> {
|
||||||
const port = getPort();
|
const port = getAvailablePort({ preferredPort: config.port });
|
||||||
if (port === undefined) {
|
if (port === undefined) {
|
||||||
throw new Error("Unable to assign to a random port");
|
throw new Error("Unable to assign to a random port");
|
||||||
}
|
}
|
||||||
@@ -78,7 +103,7 @@ export class PostgresTestContainer {
|
|||||||
|
|
||||||
const container = await docker.createContainer({
|
const container = await docker.createContainer({
|
||||||
Image: image,
|
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: {
|
ExposedPorts: {
|
||||||
"5432/tcp": {},
|
"5432/tcp": {},
|
||||||
},
|
},
|
||||||
@@ -92,9 +117,11 @@ export class PostgresTestContainer {
|
|||||||
|
|
||||||
await delay(250);
|
await delay(250);
|
||||||
|
|
||||||
return new PostgresTestContainer(container, port, {
|
return new PostgresTestContainer(container, {
|
||||||
username: config.username ?? "postgres",
|
host: config.host ?? "127.0.0.1",
|
||||||
password: config.password ?? "postgres",
|
port,
|
||||||
|
user: config.user ?? "postgres",
|
||||||
|
pass: config.pass ?? "postgres",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +144,7 @@ export class PostgresTestContainer {
|
|||||||
* @param name - Name of the database to create.
|
* @param name - Name of the database to create.
|
||||||
*/
|
*/
|
||||||
async create(name: string): Promise<void> {
|
async create(name: string): Promise<void> {
|
||||||
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 name - Name of the database to connect to.
|
||||||
* @param options - Connection options to append to the URL.
|
* @param options - Connection options to append to the URL.
|
||||||
*/
|
*/
|
||||||
url(name: string, options?: PostgresConnectionOptions): string {
|
url(name: string, options?: PostgresConnectionOptions): PostgresConnectionUrl {
|
||||||
return getConnectionUrl({ ...this.connectionInfo, name, options });
|
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) {
|
function postgresOptionsToString(options?: PostgresConnectionOptions) {
|
||||||
if (options === undefined) {
|
if (options === undefined) {
|
||||||
return "";
|
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 PostgresConnectionUrl = `postgres://${string}:${string}@${string}:${number}/${string}`;
|
||||||
|
|
||||||
type ConnectionUrlConfig = {
|
|
||||||
name: string;
|
|
||||||
options?: PostgresConnectionOptions;
|
|
||||||
} & PostgresConnectionInfo;
|
|
||||||
|
|
||||||
type PostgresConnectionOptions = {
|
type PostgresConnectionOptions = {
|
||||||
schema?: string;
|
schema?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"imports": {
|
"imports": {
|
||||||
"@std/assert": "jsr:@std/assert@^1.0.0",
|
"@std/assert": "jsr:@std/assert@^1.0.0",
|
||||||
"@std/async": "jsr:@std/async@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"
|
"postgres": "npm:postgres@3.4.4"
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|||||||
9
deno.lock
generated
9
deno.lock
generated
@@ -5,7 +5,8 @@
|
|||||||
"jsr:@std/assert@^1.0.0": "jsr:@std/assert@1.0.0",
|
"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/async@1.0.0": "jsr:@std/async@1.0.0",
|
||||||
"jsr:@std/internal@^1.0.1": "jsr:@std/internal@1.0.1",
|
"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:@types/node": "npm:@types/node@18.16.19",
|
||||||
"npm:postgres@3.4.4": "npm:postgres@3.4.4"
|
"npm:postgres@3.4.4": "npm:postgres@3.4.4"
|
||||||
},
|
},
|
||||||
@@ -22,6 +23,9 @@
|
|||||||
"@std/internal@1.0.1": {
|
"@std/internal@1.0.1": {
|
||||||
"integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6"
|
"integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6"
|
||||||
},
|
},
|
||||||
|
"@std/net@0.224.5": {
|
||||||
|
"integrity": "9c2ae90a5c3dc7771da5ae5e13b6f7d5d0b316c1954c5d53f2bfc1129fb757ff"
|
||||||
|
},
|
||||||
"@std/testing@0.225.3": {
|
"@std/testing@0.225.3": {
|
||||||
"integrity": "348c24d0479d44ab3dbb4f26170f242e19f24051b45935d4a9e7ca0ab7e37780"
|
"integrity": "348c24d0479d44ab3dbb4f26170f242e19f24051b45935d4a9e7ca0ab7e37780"
|
||||||
}
|
}
|
||||||
@@ -79,7 +83,8 @@
|
|||||||
"dependencies": [
|
"dependencies": [
|
||||||
"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/testing@^0.225.3",
|
"jsr:@std/net@0.224.5",
|
||||||
|
"jsr:@std/testing@0.225.3",
|
||||||
"npm:postgres@3.4.4"
|
"npm:postgres@3.4.4"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
1
mod.ts
1
mod.ts
@@ -5,6 +5,5 @@ export type { Docker } from "./docker/libraries/docker.ts";
|
|||||||
export type { Exec } from "./docker/libraries/exec.ts";
|
export type { Exec } from "./docker/libraries/exec.ts";
|
||||||
export type { Image } from "./docker/libraries/image.ts";
|
export type { Image } from "./docker/libraries/image.ts";
|
||||||
export { modem } from "./docker/libraries/modem.ts";
|
export { modem } from "./docker/libraries/modem.ts";
|
||||||
export * from "./docker/libraries/port.ts";
|
|
||||||
|
|
||||||
export const docker: Docker = new Docker();
|
export const docker: Docker = new Docker();
|
||||||
|
|||||||
Reference in New Issue
Block a user