Skip to content

Commit

Permalink
feat: make client config argument optional
Browse files Browse the repository at this point in the history
  • Loading branch information
ozum committed Feb 8, 2021
1 parent 41a409e commit f6fa6aa
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 30 deletions.
8 changes: 8 additions & 0 deletions module-files/configs/vuepress-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ if (GOOGLE_ANALYTICS_ID) plugins.push(["@vuepress/google-analytics", { ga: GOOGL
// sidebar["/nav.02.api/"].unshift("");
// }

// Find item with text "Api" and change it to "API".
nav
.filter((item) => ["Api", "Cli"].includes(item.text))
.map((item) => {
item.text = item.text.toUpperCase(item.text); // eslint-disable-line no-param-reassign
return item;
});

module.exports = {
title: packageData.label || packageData.name,
description: packageData.description,
Expand Down
98 changes: 68 additions & 30 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
FunctionQueryResult,
TriggerQueryResult,
} from "./types/query-result";
import { executeSqlFile, getConnectedPgClient, getQueryVersionFor, getEnvValues } from "./util/helper";
import { executeSqlFile, getConnectedPgClient, getQueryVersionFor, getEnvValues, arrify } from "./util/helper";
import Db from "./pg-structure/db";
import Schema from "./pg-structure/schema";

Expand Down Expand Up @@ -372,6 +372,30 @@ function addObjects(db: Db, queryResults: QueryResults): void {
addTriggers(db, queryResults[8]);
}

/**
* Checks whether given object are options for the `pgStructure` function.
*
* @param input is the input to check.
* @returns whether given input are options for the `pgStructure` function.
*/
function isOptions(input?: Client | ClientConfig | string | Options): input is Options {
if (input === undefined) return false;
const optionsAvailable: Required<{ [K in keyof Options]: true }> = {
envPrefix: true,
name: true,
commentDataToken: true,
includeSchemas: true,
excludeSchemas: true,
includeSystemSchemas: true,
foreignKeyAliasSeparator: true,
foreignKeyAliasTargetFirst: true,
relationNameFunctions: true,
keepConnection: true,
};

return Object.keys(input).some((key) => Object.prototype.hasOwnProperty.call(optionsAvailable, key));
}

/**
* Reverse engineers a PostgreSQL database and creates [[Db]] instance which represents given database's structure.
* There are several options such as to include or exclude schemas, provide custom names to relations. Please refer to [[Options]]
Expand All @@ -380,49 +404,63 @@ function addObjects(db: Db, queryResults: QueryResults): void {
* **IMPORTANT:** Please note that if included schemas contain references to a non-included schema, this function throws exception.
* (e.g. a foreign key to another schema or a type in another schema which is not included)
*
* @param maybePgClientOrConfig is connection string or [node-postgres client](https://node-postgres.com/api/client) configuration.
* @param __namedParameters are options to change behaviour of the function.
* @param client is either a [node-postgres client](https://node-postgres.com/api/client) or a configuration object or a connection string to create a [node-postgres client](https://node-postgres.com/api/client).
* @param options are preferences to modify reverse engineering process.
* @returns [[Db]] object which represents given database's structure.
*
* @example
* const db = await pgStructure({ database: "db", user: "u", password: "pass" }, { includeSchemas: ["public"] });
*/
export async function pgStructure(client: Client | ClientConfig | string, options?: Options): Promise<Db>;
/**
* Reads configuration details from environment variables to create [node-postgres client](https://node-postgres.com/api/client).
* Keys are upper case environment variables prefixed with `options.envPrefix` (default is `DB`).
*
* |Environment Varibale|[ClientConfig](https://node-postgres.com/api/client) Key|
* |---|---|
* |DB_DATABASE|database|
* |DB_USER|user|
* |DB_PASSWORD|password|
* |...|...|
*
* @param options are preferences to modify reverse engineering process.
* @returns [[Db]] object which represents given database's structure.
*
* @example
* const db = await pgStructure({ includeSchemas: ["public"] });
*
* @example
* const db = await pgStructure(); // Read connection details from environmet variables.
*/
export async function pgStructure(
maybePgClientOrConfig?: Client | ClientConfig | string,
{
envPrefix = "DB",
name,
commentDataToken = "pg-structure",
includeSchemas,
excludeSchemas,
includeSystemSchemas,
foreignKeyAliasSeparator = ",",
foreignKeyAliasTargetFirst = false,
relationNameFunctions = "short",
keepConnection = false,
}: Options = {}
): Promise<Db> {
/* istanbul ignore next */
const pgClientOrConfig = maybePgClientOrConfig ?? getEnvValues(envPrefix);
export async function pgStructure(options?: Options): Promise<Db>;
export async function pgStructure(clientOrOptions?: Client | ClientConfig | string | Options, maybeOptions: Options = {}): Promise<Db> {
const [maybePgClientOrConfig, options] = isOptions(clientOrOptions) ? [undefined, clientOrOptions] : [clientOrOptions, maybeOptions];
const pgClientOrConfig = maybePgClientOrConfig ?? getEnvValues(options.envPrefix ?? "DB");
const { client, shouldCloseConnection } = await getConnectedPgClient(pgClientOrConfig);

const includeSchemasArray = Array.isArray(includeSchemas) || includeSchemas === undefined ? includeSchemas : [includeSchemas];
const excludeSchemasArray = Array.isArray(excludeSchemas) || excludeSchemas === undefined ? excludeSchemas : [excludeSchemas];

const serverVersion = (await client.query("SHOW server_version")).rows[0].server_version;
const queryResults = await getQueryResultsFromDb(serverVersion, client, includeSchemasArray, excludeSchemasArray, includeSystemSchemas);
const queryResults = await getQueryResultsFromDb(
serverVersion,
client,
arrify(options.includeSchemas),
arrify(options.excludeSchemas),
options.includeSystemSchemas
);

const db = new Db(
{ name: name || getDatabaseName(pgClientOrConfig), serverVersion },
{ name: options.name || getDatabaseName(pgClientOrConfig), serverVersion },
{
commentDataToken,
relationNameFunctions,
foreignKeyAliasSeparator,
foreignKeyAliasTargetFirst,
commentDataToken: options.commentDataToken ?? "pg-structure",
relationNameFunctions: options.relationNameFunctions ?? "short",
foreignKeyAliasSeparator: options.foreignKeyAliasSeparator ?? ",",
foreignKeyAliasTargetFirst: options.foreignKeyAliasTargetFirst ?? false,
},
queryResults
);

addObjects(db, queryResults);

if (!keepConnection && shouldCloseConnection) client.end(); // If a connected client is provided, do not close connection.
if (!options.keepConnection && shouldCloseConnection) client.end(); // If a connected client is provided, do not close connection.
return db;
}

Expand Down
11 changes: 11 additions & 0 deletions src/util/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,14 @@ export function getRelationsMarkdown(db: Db, fk = false): string {
});
return result.join("\n");
}

/**
* Converts it's input to an array.
*
* @param input is the input value to convert to array.
* @returns array If input is undefined returns undefined.
*/
export function arrify<T extends any>(input: T | T[] | undefined): T[] | undefined {
if (input === undefined) return undefined;
return Array.isArray(input) ? input : [input];
}
7 changes: 7 additions & 0 deletions test/pg-structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ describe("pgStructure()", () => {
const db = await pgStructure(
{ database: "pg-structure-test-main", user: "user", password: "password" },
{
commentDataToken: "pg-structure",
foreignKeyAliasSeparator: ",",
relationNameFunctions: {
o2m: (relation) => `X_${relation.sourceTable.name}`,
m2o: (relation) => `X_${relation.sourceTable.name}`,
Expand Down Expand Up @@ -84,4 +86,9 @@ describe("pgStructure()", () => {
it("should throw if it cannot connect.", async () => {
await expect(pgStructure()).rejects.toThrow("cannot connect to the database");
});

it("should accept options as first argument and reads client config from environment variables.", async () => {
process.env.DBPX_USER = "xyz-user";
await expect(pgStructure({ envPrefix: "DBPX", name: "deneme" })).rejects.toThrow('password authentication failed for user "xyz-user"');
});
});

0 comments on commit f6fa6aa

Please sign in to comment.