Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module.exports = {
env: {
es2021: true,
node: true,
mocha: true,
},
parserOptions: {
ecmaVersion: 2020, // or 2018 for Node 10 compatibility
sourceType: "module", // for .mjs files; .cjs will still be CommonJS
},
extends: ["eslint:recommended", "plugin:prettier/recommended"],
plugins: ["prettier"],
rules: {
quotes: [1, "double"],
"prettier/prettier": "error",
},
overrides: [
{
files: ["*.cjs"],
parserOptions: {
sourceType: "script",
ecmaVersion: 2020,
},
},
{
files: ["*.mjs"],
parserOptions: {
sourceType: "module",
ecmaVersion: 2020,
},
},
],
};
18 changes: 0 additions & 18 deletions .eslintrc.js

This file was deleted.

16 changes: 9 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Test

on:
push:
branches: [ "master" ]
branches: ["master"]
pull_request:
branches: [ "master" ]
branches: ["master"]

jobs:
build:
Expand All @@ -13,14 +13,16 @@ jobs:
strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 16.x, 18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache: "npm"
- name: Write .npmrc from env
run: echo "${NPM_CREDENTIALS}" > ${{ github.workspace }}/.npmrc
env:
NPM_CREDENTIALS: ${{ secrets.NPM_CREDENTIALS }}
- run: npm ci
- run: npm test
24 changes: 12 additions & 12 deletions index.js → index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const obscurePassword = (conStr, password) => {
};

