Repository: knex/knex
Pull request #5616 (2023-06-30 10:54:22)

fix: add mjs extension type
ES Modules are supported as migrations and seed file types (thanks to [prior PR](https://github.com/knex/knex/pull/4631)) but it didn't show up in the supported file types on the command-line.


Patch:
diff --git a/bin/cli.js b/bin/cli.js
index 04d73ce6..9b584f9e 100755
--- a/bin/cli.js
+++ b/bin/cli.js
@@ -85,7 +85,7 @@ async function initKnex(env, opts, useDefaultClientIfNotSpecified) {
 }
 
 function invoke() {
-  const filetypes = ['js', 'coffee', 'ts', 'eg', 'ls'];
+  const filetypes = ['js', 'mjs', 'coffee', 'ts', 'eg', 'ls'];
 
   const cwd = argv.knexfile
     ? path.dirname(path.resolve(argv.knexfile))
diff --git a/lib/client.js b/lib/client.js
index 1a8ec2f5..00ee2893 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -31,6 +31,7 @@ const { POOL_CONFIG_OPTIONS } = require('./constants');
 const ViewBuilder = require('./schema/viewbuilder.js');
 const ViewCompiler = require('./schema/viewcompiler.js');
 const isPlainObject = require('lodash/isPlainObject');
+const { setHiddenProperty } = require('./util/security.js');
 
 const debug = require('debug')('knex:client');
 
@@ -43,6 +44,10 @@ class Client extends EventEmitter {
     this.config = config;
     this.logger = new Logger(config);
 
+    if (this.config.connection && this.config.connection.password) {
+      setHiddenProperty(this.config.connection);
+    }
+
     //Client is a required field, so throw error if it's not supplied.
     //If 'this.dialect' is set, then this is a 'super()' call, in which case
     //'client' does not have to be set as it's already assigned on the client prototype.
@@ -69,6 +74,9 @@ class Client extends EventEmitter {
       this.connectionConfigExpirationChecker = () => true; // causes the provider to be called on first use
     } else {
       this.connectionSettings = cloneDeep(config.connection || {});
+      if (config.connection && config.connection.password) {
+        setHiddenProperty(this.connectionSettings, config.connection);
+      }
       this.connectionConfigExpirationChecker = null;
     }
     if (this.driverName && config.connection) {
@@ -305,6 +313,18 @@ class Client extends EventEmitter {
     try {
       const connection = await this.pool.acquire().promise;
       debug('acquired connection from pool: %s', connection.__knexUid);
+      if (connection.config) {
+        if (connection.config.password) {
+          setHiddenProperty(connection.config);
+        }
+        if (
+          connection.config.authentication &&
+          connection.config.authentication.options &&
+          connection.config.authentication.options.password
+        ) {
+          setHiddenProperty(connection.config.authentication.options);
+        }
+      }
       return connection;
     } catch (error) {
       let convertedError = error;
diff --git a/lib/dialects/mssql/index.js b/lib/dialects/mssql/index.js
index 27654fd8..67fba028 100644
--- a/lib/dialects/mssql/index.js
+++ b/lib/dialects/mssql/index.js
@@ -12,6 +12,7 @@ const TableCompiler = require('./schema/mssql-tablecompiler');
 const ViewCompiler = require('./schema/mssql-viewcompiler');
 const ColumnCompiler = require('./schema/mssql-columncompiler');
 const QueryBuilder = require('../../query/querybuilder');
+const { setHiddenProperty } = require('../../util/security');
 
 const debug = require('debug')('knex:mssql');
 
@@ -66,6 +67,10 @@ class Client_MSSQL extends Client {
       },
     };
 
+    if (cfg.authentication.options.password) {
+      setHiddenProperty(cfg.authentication.options);
+    }
+
     // tedious always connect via tcp when port is specified
     if (cfg.options.instanceName) delete cfg.options.port;
 
diff --git a/lib/dialects/postgres/schema/pg-tablecompiler.js b/lib/dialects/postgres/schema/pg-tablecompiler.js
index dd029cf1..455a339c 100644
--- a/lib/dialects/postgres/schema/pg-tablecompiler.js
+++ b/lib/dialects/postgres/schema/pg-tablecompiler.js
@@ -235,12 +235,13 @@ class TableCompiler_PG extends TableCompiler {
       : this._indexCommand('index', this.tableNameRaw, columns);
 
     let predicate;
+    let storageEngineIndexType;
     let indexType;
 
     if (isString(options)) {
-      indexType = options;
+      storageEngineIndexType = options;
     } else if (isObject(options)) {
-      ({ indexType, predicate } = options);
+      ({ indexType, storageEngineIndexType, predicate } = options);
     }
 
     const predicateQuery = predicate
@@ -248,8 +249,12 @@ class TableCompiler_PG extends TableCompiler {
       : '';
 
     this.pushQuery(
-      `create index ${indexName} on ${this.tableName()}${
-        (indexType && ` using ${indexType}`) || ''
+      `create${
+        typeof indexType === 'string' && indexType.toLowerCase() === 'unique'
+          ? ' unique'
+          : ''
+      } index ${indexName} on ${this.tableName()}${
+        (storageEngineIndexType && ` using ${storageEngineIndexType}`) || ''
       }` +
         ' (' +
         this.formatter.columnize(columns) +
diff --git a/lib/knex-builder/make-knex.js b/lib/knex-builder/make-knex.js
index 55f9acd3..7345708c 100644
--- a/lib/knex-builder/make-knex.js
+++ b/lib/knex-builder/make-knex.js
@@ -7,6 +7,7 @@ const QueryInterface = require('../query/method-constants');
 const merge = require('lodash/merge');
 const batchInsert = require('../execution/batch-insert');
 const { isObject } = require('../util/is');
+const { setHiddenProperty } = require('../util/security');
 
 // Javascript does not officially support "callable objects".  Instead,
 // you must create a regular Function and inject properties/methods
@@ -208,6 +209,10 @@ function initContext(knexFn) {
         knexClone.client = Object.create(this.client.constructor.prototype); // Clone client to avoid leaking listeners that are set on it
         merge(knexClone.client, this.client);
         knexClone.client.config = Object.assign({}, this.client.config); // Clone client config to make sure they can be modified independently
+
+        if (this.client.config.password) {
+          setHiddenProperty(knexClone.client.config, this.client.config);
+        }
       }
 
       redefineProperties(knexClone, knexClone.client);
diff --git a/lib/util/security.js b/lib/util/security.js
new file mode 100644
index 00000000..d4b4edb9
--- /dev/null
+++ b/lib/util/security.js
@@ -0,0 +1,26 @@
+/**
+ * Sets a hidden (non-enumerable) property on the `target` object, copying it
+ * from `source`.
+ *
+ * This is useful when we want to protect certain data from being accidentally
+ * leaked through logs, also when the property is non-enumerable on the `source`
+ * object and we want to ensure that it is properly copied.
+ *
+ * @param {object} target
+ * @param {object} source - default: target
+ * @param {string} propertyName - default: 'password'
+ */
+function setHiddenProperty(target, source, propertyName = 'password') {
+  if (!source) {
+    source = target;
+  }
+
+  Object.defineProperty(target, propertyName, {
+    enumerable: false,
+    value: source[propertyName],
+  });
+}
+
+module.exports = {
+  setHiddenProperty,
+};
diff --git a/package.json b/package.json
index b814701c..fbb6ab50 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
     "getopts": "2.3.0",
     "interpret": "^2.2.0",
     "lodash": "^4.17.21",
-    "pg-connection-string": "2.5.0",
+    "pg-connection-string": "2.6.1",
     "rechoir": "^0.8.0",
     "resolve-from": "^5.0.0",
     "tarn": "^3.0.2",
diff --git a/types/index.d.ts b/types/index.d.ts
index d17d1122..2e5f7f73 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -566,7 +566,7 @@ export declare namespace Knex {
 
   interface OnConflictQueryBuilder<TRecord extends {}, TResult> {
     ignore(): QueryBuilder<TRecord, TResult>;
-    merge(mergeColumns?: (keyof TRecord)[]): QueryBuilder<TRecord, TResult>;
+    merge(mergeColumns?: (keyof ResolveTableType<TRecord, 'update'>)[]): QueryBuilder<TRecord, TResult>;
     merge(
       data?: Extract<DbRecord<ResolveTableType<TRecord, 'update'>>, object>
     ): QueryBuilder<TRecord, TResult>;


Test patch:
diff --git a/test/db-less-test-suite.js b/test/db-less-test-suite.js
index 1100a293..647d83af 100644
--- a/test/db-less-test-suite.js
+++ b/test/db-less-test-suite.js
@@ -10,6 +10,7 @@ describe('Util Tests', function () {
   require('./unit/util/nanoid');
   require('./unit/util/save-async-stack');
   require('./unit/util/comma-no-paren-regex');
+  require('./unit/util/security');
 });
 
 describe('Query Building Tests', function () {
diff --git a/test/integration/connection-config-provider.js b/test/integration/connection-config-provider.js
index 37006e90..0725fe13 100644
--- a/test/integration/connection-config-provider.js
+++ b/test/integration/connection-config-provider.js
@@ -4,6 +4,7 @@ const { expect } = require('chai');
 
 const _ = require('lodash');
 const makeKnex = require('../../knex');
+const { setHiddenProperty } = require('../../lib/util/security');
 
 module.exports = function (config) {
   describe('Connection configuration provider', function () {
@@ -13,6 +14,9 @@ module.exports = function (config) {
 
     this.beforeEach(() => {
       configWorkingCopy = _.cloneDeep(config);
+      if (config.connection && config.connection.password) {
+        setHiddenProperty(configWorkingCopy.connection, config.connection);
+      }
       configWorkingCopy.pool.min = 1;
       configWorkingCopy.pool.max = 2;
       providerInvocationCount = 0;
diff --git a/test/integration2/query/misc/additional.spec.js b/test/integration2/query/misc/additional.spec.js
index 73d9d74d..f05d4d2a 100644
--- a/test/integration2/query/misc/additional.spec.js
+++ b/test/integration2/query/misc/additional.spec.js
@@ -1,6 +1,7 @@
 /*eslint no-var:0, max-len:0 */
 'use strict';
 
+const util = require('util');
 const chai = require('chai');
 chai.use(require('chai-as-promised'));
 chai.use(require('sinon-chai'));
@@ -34,6 +35,7 @@ const {
 const logger = require('../../../integration/logger');
 const { insertAccounts } = require('../../../util/dataInsertHelper');
 const sinon = require('sinon');
+const { setHiddenProperty } = require('../../../../lib/util/security');
 
 describe('Additional', function () {
   getAllDbs().forEach((db) => {
@@ -1006,6 +1008,16 @@ describe('Additional', function () {
           // To make this test easier, I'm changing the pool settings to max 1.
           // Also setting acquireTimeoutMillis to lower as not to wait the default time
           const knexConfig = _.cloneDeep(knex.client.config);
+          if (
+            knex.client.config.connection &&
+            knex.client.config.connection.password
+          ) {
+            setHiddenProperty(
+              knexConfig.connection,
+              knex.client.config.connection
+            );
+          }
+
           knexConfig.pool.min = 0;
           knexConfig.pool.max = 1;
           knexConfig.pool.acquireTimeoutMillis = 100;
@@ -1123,6 +1135,16 @@ describe('Additional', function () {
           // To make this test easier, I'm changing the pool settings to max 1.
           // Also setting acquireTimeoutMillis to lower as not to wait the default time
           const knexConfig = _.cloneDeep(knex.client.config);
+          if (
+            knex.client.config.connection &&
+            knex.client.config.connection.password
+          ) {
+            setHiddenProperty(
+              knexConfig.connection,
+              knex.client.config.connection
+            );
+          }
+
           knexConfig.pool.min = 0;
           knexConfig.pool.max = 2;
           knexConfig.pool.acquireTimeoutMillis = 100;
@@ -1420,6 +1442,17 @@ describe('Additional', function () {
             expect(knexDb.client.pool.destroyed).to.equal(false);
           });
         });
+
+        it('should not leak passwords when logged', async function () {
+          const query = knex('test_table_two').select('*');
+          const fakeLog = util.inspect(query, { depth: null });
+
+          // These passwords come from `scripts/docker-compose.yml`
+          const passwordMatches = fakeLog.match(
+            /(knextest|testpassword|S0meVeryHardPassword|testrootpassword)/g
+          );
+          expect(passwordMatches).to.be.null;
+        });
       });
     });
   });
diff --git a/test/unit/schema-builder/postgres.js b/test/unit/schema-builder/postgres.js
index fb9065d5..313ff118 100644
--- a/test/unit/schema-builder/postgres.js
+++ b/test/unit/schema-builder/postgres.js
@@ -973,7 +973,7 @@ describe('PostgreSQL SchemaBuilder', function () {
     );
   });
 
-  it('adding index with an index type', function () {
+  it('adding index with a storage engine index type', function () {
     tableSql = client
       .schemaBuilder()
       .table('users', function (table) {
@@ -986,7 +986,7 @@ describe('PostgreSQL SchemaBuilder', function () {
     );
   });
 
-  it('adding index with an index type fluently', function () {
+  it('adding index with a storage engine index type fluently', function () {
     tableSql = client
       .schemaBuilder()
       .table('users', function (table) {
@@ -1002,11 +1002,11 @@ describe('PostgreSQL SchemaBuilder', function () {
     );
   });
 
-  it('adding index with an index type and default name fluently', function () {
+  it('adding index with a storage engine index type and default name fluently', function () {
     tableSql = client
       .schemaBuilder()
       .table('users', function (table) {
-        table.string('name').index(null, { indexType: 'gist' });
+        table.string('name').index(null, { storageEngineIndexType: 'gist' });
       })
       .toSQL();
     equal(2, tableSql.length);
@@ -1033,12 +1033,27 @@ describe('PostgreSQL SchemaBuilder', function () {
     );
   });
 
-  it('adding index with an index type and a predicate', function () {
+  it('adding unique index using index method', function () {
     tableSql = client
       .schemaBuilder()
       .table('users', function (table) {
         table.index(['foo', 'bar'], 'baz', {
-          indexType: 'gist',
+          indexType: 'unique',
+        });
+      })
+      .toSQL();
+    equal(1, tableSql.length);
+    expect(tableSql[0].sql).to.equal(
+      'create unique index "baz" on "users" ("foo", "bar")'
+    );
+  });
+
+  it('adding index with a storage engine index type and a predicate', function () {
+    tableSql = client
+      .schemaBuilder()
+      .table('users', function (table) {
+        table.index(['foo', 'bar'], 'baz', {
+          storageEngineIndexType: 'gist',
           predicate: client.queryBuilder().whereRaw('email = "foo@bar"'),
         });
       })
diff --git a/test/unit/util/security.js b/test/unit/util/security.js
new file mode 100644
index 00000000..c2846b12
--- /dev/null
+++ b/test/unit/util/security.js
@@ -0,0 +1,39 @@
+const { expect } = require('chai');
+const { setHiddenProperty } = require('../../../lib/util/security');
+
+describe('setHiddenProperty', () => {
+  it('hides password property when only target object is passed', () => {
+    const target = { password: 'xyz' };
+    expect(Object.keys(target)).to.contain('password'); // sanity check
+
+    setHiddenProperty(target);
+    expect(Object.keys(target)).to.not.contain('password');
+
+    // Although not enumerable, it is still present
+    expect(target).to.have.property('password');
+    expect(target.password).to.equal('xyz');
+  });
+
+  it('sets property from source even when it is not enumerable', () => {
+    const source = Object.create(
+      {},
+      {
+        password: {
+          enumerable: false,
+          value: 'xyz',
+        },
+      }
+    );
+
+    expect(Object.keys(source)).to.not.contain('password'); // sanity check
+    expect(source).to.have.property('password'); // sanity check
+    expect(source.password).to.equal('xyz'); // sanity check
+
+    const target = {};
+    setHiddenProperty(target, source);
+
+    expect(Object.keys(target)).to.not.contain('password');
+    expect(target).to.have.property('password');
+    expect(target.password).to.equal('xyz');
+  });
+});