From 431d24f7ad0c566dfd6c9955343d316fe1111ea6 Mon Sep 17 00:00:00 2001 From: Sampo Silvennoinen Date: Sun, 27 Oct 2024 13:08:22 +0200 Subject: [PATCH] Add MariaDB module --- docs/modules/mariadb.md | 29 ++++ mkdocs.yml | 1 + package-lock.json | 58 ++++++++ packages/modules/mariadb/jest.config.ts | 11 ++ packages/modules/mariadb/package.json | 37 +++++ packages/modules/mariadb/src/index.ts | 1 + .../mariadb/src/mariadb-container.test.ts | 128 ++++++++++++++++++ .../modules/mariadb/src/mariadb-container.ts | 96 +++++++++++++ packages/modules/mariadb/tsconfig.build.json | 13 ++ packages/modules/mariadb/tsconfig.json | 21 +++ 10 files changed, 395 insertions(+) create mode 100644 docs/modules/mariadb.md create mode 100644 packages/modules/mariadb/jest.config.ts create mode 100644 packages/modules/mariadb/package.json create mode 100644 packages/modules/mariadb/src/index.ts create mode 100644 packages/modules/mariadb/src/mariadb-container.test.ts create mode 100644 packages/modules/mariadb/src/mariadb-container.ts create mode 100644 packages/modules/mariadb/tsconfig.build.json create mode 100644 packages/modules/mariadb/tsconfig.json diff --git a/docs/modules/mariadb.md b/docs/modules/mariadb.md new file mode 100644 index 000000000..aefc08af1 --- /dev/null +++ b/docs/modules/mariadb.md @@ -0,0 +1,29 @@ +# MariaDB Module + +[MariaDB](https://mariadb.org/) is one of the most popular open source relational databases. It’s made by the original developers of MySQL and guaranteed to stay open source. It is part of most cloud offerings and the default in most Linux distributions. + + + +## Install + +```bash +npm install @testcontainers/mariadb --save-dev +``` + +## Examples + + +[Connect and execute query:](../../packages/modules/mariadb/src/mariadb-container.test.ts) inside_block:connect + + + +[Connect and execute query using URI:](../../packages/modules/mariadb/src/mariadb-container.test.ts) inside_block:uriConnect + + + +[Set username:](../../packages/modules/mariadb/src/mariadb-container.test.ts) inside_block:setUsername + + + +[Insert & fetch data:](../../packages/modules/mariadb/src/mariadb-container.test.ts) inside_block:insertAndFetchData + diff --git a/mkdocs.yml b/mkdocs.yml index c64c19f31..3bf5c5077 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,6 +50,7 @@ nav: - HiveMQ: modules/hivemq.md - Kafka: modules/kafka.md - Localstack: modules/localstack.md + - MariaDB: modules/mariadb.md - MongoDB: modules/mongodb.md - MSSQLServer: modules/mssqlserver.md - MySQL: modules/mysql.md diff --git a/package-lock.json b/package-lock.json index 5195a21dd..7281d70f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5285,6 +5285,10 @@ "resolved": "packages/modules/localstack", "link": true }, + "node_modules/@testcontainers/mariadb": { + "resolved": "packages/modules/mariadb", + "link": true + }, "node_modules/@testcontainers/mongodb": { "resolved": "packages/modules/mongodb", "link": true @@ -5589,6 +5593,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -13877,6 +13887,43 @@ "tmpl": "1.0.5" } }, + "node_modules/mariadb": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.4.0.tgz", + "integrity": "sha512-hdRPcAzs+MTxK5VG1thBW18gGTlw6yWBe9YnLB65GLo7q0fO5DWsgomIevV/pXSaWRmD3qi6ka4oSFRTExRiEQ==", + "dev": true, + "dependencies": { + "@types/geojson": "^7946.0.14", + "@types/node": "^22.5.4", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.3.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mariadb/node_modules/@types/node": { + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/mariadb/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/mariadb/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -19743,6 +19790,17 @@ "@aws-sdk/client-s3": "^3.614.0" } }, + "packages/modules/mariadb": { + "name": "@testcontainers/mariadb", + "version": "10.13.2", + "license": "MIT", + "dependencies": { + "testcontainers": "^10.13.2" + }, + "devDependencies": { + "mariadb": "^3.4.0" + } + }, "packages/modules/mongodb": { "name": "@testcontainers/mongodb", "version": "10.13.2", diff --git a/packages/modules/mariadb/jest.config.ts b/packages/modules/mariadb/jest.config.ts new file mode 100644 index 000000000..1f677baaf --- /dev/null +++ b/packages/modules/mariadb/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "jest"; +import * as path from "path"; + +const config: Config = { + preset: "ts-jest", + moduleNameMapper: { + "^testcontainers$": path.resolve(__dirname, "../../testcontainers/src"), + }, +}; + +export default config; diff --git a/packages/modules/mariadb/package.json b/packages/modules/mariadb/package.json new file mode 100644 index 000000000..43bb6506e --- /dev/null +++ b/packages/modules/mariadb/package.json @@ -0,0 +1,37 @@ +{ + "name": "@testcontainers/mariadb", + "version": "10.13.2", + "license": "MIT", + "keywords": [ + "mariadb", + "testing", + "docker", + "testcontainers" + ], + "description": "MariaDB module for Testcontainers", + "homepage": "https://github.com/testcontainers/testcontainers-node#readme", + "repository": { + "type": "git", + "url": "https://github.com/testcontainers/testcontainers-node" + }, + "bugs": { + "url": "https://github.com/testcontainers/testcontainers-node/issues" + }, + "main": "build/index.js", + "files": [ + "build" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepack": "shx cp ../../../README.md . && shx cp ../../../LICENSE .", + "build": "tsc --project tsconfig.build.json" + }, + "dependencies": { + "testcontainers": "^10.13.2" + }, + "devDependencies": { + "mariadb": "^3.4.0" + } +} diff --git a/packages/modules/mariadb/src/index.ts b/packages/modules/mariadb/src/index.ts new file mode 100644 index 000000000..164bf668a --- /dev/null +++ b/packages/modules/mariadb/src/index.ts @@ -0,0 +1 @@ +export { MariaDbContainer, StartedMariaDbContainer } from "./mariadb-container"; diff --git a/packages/modules/mariadb/src/mariadb-container.test.ts b/packages/modules/mariadb/src/mariadb-container.test.ts new file mode 100644 index 000000000..534f5bbbf --- /dev/null +++ b/packages/modules/mariadb/src/mariadb-container.test.ts @@ -0,0 +1,128 @@ +import mariadb from "mariadb"; +import { MariaDbContainer } from "./mariadb-container"; + +describe("MariaDb", () => { + jest.setTimeout(240_000); + + // connect { + it("should connect and execute query", async () => { + const container = await new MariaDbContainer().start(); + + const client = await mariadb.createConnection({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getUserPassword(), + }); + + const rows = await client.query("SELECT 1 as res"); + expect(rows).toEqual([{ res: 1 }]); + + await client.end(); + await container.stop(); + }); + // } + + // uriConnect { + it("should work with database URI", async () => { + const username = "testUser"; + const password = "testPassword"; + const database = "testDB"; + + // Test non-root user + const container = await new MariaDbContainer() + .withUsername(username) + .withUserPassword(password) + .withDatabase(database) + .start(); + expect(container.getConnectionUri()).toEqual( + `mariadb://${username}:${password}@${container.getHost()}:${container.getPort()}/${database}` + ); + await container.stop(); + + // Test root user + const rootContainer = await new MariaDbContainer().withRootPassword(password).withDatabase(database).start(); + expect(rootContainer.getConnectionUri(true)).toEqual( + `mariadb://root:${password}@${rootContainer.getHost()}:${rootContainer.getPort()}/${database}` + ); + await rootContainer.stop(); + }); + // } + + // setDatabase { + it("should set database", async () => { + const container = await new MariaDbContainer().withDatabase("customDatabase").start(); + + const client = await mariadb.createConnection({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getUserPassword(), + }); + + const rows = await client.query("SELECT DATABASE() as res"); + expect(rows).toEqual([{ res: "customDatabase" }]); + + await client.end(); + await container.stop(); + }); + // } + + // setUsername { + it("should set username", async () => { + const container = await new MariaDbContainer().withUsername("customUsername").start(); + + const client = await mariadb.createConnection({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getUserPassword(), + }); + + const rows = await client.query("SELECT CURRENT_USER() as res"); + expect(rows).toEqual([{ res: "customUsername@%" }]); + + await client.end(); + await container.stop(); + }); + // } + + // insertAndFetchData { + it("should create a table, insert a row, and fetch that row", async () => { + const container = await new MariaDbContainer().start(); + + const client = await mariadb.createConnection({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getUserPassword(), + }); + + // Create table + await client.query(` + CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE + ); + `); + + // Insert a row + const name = "John Doe"; + const email = "john.doe@example.com"; + const insertResult = await client.query("INSERT INTO users (name, email) VALUES (?, ?)", [name, email]); + expect(insertResult.affectedRows).toBe(1); + + // Fetch the row + const [user] = await client.query("SELECT id, name, email FROM users WHERE email = ?", [email]); + expect(user).toEqual({ id: expect.any(Number), name, email }); + + await client.end(); + await container.stop(); + }); + // } +}); diff --git a/packages/modules/mariadb/src/mariadb-container.ts b/packages/modules/mariadb/src/mariadb-container.ts new file mode 100644 index 000000000..3a34ca929 --- /dev/null +++ b/packages/modules/mariadb/src/mariadb-container.ts @@ -0,0 +1,96 @@ +import { AbstractStartedContainer, GenericContainer, StartedTestContainer } from "testcontainers"; + +const MARIADB_PORT = 3306; + +export class MariaDbContainer extends GenericContainer { + private database = "test"; + private username = "test"; + private userPassword = "test"; + private rootPassword = "test"; + + constructor(image = "mariadb:11.5.2") { + super(image); + this.withExposedPorts(MARIADB_PORT).withStartupTimeout(120_000); + } + + public withDatabase(database: string): this { + this.database = database; + return this; + } + + public withUsername(username: string): this { + this.username = username; + return this; + } + + public withRootPassword(rootPassword: string): this { + this.rootPassword = rootPassword; + return this; + } + + public withUserPassword(userPassword: string): this { + this.userPassword = userPassword; + return this; + } + + public override async start(): Promise { + this.withEnvironment({ + MARIADB_DATABASE: this.database, + MARIADB_ROOT_PASSWORD: this.rootPassword, + MARIADB_USER: this.username, + MARIADB_PASSWORD: this.userPassword, + }); + return new StartedMariaDbContainer( + await super.start(), + this.database, + this.username, + this.userPassword, + this.rootPassword + ); + } +} + +export class StartedMariaDbContainer extends AbstractStartedContainer { + private readonly port: number; + + constructor( + startedTestContainer: StartedTestContainer, + private readonly database: string, + private readonly username: string, + private readonly userPassword: string, + private readonly rootPassword: string + ) { + super(startedTestContainer); + this.port = startedTestContainer.getMappedPort(MARIADB_PORT); + } + + public getPort(): number { + return this.port; + } + + public getDatabase(): string { + return this.database; + } + + public getUsername(): string { + return this.username; + } + + public getUserPassword(): string { + return this.userPassword; + } + + public getRootPassword(): string { + return this.rootPassword; + } + + public getConnectionUri(isRoot = false): string { + const url = new URL("", "mariadb://"); + url.hostname = this.getHost(); + url.port = this.getPort().toString(); + url.pathname = this.getDatabase(); + url.username = isRoot ? "root" : this.getUsername(); + url.password = isRoot ? this.getRootPassword() : this.getUserPassword(); + return url.toString(); + } +} diff --git a/packages/modules/mariadb/tsconfig.build.json b/packages/modules/mariadb/tsconfig.build.json new file mode 100644 index 000000000..0222f6ff1 --- /dev/null +++ b/packages/modules/mariadb/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "build", + "jest.config.ts", + "src/**/*.test.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file diff --git a/packages/modules/mariadb/tsconfig.json b/packages/modules/mariadb/tsconfig.json new file mode 100644 index 000000000..39b165817 --- /dev/null +++ b/packages/modules/mariadb/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "paths": { + "testcontainers": [ + "../../testcontainers/src" + ] + } + }, + "exclude": [ + "build", + "jest.config.ts" + ], + "references": [ + { + "path": "../../testcontainers" + } + ] +} \ No newline at end of file