// Deprecated: Parse a connection string to a pg configuration object
const urlToObj = conStr => {
const urlToObj = (conStr) => {
if (typeof conStr !== "string") {
return conStr;
}
Expand All @@ -30,7 +30,7 @@ const urlToObj = conStr => {

const auth = {
username: "",
password: ""
password: "",
};

if (hasAuth) {
Expand Down Expand Up @@ -69,12 +69,12 @@ But you gave me:
min: conParams.query.min && +conParams.query.min,
max: conParams.query.max && +conParams.query.max,
idleTimeoutMillis:
conParams.query.idleTimeoutMillis && +conParams.query.idleTimeoutMillis
conParams.query.idleTimeoutMillis && +conParams.query.idleTimeoutMillis,
};
};

// Parse a config object or a string to support old and new ways of configuring postache
const parseConfig = urlOrPgConfig => {
const parseConfig = (urlOrPgConfig) => {
if (typeof urlOrPgConfig === "string") {
info(
"String database URL is deprecated, use an object instead! More info: https://github.com/omninews/node-postache#configuration"
Expand All @@ -93,29 +93,29 @@ const parseConfig = urlOrPgConfig => {
module.exports = (queries, context, urlOrPgConfig, pgConfig = {}) => {
const pgConfigObj = {
...parseConfig(urlOrPgConfig),
...parseConfig(pgConfig)
...parseConfig(pgConfig),
};

const db = new pg.Pool(pgConfigObj);

db.on("error", err => {
db.on("error", (err) => {
error("idle client error: %s %j", err.message, err.stack);
});

const renderedQueries = R.mapObjIndexed(
query => mustache.render(query, context, queries),
(query) => mustache.render(query, context, queries),
queries
);

const queryWithObj = objQuery => argsObj => {
const queryWithObj = (objQuery) => (argsObj) => {
const args = [];
const pgQuery = objQuery.replace(dollarFollowedByAscii, (_, name) => {
args.push(R.path(name.split("."), argsObj));
return `$${args.length}`;
});
info("Running query: %s", pgQuery);
sensitive("Args: %j", args);
return db.query(pgQuery, args).catch(e => {
return db.query(pgQuery, args).catch((e) => {
error("Postache error: %j", e);
return Promise.reject(e);
});
Expand All @@ -124,7 +124,7 @@ module.exports = (queries, context, urlOrPgConfig, pgConfig = {}) => {
return {
query: db.query.bind(db),
db,
...R.mapObjIndexed(queryWithObj, renderedQueries)
...R.mapObjIndexed(queryWithObj, renderedQueries),
};
};

Expand All @@ -136,11 +136,11 @@ module.exports.loadDir = (dir, extOption) => {
const isSqlFile = new RegExp(`.*\\${ext}$`);

return readdir(dir)
.filter(file => file.match(isSqlFile))
.filter((file) => file.match(isSqlFile))
.reduce((files, filePath) => {
const name = filePath.replace(`${dir}/`, "").replace(ext, "");
return Object.assign(files, {
[name]: fs.readFileSync(filePath, { encoding: "utf8" })
[name]: fs.readFileSync(filePath, { encoding: "utf8" }),
});
}, {});
};
146 changes: 146 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import R from "ramda";
import pg from "pg";
import mustache from "mustache";
import fs from "fs";
import url from "url";
import readdir from "recursive-readdir-sync";
import debug from "debug";

const info = debug("postache:info");
const sensitive = debug("postache:sensitive");
const error = debug("postache:error");

const dollarFollowedByAscii = /\$([a-zA-Z0-9_.]+)/g;

/* Remove the password from a given url string */
const obscurePassword = (conStr, password) => {
if (password === "") return conStr;
return conStr.replace(password, "\x1B[31m{password omitted}\x1B[39m");
};

// Deprecated: Parse a connection string to a pg configuration object
const urlToObj = (conStr) => {
if (typeof conStr !== "string") {
return conStr;
}

const conParams = url.parse(conStr, true);

const hasAuth = conParams.auth !== null;

const auth = {
username: "",
password: "",
};

if (hasAuth) {
if (conParams.auth.indexOf(":") === -1) {
auth.username = conParams.auth;
info("No password supplied, using only username");
} else {
const [username, password] = conParams.auth.split(":");
auth.username = username;
auth.password = password;
}
} else {
info("No username or password supplied, using blank name and password");
}

if (conParams.pathname === null) {
error(
`
You must define a valid database url!
A database URL looks like:
postgres://localhost/omni/
But you gave me:
${obscurePassword(conStr, auth.password)}
`.trim()
);
throw new Error("Invalid database URL!");
}

return {
user: auth.username,
password: auth.password,
host: conParams.hostname,
port: conParams.port,
database: conParams.pathname.split("/")[1],
ssl: !!conParams.query.ssl,
min: conParams.query.min && +conParams.query.min,
max: conParams.query.max && +conParams.query.max,
idleTimeoutMillis:
conParams.query.idleTimeoutMillis && +conParams.query.idleTimeoutMillis,
};
};

// Parse a config object or a string to support old and new ways of configuring postache
const parseConfig = (urlOrPgConfig) => {
if (typeof urlOrPgConfig === "string") {
info(
"String database URL is deprecated, use an object instead! More info: https://github.com/omninews/node-postache#configuration"
);

return urlToObj(urlOrPgConfig);
}

if (typeof urlOrPgConfig === "object") {
return urlOrPgConfig;
}

throw new Error("Invalid database URL!");
};

export default (queries, context, urlOrPgConfig, pgConfig = {}) => {
const pgConfigObj = {
...parseConfig(urlOrPgConfig),
...parseConfig(pgConfig),
};

const db = new pg.Pool(pgConfigObj);

db.on("error", (err) => {
error("idle client error: %s %j", err.message, err.stack);
});

const renderedQueries = R.mapObjIndexed(
(query) => mustache.render(query, context, queries),
queries
);

const queryWithObj = (objQuery) => (argsObj) => {
const args = [];
const pgQuery = objQuery.replace(dollarFollowedByAscii, (_, name) => {
args.push(R.path(name.split("."), argsObj));
return `$${args.length}`;
});
info("Running query: %s", pgQuery);
sensitive("Args: %j", args);
return db.query(pgQuery, args).catch((e) => {
error("Postache error: %j", e);
return Promise.reject(e);
});
};

return {
query: db.query.bind(db),
db,
...R.mapObjIndexed(queryWithObj, renderedQueries),
};
};

export const loadDir = (dir, extOption) => {
let ext = extOption || ".sql";
if (ext[0] !== ".") {
ext = `.${ext}`;
}
const isSqlFile = new RegExp(`.*\\${ext}$`);

return readdir(dir)
.filter((file) => file.match(isSqlFile))
.reduce((files, filePath) => {
const name = filePath.replace(`${dir}/`, "").replace(ext, "");
return Object.assign(files, {
[name]: fs.readFileSync(filePath, { encoding: "utf8" }),
});
}, {});
};
Loading