Skip to content

Commit

Permalink
Rework merging DB connection configuration (#214)
Browse files Browse the repository at this point in the history
- Ability to provide partialConfig object when using DB URI in constructor (Resolves: #196)
- Fix encoding password in MongoDB connection URI (Resolves: #209)
  • Loading branch information
pkosiec committed Dec 9, 2023
1 parent b9a1542 commit 256212f
Show file tree
Hide file tree
Showing 14 changed files with 429 additions and 298 deletions.
2 changes: 1 addition & 1 deletion cli/src/options.ts
Expand Up @@ -5,7 +5,7 @@ import {
CommandLineArguments,
PartialCliOptions,
} from './types';
import { SeederDatabaseConfigObjectOptions } from 'mongo-seeding/dist/database';
import { SeederDatabaseConfigObjectOptions } from 'mongo-seeding';
import { Seeder, SeederCollectionReadingOptions } from 'mongo-seeding';

export const DEFAULT_INPUT_PATH = './';
Expand Down
9 changes: 9 additions & 0 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/package.json
Expand Up @@ -59,6 +59,7 @@
},
"dependencies": {
"bson": "^6.1.0",
"connection-string": "^4.4.0",
"debug": "^4.3.1",
"extend": "^3.0.0",
"import-fresh": "^3.3.0",
Expand Down
113 changes: 106 additions & 7 deletions core/src/config.ts
@@ -1,8 +1,9 @@
import * as extend from 'extend';
import { SeederCollection, DeepPartial } from './common';
import { SeederDatabaseConfig, defaultDatabaseConfigObject } from './database';
import { BulkWriteOptions, MongoClientOptions } from 'mongodb';
import { EJSONOptions } from 'bson';
import { ConnectionString } from 'connection-string';
import { parseSeederDatabaseConfig } from './database';

/**
* Defines configuration for database seeding.
Expand All @@ -11,7 +12,7 @@ export interface SeederConfig {
/**
* Database connection URI or configuration object.
*/
database: SeederDatabaseConfig;
database?: SeederDatabaseConfig;
/**
* Maximum time of waiting for successful MongoDB connection in milliseconds. Ignored when `mongoClientOptions` are passed.
*/
Expand All @@ -38,37 +39,135 @@ export interface SeederConfig {
bulkWriteOptions?: BulkWriteOptions;
}

export type SeederConfigWithoutDatabase = Omit<SeederConfig, 'database'>;

/**
* Stores default configuration for database seeding.
*/
export const defaultSeederConfig: SeederConfig = {
database: defaultDatabaseConfigObject,
export const defaultSeederConfig: SeederConfigWithoutDatabase = {
databaseReconnectTimeout: 10000,
dropDatabase: false,
dropCollections: false,
removeAllDocuments: false,
};

/**
* Merges configuration for database seeding.
* Represents database connection configuration. It can be a URI string or object.
*/
export type SeederDatabaseConfig = string | SeederDatabaseConfigObject;

/**
* Defines configuration for Database connection in a form of an object.
*/
export interface SeederDatabaseConfigObject {
/**
* Database connection protocol
*/
protocol: string;

/**
* Database connection host
*/
host: string;

/**
* Database connection port
*/
port: number;

/**
* Database name.
*/
name: string;

/**
* Database Username.
*/
username?: string;

/**
* Database password.
*/
password?: string;

/**
* Options for MongoDB Database Connection URI.
* Read more on: https://docs.mongodb.com/manual/reference/connection-string.
*/
options?: SeederDatabaseConfigObjectOptions;
}

/**
* Defines options for MongoDB Database Connection URI.
* Read more on: https://docs.mongodb.com/manual/reference/connection-string.
*/
export interface SeederDatabaseConfigObjectOptions {
[key: string]: unknown;
}

/**
* Merges configuration for seeding and deletes database property.
*
* @param partial Partial config object. If not specified, returns a default config object.
* @param previous Previous config object. If not specified, uses a default config object as a base.
*/
export const mergeSeederConfig = (
export const mergeSeederConfigAndDeleteDb = (
partial?: DeepPartial<SeederConfig>,
previous?: SeederConfig,
): SeederConfig => {
): SeederConfigWithoutDatabase => {
const source = previous ? previous : defaultSeederConfig;
if ('database' in source) {
delete source.database;
}

if (!partial) {
return source;
}

const config = {};
delete partial.database;
return extend(true, config, source, partial);
};

export const mergeConnection = (
partial?: DeepPartial<SeederDatabaseConfig>,
previous?: ConnectionString,
): ConnectionString => {
const source = previous ?? parseSeederDatabaseConfig(undefined);
if (source.hosts && source.hosts.length > 1) {
source.hosts = [source.hosts[0]];
}
if (!partial) {
return source;
}

if (typeof partial === 'string') {
return parseSeederDatabaseConfig(partial);
}

const partialConn = parseSeederDatabaseConfig(partial, true);

// override hosts manually
if (
partialConn.hosts &&
partialConn.hosts.length > 0 &&
source.hosts &&
source.hosts.length > 0
) {
const newHost = partialConn.hosts[0];
if (!newHost.name) {
newHost.name = source.hosts[0].name;
}

if (!newHost.port) {
newHost.port = source.hosts[0].port;
}
}

const config = new ConnectionString();
return extend(true, config, source, partialConn);
};

/**
* Defines collection reading configuration.
*/
Expand Down
86 changes: 0 additions & 86 deletions core/src/database/config.ts

This file was deleted.

69 changes: 6 additions & 63 deletions core/src/database/database-connector.ts
@@ -1,11 +1,5 @@
import { MongoClient, MongoClientOptions } from 'mongodb';
import { URLSearchParams } from 'url';
import {
Database,
SeederDatabaseConfig,
isSeederDatabaseConfigObject,
SeederDatabaseConfigObject,
} from '.';
import { Database } from '.';
import { LogFn } from '../common';

/**
Expand Down Expand Up @@ -61,70 +55,19 @@ export class DatabaseConnector {
/**
* Connects to database.
*
* @param config Database configuration
* @param connectionString Database connection string
*/
async connect(config: SeederDatabaseConfig): Promise<Database> {
const dbConnectionUri = this.getUri(config);
const mongoClient = new MongoClient(dbConnectionUri, this.clientOptions);
async connect(connectionString: string): Promise<Database> {
const mongoClient = new MongoClient(connectionString, this.clientOptions);

this.log(`Connecting to ${this.maskUriCredentials(dbConnectionUri)}...`);

try {
await mongoClient.connect();
} catch (err) {
const e = err as Error;
throw new Error(`Error connecting to database: ${e.name}: ${e.message}`);
}
this.log(`Connecting to ${this.maskUriCredentials(connectionString)}...`);

await mongoClient.connect();
this.log('Connection with database established.');

return new Database(mongoClient);
}

/**
* Gets MongoDB Connection URI from config.
*
* @param config Database configuration
*/
private getUri(config: SeederDatabaseConfig): string {
if (typeof config === 'string') {
return config;
}

if (isSeederDatabaseConfigObject(config as unknown)) {
return this.getDbConnectionUri(config);
}

throw new Error(
'Connection URI or database config object is required to connect to database',
);
}

/**
* Constructs database connection URI from database configuration object.
*
* @param param0 Database connection object
*/
private getDbConnectionUri({
protocol,
host,
port,
name,
username,
password,
options,
}: SeederDatabaseConfigObject) {
const credentials = username
? `${username}${password ? `:${password}` : ''}@`
: '';
const optsUriPart = options
? `?${new URLSearchParams(options).toString()}`
: '';
const portUriPart = protocol !== 'mongodb+srv' ? `:${port}` : '';

return `${protocol}://${credentials}${host}${portUriPart}/${name}${optsUriPart}`;
}

/**
* Detects database connection credentials and masks them, replacing with masked URI credentials token.
*
Expand Down

0 comments on commit 256212f

Please sign in to comment.