diff --git a/package.json b/package.json index e3cef89..98ba687 100644 --- a/package.json +++ b/package.json @@ -28,24 +28,24 @@ "test": "env CRDB_VERSION=$npm_config_crdb_version mocha --check-leaks --colors -t 300000 --reporter spec \"tests/*_test.js\"" }, "dependencies": { - "lodash": "^4.17.20", + "lodash": "^4.17.21", "p-settle": "^4.1.1", - "pg": "^8.4.1", - "semver": "^7.3.2" + "pg": "^8.21.0", + "semver": "^7.6.2" }, "devDependencies": { - "assert": "^2.0.0", - "chai": "^4.2.0", - "chai-as-promised": "^7.1.1", - "chai-datetime": "^1.7.0", + "assert": "^2.1.0", + "chai": "^4.4.1", + "chai-as-promised": "^7.1.2", + "chai-datetime": "^1.8.0", "cls-hooked": "^4.2.2", "delay": "^5.0.0", - "mocha": "^8.2.0", + "mocha": "^10.4.0", "p-timeout": "^4.1.0", - "prettier": "2.2.1", - "sequelize": "^6.13.0", - "sinon": "^9.2.4", - "sinon-chai": "^3.5.0" + "prettier": "^2.8.8", + "sequelize": "^6.37.8", + "sinon": "^17.0.1", + "sinon-chai": "^3.7.0" }, "peerDependencies": { "sequelize": "5 - 6" diff --git a/source/index.js b/source/index.js index 1ef71ad..e8a90ed 100644 --- a/source/index.js +++ b/source/index.js @@ -14,6 +14,28 @@ 'use strict'; +// Intercept require calls to load 'sequelize' from the parent application context. +// This prevents multiple conflicting instances of Sequelize when developing/linking. +const Module = require('module'); +const originalRequire = Module.prototype.require; +Module.prototype.require = function (id) { + if ( + id.startsWith('sequelize') && + this.filename.includes('sequelize-cockroachdb') + ) { + try { + const parentPaths = module.parent ? module.parent.paths : []; + const resolved = require.resolve(id, { + paths: [process.cwd(), ...parentPaths] + }); + return originalRequire.call(this, resolved); + } catch (e) { + // Fallback to original require if resolution fails + } + } + return originalRequire.call(this, id); +}; + // Ensure the user did not forget to install Sequelize. try { require('sequelize'); @@ -28,10 +50,10 @@ const { Sequelize, DataTypes, Model } = require('sequelize'); const QueryGenerator = require('sequelize/lib/dialects/postgres/query-generator'); // Ensure Sequelize version compatibility. -const version_helper = require ('./version_helper.js') +const version_helper = require('./version_helper.js'); const semver = require('semver'); -const sequelizeVersion = version_helper.GetSequelizeVersion() +const sequelizeVersion = version_helper.GetSequelizeVersion(); if (semver.satisfies(sequelizeVersion, '<=4')) { throw new Error( @@ -39,7 +61,7 @@ if (semver.satisfies(sequelizeVersion, '<=4')) { ); } -require('./telemetry.js') +require('./telemetry.js'); //// [1] Override the `upsert` query method from Sequelize v5 to make it work with CockroachDB if (semver.satisfies(sequelizeVersion, '5.x')) { @@ -81,17 +103,16 @@ PostgresDialect.prototype.supports.lockKey = false; // invalid integers. intType.prototype.escape = false; - intType.prototype.$stringify = intType.prototype._stringify = function stringify( - value - ) { - var rep = String(value); - if (!/^[-+]?[0-9]+$/.test(rep)) { - throw new Sequelize.ValidationError( - util.format('%j is not a valid integer', value) - ); - } - return rep; - }; + intType.prototype.$stringify = intType.prototype._stringify = + function stringify(value) { + var rep = String(value); + if (!/^[-+]?[0-9]+$/.test(rep)) { + throw new Sequelize.ValidationError( + util.format('%j is not a valid integer', value) + ); + } + return rep; + }; }); // [4] Fix int to string conversion @@ -103,14 +124,76 @@ const { ConnectionManager.prototype.__loadDialectModule = ConnectionManager.prototype._loadDialectModule; ConnectionManager.prototype._loadDialectModule = function (...args) { - const pg = this.__loadDialectModule(...args); - pg.types.setTypeParser(20, function (val) { - if (val > Number.MAX_SAFE_INTEGER) return String(val); - else return parseInt(val, 10); - }); + let pg = this.__loadDialectModule(...args); + if (args[0] === 'pg' && (!pg || !pg.types)) { + pg = require('pg'); + } + if (pg && pg.types) { + pg.types.setTypeParser(20, function (val) { + if (val > Number.MAX_SAFE_INTEGER) return String(val); + else return parseInt(val, 10); + }); + } return pg; }; +const PostgresConnectionManager = require('sequelize/lib/dialects/postgres/connection-manager'); +const originalConnect = PostgresConnectionManager.prototype.connect; +PostgresConnectionManager.prototype.connect = async function (config) { + const connection = await originalConnect.call(this, config); + this.sequelize.isCockroachDB = true; // Default to true for CockroachDB package + try { + const result = await connection.query('SELECT version() AS version'); + const versionStr = result.rows && result.rows[0] && result.rows[0].version; + if (versionStr) { + this.sequelize.isCockroachDB = versionStr.includes('CockroachDB'); + } + } catch (err) { + // ignore + } + + // Dynamically configure feature support flags if the target database is standard PostgreSQL + if (!this.sequelize.isCockroachDB) { + this.sequelize.dialect.supports.EXCEPTION = true; + this.sequelize.dialect.supports.lockOuterJoinFailure = true; + this.sequelize.dialect.supports.skipLocked = true; + this.sequelize.dialect.supports.lockKey = true; + } + return connection; +}; + +QueryGenerator.prototype.pgEnum = function ( + tableName, + attr, + dataType, + options +) { + const enumName = this.pgEnumName(tableName, attr, options); + let values; + + if (dataType.values) { + values = `ENUM(${dataType.values + .map(value => this.escape(value)) + .join(', ')})`; + } else { + values = dataType.toString().match(/^ENUM\(.+\)/)[0]; + } + + let sql; + if (this.sequelize.isCockroachDB) { + sql = `CREATE TYPE IF NOT EXISTS ${enumName} AS ${values};`; + } else { + sql = `DO ${this.escape( + `BEGIN CREATE TYPE ${enumName} AS ${values}; EXCEPTION WHEN duplicate_object THEN null; END` + )};`; + } + + if (!!options && options.force === true) { + sql = this.pgEnumDrop(tableName, attr) + sql; + } + return sql; +}; + QueryGenerator.prototype.__describeTableQuery = QueryGenerator.prototype.describeTableQuery; QueryGenerator.prototype.describeTableQuery = function (...args) { @@ -128,7 +211,10 @@ QueryGenerator.prototype.describeTableQuery = function (...args) { // Change unimplemented column .replace('relid', 'oid') // Aggregate enums in sort order - .replace('array_agg(e.enumlabel)', 'array_agg(e.enumlabel ORDER BY e.enumsortorder ASC)') + .replace( + 'array_agg(e.enumlabel)', + 'array_agg(e.enumlabel ORDER BY e.enumsortorder ASC)' + ) ); }; @@ -301,7 +387,36 @@ Model.findOrCreate = async function findOrCreate(options) { // Got to explicitly cast it is a GEOGRAPHY type. DataTypes.postgres.GEOGRAPHY.prototype.bindParam = (value, options) => { return `ST_GeomFromGeoJSON(${options.bindParam(value)}::json)::geography`; -} +}; + +// [8] Handle SPATIAL indexes by mapping them to GIST indexes +const originalAddIndexQuery = QueryGenerator.prototype.addIndexQuery; +QueryGenerator.prototype.addIndexQuery = function ( + tableName, + attributes, + options, + rawTablename +) { + let opts = options; + if (!Array.isArray(attributes)) { + opts = attributes; + } + if ( + opts && + typeof opts.type === 'string' && + opts.type.toUpperCase() === 'SPATIAL' + ) { + opts.using = 'gist'; + delete opts.type; + } + return originalAddIndexQuery.call( + this, + tableName, + attributes, + options, + rawTablename + ); +}; //// Done! diff --git a/source/patch-upsert-v5.js b/source/patch-upsert-v5.js index 448dd72..99aa6c1 100644 --- a/source/patch-upsert-v5.js +++ b/source/patch-upsert-v5.js @@ -609,9 +609,8 @@ const postgresQueryPatches = { attribute.fieldName === key || attribute.field === key ); - this.instance.dataValues[ - (attr && attr.fieldName) || key - ] = record; + this.instance.dataValues[(attr && attr.fieldName) || key] = + record; } } } diff --git a/source/telemetry.js b/source/telemetry.js index 0b1f4ff..33b0360 100644 --- a/source/telemetry.js +++ b/source/telemetry.js @@ -1,31 +1,47 @@ const { Sequelize, QueryTypes } = require('sequelize'); -const version_helper = require('./version_helper.js') +const version_helper = require('./version_helper.js'); //// Log telemetry for Sequelize ORM. -Sequelize.addHook('afterInit', async (connection) => { - try { - if (connection.options.dialectOptions) { - var telemetryDisabled = connection.options.dialectOptions.cockroachdbTelemetryDisabled - if (telemetryDisabled) { - return - } - } - - // crdb_internal.increment_feature_counter is only available on 21.1 and above. - if (!(await version_helper.IsCockroachVersion21_1Plus(connection))) { - return - } - var sequelizeVersion = version_helper.GetSequelizeVersion() - var sequelizeVersionSeries = version_helper.GetVersionSeries(sequelizeVersion.version) - var sequelizeVersionStr = (sequelizeVersionSeries===null)?sequelizeVersion:sequelizeVersionSeries - await connection.query(`SELECT crdb_internal.increment_feature_counter(concat('Sequelize ', :SequelizeVersionString))`, - { replacements: { SequelizeVersionString: sequelizeVersionStr }, type: QueryTypes.SELECT }) +Sequelize.addHook('afterInit', async connection => { + try { + if (connection.options.dialectOptions) { + var telemetryDisabled = + connection.options.dialectOptions.cockroachdbTelemetryDisabled; + if (telemetryDisabled) { + return; + } + } - var adapterVersion = version_helper.GetAdapterVersion() - await connection.query(`SELECT crdb_internal.increment_feature_counter(concat('sequelize-cockroachdb ', :AdapterVersion))`, - { replacements: { AdapterVersion: adapterVersion.version }, type: QueryTypes.SELECT }) - } catch (error) { - console.info("Could not record telemetry.\n" + error) + // crdb_internal.increment_feature_counter is only available on 21.1 and above. + if (!(await version_helper.IsCockroachVersion21_1Plus(connection))) { + return; } + var sequelizeVersion = version_helper.GetSequelizeVersion(); + var sequelizeVersionSeries = version_helper.GetVersionSeries( + sequelizeVersion.version + ); + var sequelizeVersionStr = + sequelizeVersionSeries === null + ? sequelizeVersion + : sequelizeVersionSeries; + await connection.query( + `SELECT crdb_internal.increment_feature_counter(concat('Sequelize ', :SequelizeVersionString))`, + { + replacements: { SequelizeVersionString: sequelizeVersionStr }, + type: QueryTypes.SELECT + } + ); + + var adapterVersion = version_helper.GetAdapterVersion(); + await connection.query( + `SELECT crdb_internal.increment_feature_counter(concat('sequelize-cockroachdb ', :AdapterVersion))`, + { + replacements: { AdapterVersion: adapterVersion.version }, + type: QueryTypes.SELECT + } + ); + } catch (error) { + console.info('Could not record telemetry.\n' + error); + } }); diff --git a/source/version_helper.js b/source/version_helper.js index 4ff7b2a..c273164 100644 --- a/source/version_helper.js +++ b/source/version_helper.js @@ -3,37 +3,40 @@ const { version, release } = require('sequelize/package.json'); const { QueryTypes } = require('sequelize'); module.exports = { - GetSequelizeVersion: function() { - // In v4 and v5 package.json files, have a property 'release: { branch: 'v5' }' - // but in v6 it has 'release: { branches: ['v6'] }' - var branchVersion = release.branches ? release.branches[0] : release.branch; - // When executing the tests on Github Actions the version it gets from sequelize is from the repository which has a development version '0.0.0-development' - // in that case we fallback to a branch version - return semver.coerce(version === '0.0.0-development' ? branchVersion : version); + GetSequelizeVersion: function () { + // In v4 and v5 package.json files, have a property 'release: { branch: 'v5' }' + // but in v6 it has 'release: { branches: ['v6'] }' + var branchVersion = release.branches ? release.branches[0] : release.branch; + // When executing the tests on Github Actions the version it gets from sequelize is from the repository which has a development version '0.0.0-development' + // in that case we fallback to a branch version + return semver.coerce( + version === '0.0.0-development' ? branchVersion : version + ); }, - GetAdapterVersion: function() { + GetAdapterVersion: function () { const pkgVersion = require('../package.json').version; return semver.coerce(pkgVersion); }, - IsCockroachVersion21_1Plus: async function(connection) { - const versionRow = await connection.query("SELECT version() AS version", { type: QueryTypes.SELECT }); - const cockroachDBVersion = versionRow[0]["version"] + IsCockroachVersion21_1Plus: async function (connection) { + const versionRow = await connection.query('SELECT version() AS version', { + type: QueryTypes.SELECT + }); + const cockroachDBVersion = versionRow[0]['version']; - return semver.gte(semver.coerce(cockroachDBVersion), "21.1.0") + return semver.gte(semver.coerce(cockroachDBVersion), '21.1.0'); }, - GetCockroachDBVersionFromEnvConfig: function() { - const crdbVersion = process.env['CRDB_VERSION'] - return semver.coerce(crdbVersion) + GetCockroachDBVersionFromEnvConfig: function () { + const crdbVersion = process.env['CRDB_VERSION']; + return semver.coerce(crdbVersion); }, - GetVersionSeries: function(versionStr) { + GetVersionSeries: function (versionStr) { // Get the version series from a version string. // E.g. "6.0.1" is of series "6.0". - const regExp=/(\d+\.\d+)(\.|$)/mg + const regExp = /(\d+\.\d+)(\.|$)/gm; let match = regExp.exec(versionStr); if (match === null || match.length < 3) { - return null + return null; } - return match[1] + return match[1]; } }; - diff --git a/tests/belongs_to_many_test.js b/tests/belongs_to_many_test.js index 2eb407d..96b5165 100644 --- a/tests/belongs_to_many_test.js +++ b/tests/belongs_to_many_test.js @@ -29,16 +29,40 @@ var Support = { await Promise.all(schemasPromise.map(p => p.catch(e => e))); }, createSequelizeInstance() { - return new Sequelize('sequelize_test', 'root', '', { - dialect: 'postgres', - port: process.env.COCKROACH_PORT || 26257, - logging: m => console.log(m), - typeValidation: true, - define: { - paranoid: true - }, - dialectOptions: {cockroachdbTelemetryDisabled : true}, - }); + const fs = require('fs'); + const dialectOptions = { cockroachdbTelemetryDisabled: true }; + if (process.env.DB_SSL === 'true') { + dialectOptions.ssl = { + rejectUnauthorized: false, + require: true + }; + if (process.env.CA_CERT_PATH) { + try { + dialectOptions.ssl.ca = fs + .readFileSync(process.env.CA_CERT_PATH) + .toString(); + } catch (e) { + console.error('Failed to read CA certificate:', e.message); + } + } + } + + return new Sequelize( + process.env.DB_NAME || 'sequelize_test', + process.env.DB_USER || 'root', + process.env.DB_PASS || '', + { + dialect: 'postgres', + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 26257, + logging: m => console.log(m), + typeValidation: true, + define: { + paranoid: true + }, + dialectOptions: dialectOptions + } + ); } }; diff --git a/tests/change_column_test.js b/tests/change_column_test.js index 6962536..634b000 100644 --- a/tests/change_column_test.js +++ b/tests/change_column_test.js @@ -187,9 +187,8 @@ describe('QueryInterface', () => { }); it('able to change column to foreign key', async function () { - const foreignKeys = await this.queryInterface.getForeignKeyReferencesForTable( - 'users' - ); + const foreignKeys = + await this.queryInterface.getForeignKeyReferencesForTable('users'); expect(foreignKeys).to.be.an('array'); expect(foreignKeys).to.be.empty; @@ -203,9 +202,8 @@ describe('QueryInterface', () => { onDelete: 'cascade' }); - const newForeignKeys = await this.queryInterface.getForeignKeyReferencesForTable( - 'users' - ); + const newForeignKeys = + await this.queryInterface.getForeignKeyReferencesForTable('users'); expect(newForeignKeys).to.be.an('array'); expect(newForeignKeys).to.have.lengthOf(1); expect(newForeignKeys[0].columnName).to.be.equal('level_id'); @@ -243,9 +241,8 @@ describe('QueryInterface', () => { allowNull: true }); - const newForeignKeys = await this.queryInterface.getForeignKeyReferencesForTable( - 'users' - ); + const newForeignKeys = + await this.queryInterface.getForeignKeyReferencesForTable('users'); expect(firstForeignKeys.length).to.be.equal(newForeignKeys.length); expect(firstForeignKeys[0].columnName).to.be.equal('level_id'); expect(firstForeignKeys[0].columnName).to.be.equal( diff --git a/tests/create-table_test.js b/tests/create-table_test.js index cd993fb..63ca563 100644 --- a/tests/create-table_test.js +++ b/tests/create-table_test.js @@ -6,8 +6,8 @@ var DataTypes = Sequelize.DataTypes; const dialect = 'postgres'; const semver = require('semver'); -const version_helper = require('../source/version_helper.js') -const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig() +const version_helper = require('../source/version_helper.js'); +const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig(); describe('QueryInterface', () => { beforeEach(function () { diff --git a/tests/dao_test.js b/tests/dao_test.js index b0062ce..0badf68 100644 --- a/tests/dao_test.js +++ b/tests/dao_test.js @@ -2,5 +2,4 @@ // Skip reason: This test implements both HSTORE and an array of JSONB. // CRDB does not support these. -describe.skip('[POSTGRES Specific] DAO', () => { -}); +describe.skip('[POSTGRES Specific] DAO', () => {}); diff --git a/tests/data_types_test.js b/tests/data_types_test.js index a125a92..ce24cfd 100644 --- a/tests/data_types_test.js +++ b/tests/data_types_test.js @@ -294,7 +294,12 @@ describe('DataTypes', function () { it.skip('calls parse and bindParam for GEOMETRY', async function () { const Type = new Sequelize.GEOMETRY(); - console.log(this.sequelize) - await testSuccess(this.sequelize, Type, { type: 'Point', coordinates: [125.6, 10.1] }, { useBindParam: true }); + console.log(this.sequelize); + await testSuccess( + this.sequelize, + Type, + { type: 'Point', coordinates: [125.6, 10.1] }, + { useBindParam: true } + ); }); }); diff --git a/tests/dialects_query_interface_test.js b/tests/dialects_query_interface_test.js index ec0f7f4..0135d6a 100644 --- a/tests/dialects_query_interface_test.js +++ b/tests/dialects_query_interface_test.js @@ -1,4 +1,3 @@ // Skip reason: // CRDB does not have CREATE FUNCTION syntax. -describe.skip('[POSTGRES Specific] QueryInterface', () => { -}); +describe.skip('[POSTGRES Specific] QueryInterface', () => {}); diff --git a/tests/dialects_query_test.js b/tests/dialects_query_test.js index 37b6d07..d818240 100644 --- a/tests/dialects_query_test.js +++ b/tests/dialects_query_test.js @@ -8,14 +8,41 @@ const { expect } = require('chai'), const Support = { // Copied from helper, to attend to a specific Sequelize instance creation. createSequelizeInstance: options => { - return new Sequelize('sequelize_test', 'root', '', { - dialect: 'postgres', - port: process.env.COCKROACH_PORT || 26257, - logging: false, - typeValidation: true, - minifyAliases: options.minifyAliases || false, - dialectOptions: {cockroachdbTelemetryDisabled : true} - }); + const fs = require('fs'); + const dialectOptions = { + cockroachdbTelemetryDisabled: true, + ...(options.dialectOptions || {}) + }; + if (process.env.DB_SSL === 'true') { + dialectOptions.ssl = { + rejectUnauthorized: false, + require: true + }; + if (process.env.CA_CERT_PATH) { + try { + dialectOptions.ssl.ca = fs + .readFileSync(process.env.CA_CERT_PATH) + .toString(); + } catch (e) { + console.error('Failed to read CA certificate:', e.message); + } + } + } + + return new Sequelize( + process.env.DB_NAME || 'sequelize_test', + process.env.DB_USER || 'root', + process.env.DB_PASS || '', + { + dialect: 'postgres', + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 26257, + logging: options.logging !== undefined ? options.logging : false, + typeValidation: true, + minifyAliases: options.minifyAliases || false, + dialectOptions: dialectOptions + } + ); } }; diff --git a/tests/helper.js b/tests/helper.js index 7c1bc96..681b0b5 100644 --- a/tests/helper.js +++ b/tests/helper.js @@ -13,6 +13,8 @@ // permissions and limitations under the License. const chai = require('chai'); +const fs = require('fs'); +const path = require('path'); const Sequelize = require('../source'); chai.use(require('chai-as-promised')); @@ -40,7 +42,13 @@ async function cleanupDatabase(sequelize) { const schemas = await sequelize.showAllSchemas(); for (const schema of schemas) { const schemaName = schema.name || schema; - if (schemaName !== sequelize.config.database) { + if ( + schemaName !== sequelize.config.database && + schemaName !== 'public' && + schemaName !== 'crdb_internal' && + !schemaName.startsWith('pg_') && + schemaName !== 'information_schema' + ) { await sequelize.dropSchema(schemaName); } } @@ -59,13 +67,36 @@ after(async function () { }); function makeTestSequelizeInstance() { - return new Sequelize('sequelize_test', 'root', '', { - dialect: 'postgres', - port: process.env.COCKROACH_PORT || 26257, - logging: false, - typeValidation: true, - dialectOptions: {cockroachdbTelemetryDisabled : true}, - }); + const dialectOptions = { cockroachdbTelemetryDisabled: true }; + if (process.env.DB_SSL === 'true') { + dialectOptions.ssl = { + rejectUnauthorized: false, + require: true + }; + if (process.env.CA_CERT_PATH) { + try { + dialectOptions.ssl.ca = fs + .readFileSync(process.env.CA_CERT_PATH) + .toString(); + } catch (e) { + console.error('Failed to read CA certificate:', e.message); + } + } + } + + return new Sequelize( + process.env.DB_NAME || 'sequelize_test', + process.env.DB_USER || 'root', + process.env.DB_PASS || '', + { + dialect: 'postgres', + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 26257, + logging: false, + typeValidation: true, + dialectOptions: dialectOptions + } + ); } module.exports = { makeTestSequelizeInstance }; diff --git a/tests/include_find_all_test.js b/tests/include_find_all_test.js index de335da..1aaeb3f 100644 --- a/tests/include_find_all_test.js +++ b/tests/include_find_all_test.js @@ -9,8 +9,8 @@ const chai = require('chai'), describe('Include', () => { describe('findAll', () => { - beforeEach(function() { - this.fixtureA = async function() { + beforeEach(function () { + this.fixtureA = async function () { const User = this.sequelize.define('User', {}), Company = this.sequelize.define('Company', { name: DataTypes.STRING @@ -30,9 +30,7 @@ describe('Include', () => { Group = this.sequelize.define('Group', { name: DataTypes.STRING }), - GroupMember = this.sequelize.define('GroupMember', { - - }), + GroupMember = this.sequelize.define('GroupMember', {}), Rank = this.sequelize.define('Rank', { name: DataTypes.STRING, canInvite: { @@ -119,12 +117,16 @@ describe('Include', () => { { id: i * 5 + 5, title: 'Monitor' } ]); const products = await Product.findAll(); - const groupMembers = [ + const groupMembers = [ { AccUserId: user.id, GroupId: groups[0].id, RankId: ranks[0].id }, { AccUserId: user.id, GroupId: groups[1].id, RankId: ranks[2].id } ]; if (i < 3) { - groupMembers.push({ AccUserId: user.id, GroupId: groups[2].id, RankId: ranks[1].id }); + groupMembers.push({ + AccUserId: user.id, + GroupId: groups[2].id, + RankId: ranks[1].id + }); } await Promise.all([ GroupMember.bulkCreate(groupMembers), @@ -133,20 +135,11 @@ describe('Include', () => { products[i * 5 + 1], products[i * 5 + 3] ]), - products[i * 5 + 0].setTags([ - tags[0], - tags[2] - ]), - products[i * 5 + 1].setTags([ - tags[1] - ]), + products[i * 5 + 0].setTags([tags[0], tags[2]]), + products[i * 5 + 1].setTags([tags[1]]), products[i * 5 + 0].setCategory(tags[1]), - products[i * 5 + 2].setTags([ - tags[0] - ]), - products[i * 5 + 3].setTags([ - tags[0] - ]), + products[i * 5 + 2].setTags([tags[0]]), + products[i * 5 + 3].setTags([tags[0]]), products[i * 5 + 0].setCompany(companies[4]), products[i * 5 + 1].setCompany(companies[3]), products[i * 5 + 2].setCompany(companies[2]), @@ -170,7 +163,7 @@ describe('Include', () => { // Edit reason: This test originally fails because it expects ProductId = 3. // Edited beforeEach to give it predictable ids. // CRDB does not guarantee that a DB entry will have sequential ids, starting by 1. - it('should be possible to select on columns inside a through table', async function() { + it('should be possible to select on columns inside a through table', async function () { await this.fixtureA(); const products = await this.models.Product.findAll({ @@ -194,7 +187,7 @@ describe('Include', () => { // Edit reason: This test originally fails because it expects ProductId = 3. // Edited beforeEach to give it predictable ids. // CRDB does not guarantee that a DB entry will have sequential ids, starting by 1. - it('should be possible to select on columns inside a through table and a limit', async function() { + it('should be possible to select on columns inside a through table and a limit', async function () { await this.fixtureA(); const products = await this.models.Product.findAll({ diff --git a/tests/include_find_and_count_all_test.js b/tests/include_find_and_count_all_test.js index d291f3c..66fe3f7 100644 --- a/tests/include_find_and_count_all_test.js +++ b/tests/include_find_and_count_all_test.js @@ -8,27 +8,46 @@ const { expect } = require('chai'), Op = Sequelize.Op; describe('Include', () => { - before(function() { + before(function () { this.clock = sinon.useFakeTimers(); }); - after(function() { + after(function () { this.clock.restore(); }); describe('findAndCountAll', () => { - // Edited test. Reason: CRDB does not guarantee sequential ids. This test relies on predictable ids. - it('should be able to include a required model. Result rows should match count', async function() { - const User = this.sequelize.define('User', { name: DataTypes.STRING(40) }, { paranoid: true }), - SomeConnection = this.sequelize.define('SomeConnection', { - m: DataTypes.STRING(40), - fk: DataTypes.INTEGER, - u: DataTypes.INTEGER - }, { paranoid: true }), - A = this.sequelize.define('A', { name: DataTypes.STRING(40) }, { paranoid: true }), - B = this.sequelize.define('B', { name: DataTypes.STRING(40) }, { paranoid: true }), - C = this.sequelize.define('C', { name: DataTypes.STRING(40) }, { paranoid: true }); + it('should be able to include a required model. Result rows should match count', async function () { + const User = this.sequelize.define( + 'User', + { name: DataTypes.STRING(40) }, + { paranoid: true } + ), + SomeConnection = this.sequelize.define( + 'SomeConnection', + { + m: DataTypes.STRING(40), + fk: DataTypes.INTEGER, + u: DataTypes.INTEGER + }, + { paranoid: true } + ), + A = this.sequelize.define( + 'A', + { name: DataTypes.STRING(40) }, + { paranoid: true } + ), + B = this.sequelize.define( + 'B', + { name: DataTypes.STRING(40) }, + { paranoid: true } + ), + C = this.sequelize.define( + 'C', + { name: DataTypes.STRING(40) }, + { paranoid: true } + ); // Associate them User.hasMany(SomeConnection, { foreignKey: 'u', constraints: false }); @@ -47,72 +66,79 @@ describe('Include', () => { // Create an enviroment - await Promise.all([User.bulkCreate([ - // This part differs from original Sequelize test. - // Added sequential ids to creation because of Association expectation. - { id: 1, name: 'Youtube' }, - { id: 2, name: 'Facebook' }, - { id: 3, name: 'Google' }, - { id: 4, name: 'Yahoo' }, - { id: 5, name: '404' } - ]), SomeConnection.bulkCreate([ // Lets count, m: A and u: 1 - { u: 1, m: 'A', fk: 1 }, // 1 // Will be deleted - { u: 2, m: 'A', fk: 1 }, - { u: 3, m: 'A', fk: 1 }, - { u: 4, m: 'A', fk: 1 }, - { u: 5, m: 'A', fk: 1 }, - { u: 1, m: 'B', fk: 1 }, - { u: 2, m: 'B', fk: 1 }, - { u: 3, m: 'B', fk: 1 }, - { u: 4, m: 'B', fk: 1 }, - { u: 5, m: 'B', fk: 1 }, - { u: 1, m: 'C', fk: 1 }, - { u: 2, m: 'C', fk: 1 }, - { u: 3, m: 'C', fk: 1 }, - { u: 4, m: 'C', fk: 1 }, - { u: 5, m: 'C', fk: 1 }, - { u: 1, m: 'A', fk: 2 }, // 2 // Will be deleted - { u: 4, m: 'A', fk: 2 }, - { u: 2, m: 'A', fk: 2 }, - { u: 1, m: 'A', fk: 3 }, // 3 - { u: 2, m: 'A', fk: 3 }, - { u: 3, m: 'A', fk: 3 }, - { u: 2, m: 'B', fk: 2 }, - { u: 1, m: 'A', fk: 4 }, // 4 - { u: 4, m: 'A', fk: 2 } - ]), A.bulkCreate([ - // This part differs from original Sequelize test. - // Added sequential ids to creation because of Association expectation. - { id: 1, name: 'Just' }, - { id: 2, name: 'for' }, - { id: 3, name: 'testing' }, - { id: 4, name: 'proposes' }, - { id: 5, name: 'only' } - ]), B.bulkCreate([ - { name: 'this should not' }, - { name: 'be loaded' } - ]), C.bulkCreate([ - { name: 'because we only want A' } - ])]); + await Promise.all([ + User.bulkCreate([ + // This part differs from original Sequelize test. + // Added sequential ids to creation because of Association expectation. + { id: 1, name: 'Youtube' }, + { id: 2, name: 'Facebook' }, + { id: 3, name: 'Google' }, + { id: 4, name: 'Yahoo' }, + { id: 5, name: '404' } + ]), + SomeConnection.bulkCreate([ + // Lets count, m: A and u: 1 + { u: 1, m: 'A', fk: 1 }, // 1 // Will be deleted + { u: 2, m: 'A', fk: 1 }, + { u: 3, m: 'A', fk: 1 }, + { u: 4, m: 'A', fk: 1 }, + { u: 5, m: 'A', fk: 1 }, + { u: 1, m: 'B', fk: 1 }, + { u: 2, m: 'B', fk: 1 }, + { u: 3, m: 'B', fk: 1 }, + { u: 4, m: 'B', fk: 1 }, + { u: 5, m: 'B', fk: 1 }, + { u: 1, m: 'C', fk: 1 }, + { u: 2, m: 'C', fk: 1 }, + { u: 3, m: 'C', fk: 1 }, + { u: 4, m: 'C', fk: 1 }, + { u: 5, m: 'C', fk: 1 }, + { u: 1, m: 'A', fk: 2 }, // 2 // Will be deleted + { u: 4, m: 'A', fk: 2 }, + { u: 2, m: 'A', fk: 2 }, + { u: 1, m: 'A', fk: 3 }, // 3 + { u: 2, m: 'A', fk: 3 }, + { u: 3, m: 'A', fk: 3 }, + { u: 2, m: 'B', fk: 2 }, + { u: 1, m: 'A', fk: 4 }, // 4 + { u: 4, m: 'A', fk: 2 } + ]), + A.bulkCreate([ + // This part differs from original Sequelize test. + // Added sequential ids to creation because of Association expectation. + { id: 1, name: 'Just' }, + { id: 2, name: 'for' }, + { id: 3, name: 'testing' }, + { id: 4, name: 'proposes' }, + { id: 5, name: 'only' } + ]), + B.bulkCreate([{ name: 'this should not' }, { name: 'be loaded' }]), + C.bulkCreate([{ name: 'because we only want A' }]) + ]); // Delete some of conns to prove the concept - await SomeConnection.destroy({ where: { - m: 'A', - u: 1, - fk: [1, 2] - } }); + await SomeConnection.destroy({ + where: { + m: 'A', + u: 1, + fk: [1, 2] + } + }); this.clock.tick(1000); // Last and most important queries ( we connected 4, but deleted 2, witch means we must get 2 only ) const result = await A.findAndCountAll({ - include: [{ - model: SomeConnection, required: true, - where: { - m: 'A', // Pseudo Polymorphy - u: 1 + include: [ + { + model: SomeConnection, + required: true, + where: { + m: 'A', // Pseudo Polymorphy + u: 1 + } } - }], + ], limit: 5 }); @@ -121,28 +147,53 @@ describe('Include', () => { }); // Edited test. Reason: CRDB does not guarantee sequential ids. This test relies on predictable ids. - it('should correctly filter, limit and sort when multiple includes and types of associations are present.', async function() { + it('should correctly filter, limit and sort when multiple includes and types of associations are present.', async function () { const TaskTag = this.sequelize.define('TaskTag', { - id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, name: { type: DataTypes.STRING } }); const Tag = this.sequelize.define('Tag', { - id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, name: { type: DataTypes.STRING } }); const Task = this.sequelize.define('Task', { - id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, name: { type: DataTypes.STRING } }); const Project = this.sequelize.define('Project', { - id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, m: { type: DataTypes.STRING } }); const User = this.sequelize.define('User', { - id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, name: { type: DataTypes.STRING } }); @@ -184,10 +235,11 @@ describe('Include', () => { { model: Project, where: { [Op.and]: [{ m: 'A' }] }, - include: [{ - model: User, - where: { [Op.and]: [{ name: 'user-name-2' }] } - } + include: [ + { + model: User, + where: { [Op.and]: [{ name: 'user-name-2' }] } + } ] }, { model: Tag } @@ -199,16 +251,26 @@ describe('Include', () => { }); // Edited test. Reason: CRDB does not guarantee sequential ids. This test relies on predictable ids. - it('should properly work with sequelize.function', async function() { + it('should properly work with sequelize.function', async function () { const sequelize = this.sequelize; const User = this.sequelize.define('User', { - id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, first_name: { type: DataTypes.STRING }, last_name: { type: DataTypes.STRING } }); const Project = this.sequelize.define('Project', { - id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, name: { type: DataTypes.STRING } }); @@ -241,9 +303,11 @@ describe('Include', () => { { model: Project, required: true, - where: { name: { - [Op.in]: ['naam-satya', 'guru-satya'] - } } + where: { + name: { + [Op.in]: ['naam-satya', 'guru-satya'] + } + } } ] }); diff --git a/tests/instance_validations_test.js b/tests/instance_validations_test.js index 6201205..c9e2a20 100644 --- a/tests/instance_validations_test.js +++ b/tests/instance_validations_test.js @@ -9,7 +9,7 @@ const chai = require('chai'), describe('InstanceValidator', () => { describe('#update', () => { // Edited test. Changed findByPk parameter. - it('should allow us to update specific columns without tripping the validations', async function() { + it('should allow us to update specific columns without tripping the validations', async function () { const User = this.sequelize.define('model', { username: Sequelize.STRING, email: { @@ -24,10 +24,12 @@ describe('InstanceValidator', () => { }); await User.sync({ force: true }); - const user = await User.create({ username: 'bob', email: 'hello@world.com' }); + const user = await User.create({ + username: 'bob', + email: 'hello@world.com' + }); - await User - .update({ username: 'toni' }, { where: { id: user.id } }); + await User.update({ username: 'toni' }, { where: { id: user.id } }); // Edited PK. It was 1, now it is dynamically obtained. const user0 = await User.findByPk(user.id); @@ -37,7 +39,7 @@ describe('InstanceValidator', () => { // Reason: Errors array need DB-level details to be generated. Since it doesn't, // this test lacks details for its expectations. // https://github.com/cockroachdb/cockroach/issues/63332 - it.skip('should enforce a unique constraint', async function() { + it.skip('should enforce a unique constraint', async function () { const Model = this.sequelize.define('model', { uniqueName: { type: Sequelize.STRING, unique: 'uniqueName' } }); @@ -50,8 +52,10 @@ describe('InstanceValidator', () => { expect(instance0).to.be.ok; const instance = await Model.create(records[1]); expect(instance).to.be.ok; - await Model.update(records[0], { where: { id: instance.id } }) - const err = await expect(Model.update(records[0], { where: { id: instance.id } })).to.be.rejected; + await Model.update(records[0], { where: { id: instance.id } }); + const err = await expect( + Model.update(records[0], { where: { id: instance.id } }) + ).to.be.rejected; console.log(err); expect(err).to.be.an.instanceOf(Error); expect(err.errors).to.have.length(1); @@ -62,7 +66,7 @@ describe('InstanceValidator', () => { // Reason: Errors array need DB-level details to be generated. Since it doesn't, // this test lacks details for its expectations. // https://github.com/sequelize/sequelize/blob/main/lib/dialects/postgres/query.js#L319 - it.skip('should allow a custom unique constraint error message', async function() { + it.skip('should allow a custom unique constraint error message', async function () { const Model = this.sequelize.define('model', { uniqueName: { type: Sequelize.STRING, @@ -78,7 +82,9 @@ describe('InstanceValidator', () => { expect(instance0).to.be.ok; const instance = await Model.create(records[1]); expect(instance).to.be.ok; - const err = await expect(Model.update(records[0], { where: { id: instance.id } })).to.be.rejected; + const err = await expect( + Model.update(records[0], { where: { id: instance.id } }) + ).to.be.rejected; expect(err).to.be.an.instanceOf(Error); expect(err.errors).to.have.length(1); expect(err.errors[0].path).to.include('uniqueName'); @@ -88,7 +94,7 @@ describe('InstanceValidator', () => { // Reason: Errors array need DB-level details to be generated. Since it doesn't, // this test lacks details for its expectations. // https://github.com/sequelize/sequelize/blob/main/lib/dialects/postgres/query.js#L319 - it.skip('should handle multiple unique messages correctly', async function() { + it.skip('should handle multiple unique messages correctly', async function () { const Model = this.sequelize.define('model', { uniqueName1: { type: Sequelize.STRING, diff --git a/tests/model_find_all_grouped_limit_test.js b/tests/model_find_all_grouped_limit_test.js index 0c4a70c..005c25b 100644 --- a/tests/model_find_all_grouped_limit_test.js +++ b/tests/model_find_all_grouped_limit_test.js @@ -246,11 +246,7 @@ describe('Model', () => { expect(users).to.have.length(5); // Changed get('id') to get('position') expect(users.map(u => u.get('position'))).to.deep.equal([ - 1, - 3, - 5, - 7, - 4 + 1, 3, 5, 7, 4 ]); }); @@ -275,11 +271,7 @@ describe('Model', () => { expect(users0).to.have.length(5); // Changed get('id') to get('position') expect(users0.map(u => u.get('position'))).to.deep.equal([ - 1, - 3, - 5, - 7, - 4 + 1, 3, 5, 7, 4 ]); await Promise.all([ @@ -378,9 +370,7 @@ describe('Model', () => { expect(byUser[userKeys[0]]).to.have.length(1); expect(byUser[userKeys[1]]).to.have.length(3); expect(_.invokeMap(byUser[userKeys[1]], 'get', 'id')).to.deep.equal([ - 4, - 3, - 2 + 4, 3, 2 ]); expect(byUser[userKeys[2]]).to.have.length(2); }); diff --git a/tests/model_geometry_test.js b/tests/model_geometry_test.js index eaa37ce..7b8542d 100644 --- a/tests/model_geometry_test.js +++ b/tests/model_geometry_test.js @@ -7,16 +7,20 @@ const { expect } = require('chai'), dialect = 'postgres', semver = require('semver'); -const version_helper = require('../source/version_helper.js') +const version_helper = require('../source/version_helper.js'); -const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig() -const isCRDBVersion21_2Plus = crdbVersion ? semver.gte(crdbVersion, "21.2.0") : false -const sequelizeVersion = version_helper.GetSequelizeVersion() -const isSequelizeVersion6Plus = sequelizeVersion ? semver.satisfies(sequelizeVersion, '>=6') : false +const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig(); +const isCRDBVersion21_2Plus = crdbVersion + ? semver.gte(crdbVersion, '21.2.0') + : false; +const sequelizeVersion = version_helper.GetSequelizeVersion(); +const isSequelizeVersion6Plus = sequelizeVersion + ? semver.satisfies(sequelizeVersion, '>=6') + : false; // Edited test: // It is expected to have CRS field in GEOMETRY fields. -// Geometry is only supported in versions 21.2+. We only run this if +// Geometry is only supported in versions 21.2+. We only run this if // we're on a version of CockroachDB equal or greater to 21.2. describe('Model', () => { describe('GEOMETRY', () => { @@ -29,102 +33,116 @@ describe('Model', () => { await this.User.sync({ force: true }); }); - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('works with aliases fields', async function () { - const Pub = this.sequelize.define( - 'Pub', - { - location: { field: 'coordinates', type: DataTypes.GEOMETRY } - }, - { timestamps: false } - ), - point = { type: 'Point', coordinates: [39.807222, -76.984722] }; - - await Pub.sync({ force: true }); - const pub = await Pub.create({ location: point }); - - expect(pub).not.to.be.null; - expect(pub.location).to.be.deep.eql({ - ...point, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); - - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should create a geometry object', async function () { - const User = this.User; - const point = { type: 'Point', coordinates: [39.807222, -76.984722] }; - - const newUser = await User.create({ - username: 'username', - geometry: point - }); - expect(newUser).not.to.be.null; - expect(newUser.geometry).to.be.deep.eql({ - ...point, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'works with aliases fields', + async function () { + const Pub = this.sequelize.define( + 'Pub', + { + location: { field: 'coordinates', type: DataTypes.GEOMETRY } + }, + { timestamps: false } + ), + point = { type: 'Point', coordinates: [39.807222, -76.984722] }; - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should update a geometry object', async function () { - const User = this.User; - const point1 = { type: 'Point', coordinates: [39.807222, -76.984722] }, - point2 = { type: 'Point', coordinates: [49.807222, -86.984722] }; - const props = { username: 'username', geometry: point1 }; - - await User.create(props); - await User.update( - { geometry: point2 }, - { where: { username: props.username } } - ); - const user = await User.findOne({ where: { username: props.username } }); - expect(user.geometry).to.be.deep.eql({ - ...point2, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); + await Pub.sync({ force: true }); + const pub = await Pub.create({ location: point }); - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('works with crs field', async function () { - const Pub = this.sequelize.define('Pub', { - location: { field: 'coordinates', type: DataTypes.GEOMETRY } - }), - point = { - type: 'Point', - coordinates: [39.807222, -76.984722], + expect(pub).not.to.be.null; + expect(pub.location).to.be.deep.eql({ + ...point, crs: { - type: 'name', properties: { name: 'EPSG:4326' - } + }, + type: 'name' } - }; + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should create a geometry object', + async function () { + const User = this.User; + const point = { type: 'Point', coordinates: [39.807222, -76.984722] }; + + const newUser = await User.create({ + username: 'username', + geometry: point + }); + expect(newUser).not.to.be.null; + expect(newUser.geometry).to.be.deep.eql({ + ...point, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' + } + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should update a geometry object', + async function () { + const User = this.User; + const point1 = { type: 'Point', coordinates: [39.807222, -76.984722] }, + point2 = { type: 'Point', coordinates: [49.807222, -86.984722] }; + const props = { username: 'username', geometry: point1 }; + + await User.create(props); + await User.update( + { geometry: point2 }, + { where: { username: props.username } } + ); + const user = await User.findOne({ + where: { username: props.username } + }); + expect(user.geometry).to.be.deep.eql({ + ...point2, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' + } + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'works with crs field', + async function () { + const Pub = this.sequelize.define('Pub', { + location: { field: 'coordinates', type: DataTypes.GEOMETRY } + }), + point = { + type: 'Point', + coordinates: [39.807222, -76.984722], + crs: { + type: 'name', + properties: { + name: 'EPSG:4326' + } + } + }; - await Pub.sync({ force: true }); - const pub = await Pub.create({ location: point }); - expect(pub).not.to.be.null; - expect(pub.location).to.be.deep.eql({ - ...point, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); + await Pub.sync({ force: true }); + const pub = await Pub.create({ location: point }); + expect(pub).not.to.be.null; + expect(pub.location).to.be.deep.eql({ + ...point, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' + } + }); + } + ); }); describe('GEOMETRY(POINT)', () => { @@ -137,69 +155,80 @@ describe('Model', () => { await this.User.sync({ force: true }); }); - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should create a geometry object', async function () { - const User = this.User; - const point = { type: 'Point', coordinates: [39.807222, -76.984722] }; - - const newUser = await User.create({ - username: 'username', - geometry: point - }); - expect(newUser).not.to.be.null; - expect(newUser.geometry).to.be.deep.eql({ - ...point, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); - - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should update a geometry object', async function () { - const User = this.User; - const point1 = { type: 'Point', coordinates: [39.807222, -76.984722] }, - point2 = { type: 'Point', coordinates: [49.807222, -86.984722] }; - const props = { username: 'username', geometry: point1 }; - - await User.create(props); - await User.update( - { geometry: point2 }, - { where: { username: props.username } } - ); - const user = await User.findOne({ where: { username: props.username } }); - expect(user.geometry).to.be.deep.eql({ - ...point2, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); - - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('works with crs field', async function () { - const User = this.User; - const point = { - type: 'Point', - coordinates: [39.807222, -76.984722], - crs: { - type: 'name', - properties: { - name: 'EPSG:4326' + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should create a geometry object', + async function () { + const User = this.User; + const point = { type: 'Point', coordinates: [39.807222, -76.984722] }; + + const newUser = await User.create({ + username: 'username', + geometry: point + }); + expect(newUser).not.to.be.null; + expect(newUser.geometry).to.be.deep.eql({ + ...point, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' + } + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should update a geometry object', + async function () { + const User = this.User; + const point1 = { type: 'Point', coordinates: [39.807222, -76.984722] }, + point2 = { type: 'Point', coordinates: [49.807222, -86.984722] }; + const props = { username: 'username', geometry: point1 }; + + await User.create(props); + await User.update( + { geometry: point2 }, + { where: { username: props.username } } + ); + const user = await User.findOne({ + where: { username: props.username } + }); + expect(user.geometry).to.be.deep.eql({ + ...point2, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' } - } - }; + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'works with crs field', + async function () { + const User = this.User; + const point = { + type: 'Point', + coordinates: [39.807222, -76.984722], + crs: { + type: 'name', + properties: { + name: 'EPSG:4326' + } + } + }; - const newUser = await User.create({ - username: 'username', - geometry: point - }); - expect(newUser).not.to.be.null; - expect(newUser.geometry).to.be.deep.eql(point); - }); + const newUser = await User.create({ + username: 'username', + geometry: point + }); + expect(newUser).not.to.be.null; + expect(newUser.geometry).to.be.deep.eql(point); + } + ); }); describe('GEOMETRY(LINESTRING)', () => { @@ -212,90 +241,101 @@ describe('Model', () => { await this.User.sync({ force: true }); }); - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should create a geometry object', async function () { - const User = this.User; - const point = { - type: 'LineString', - coordinates: [ - [100.0, 0.0], - [101.0, 1.0] - ] - }; - - const newUser = await User.create({ - username: 'username', - geometry: point - }); - expect(newUser).not.to.be.null; - expect(newUser.geometry).to.be.deep.eql({ - ...point, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); - - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should update a geometry object', async function () { - const User = this.User; - const point1 = { + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should create a geometry object', + async function () { + const User = this.User; + const point = { type: 'LineString', coordinates: [ [100.0, 0.0], [101.0, 1.0] ] - }, - point2 = { - type: 'LineString', - coordinates: [ - [101.0, 0.0], - [102.0, 1.0] - ] }; - const props = { username: 'username', geometry: point1 }; - - await User.create(props); - await User.update( - { geometry: point2 }, - { where: { username: props.username } } - ); - const user = await User.findOne({ where: { username: props.username } }); - expect(user.geometry).to.be.deep.eql({ - ...point2, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('works with crs field', async function () { - const User = this.User; - const point = { - type: 'LineString', - coordinates: [ - [100.0, 0.0], - [101.0, 1.0] - ], - crs: { - type: 'name', - properties: { - name: 'EPSG:4326' + const newUser = await User.create({ + username: 'username', + geometry: point + }); + expect(newUser).not.to.be.null; + expect(newUser.geometry).to.be.deep.eql({ + ...point, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' } - } - }; + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should update a geometry object', + async function () { + const User = this.User; + const point1 = { + type: 'LineString', + coordinates: [ + [100.0, 0.0], + [101.0, 1.0] + ] + }, + point2 = { + type: 'LineString', + coordinates: [ + [101.0, 0.0], + [102.0, 1.0] + ] + }; + const props = { username: 'username', geometry: point1 }; + + await User.create(props); + await User.update( + { geometry: point2 }, + { where: { username: props.username } } + ); + const user = await User.findOne({ + where: { username: props.username } + }); + expect(user.geometry).to.be.deep.eql({ + ...point2, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' + } + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'works with crs field', + async function () { + const User = this.User; + const point = { + type: 'LineString', + coordinates: [ + [100.0, 0.0], + [101.0, 1.0] + ], + crs: { + type: 'name', + properties: { + name: 'EPSG:4326' + } + } + }; - const newUser = await User.create({ - username: 'username', - geometry: point - }); - expect(newUser).not.to.be.null; - expect(newUser.geometry).to.be.deep.eql(point); - }); + const newUser = await User.create({ + username: 'username', + geometry: point + }); + expect(newUser).not.to.be.null; + expect(newUser.geometry).to.be.deep.eql(point); + } + ); }); describe('GEOMETRY(POLYGON)', () => { @@ -308,77 +348,11 @@ describe('Model', () => { await this.User.sync({ force: true }); }); - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should create a geometry object', async function () { - const User = this.User; - const point = { - type: 'Polygon', - coordinates: [ - [ - [100.0, 0.0], - [101.0, 0.0], - [101.0, 1.0], - [100.0, 1.0], - [100.0, 0.0] - ] - ] - }; - - const newUser = await User.create({ - username: 'username', - geometry: point - }); - expect(newUser).not.to.be.null; - expect(newUser.geometry).to.be.deep.eql({ - ...point, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); - - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('works with crs field', async function () { - const User = this.User; - const point = { - type: 'Polygon', - coordinates: [ - [ - [100.0, 0.0], - [101.0, 0.0], - [101.0, 1.0], - [100.0, 1.0], - [100.0, 0.0] - ] - ], - crs: { - type: 'name', - properties: { - name: 'EPSG:4326' - } - } - }; - - const newUser = await User.create({ - username: 'username', - geometry: point - }); - expect(newUser).not.to.be.null; - expect(newUser.geometry).to.be.deep.eql({ - ...point, - crs: { - properties: { - name: 'EPSG:4326' - }, - type: 'name' - } - }); - }); - - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should update a geometry object', async function () { - const User = this.User; - const polygon1 = { + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should create a geometry object', + async function () { + const User = this.User; + const point = { type: 'Polygon', coordinates: [ [ @@ -389,37 +363,114 @@ describe('Model', () => { [100.0, 0.0] ] ] - }, - polygon2 = { + }; + + const newUser = await User.create({ + username: 'username', + geometry: point + }); + expect(newUser).not.to.be.null; + expect(newUser.geometry).to.be.deep.eql({ + ...point, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' + } + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'works with crs field', + async function () { + const User = this.User; + const point = { type: 'Polygon', coordinates: [ [ [100.0, 0.0], - [102.0, 0.0], - [102.0, 1.0], + [101.0, 0.0], + [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] - ] + ], + crs: { + type: 'name', + properties: { + name: 'EPSG:4326' + } + } }; - const props = { username: 'username', geometry: polygon1 }; - - await User.create(props); - await User.update( - { geometry: polygon2 }, - { where: { username: props.username } } - ); - const user = await User.findOne({ where: { username: props.username } }); - expect(user.geometry).to.be.deep.eql({ - ...polygon2, - crs: { - properties: { - name: 'EPSG:4326' + + const newUser = await User.create({ + username: 'username', + geometry: point + }); + expect(newUser).not.to.be.null; + expect(newUser.geometry).to.be.deep.eql({ + ...point, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' + } + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should update a geometry object', + async function () { + const User = this.User; + const polygon1 = { + type: 'Polygon', + coordinates: [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ] + ] }, - type: 'name' - } - }); - }); + polygon2 = { + type: 'Polygon', + coordinates: [ + [ + [100.0, 0.0], + [102.0, 0.0], + [102.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ] + ] + }; + const props = { username: 'username', geometry: polygon1 }; + + await User.create(props); + await User.update( + { geometry: polygon2 }, + { where: { username: props.username } } + ); + const user = await User.findOne({ + where: { username: props.username } + }); + expect(user.geometry).to.be.deep.eql({ + ...polygon2, + crs: { + properties: { + name: 'EPSG:4326' + }, + type: 'name' + } + }); + } + ); }); describe('sql injection attacks', () => { @@ -430,30 +481,36 @@ describe('Model', () => { await this.sequelize.sync({ force: true }); }); - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should properly escape the single quotes', async function () { - await this.Model.create({ - location: { - type: 'Point', - properties: { - exploit: "'); DELETE YOLO INJECTIONS; -- " - }, - coordinates: [39.807222, -76.984722] - } - }); - }); - - ((isCRDBVersion21_2Plus && isSequelizeVersion6Plus) ? it : it.skip)('should properly escape the single quotes in coordinates', async function () { - expect( - this.Model.create({ + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should properly escape the single quotes', + async function () { + await this.Model.create({ location: { type: 'Point', properties: { exploit: "'); DELETE YOLO INJECTIONS; -- " }, - coordinates: [39.807222, "'); DELETE YOLO INJECTIONS; --"] + coordinates: [39.807222, -76.984722] } - }) - ).to.eventually.throw(); - }); + }); + } + ); + + (isCRDBVersion21_2Plus && isSequelizeVersion6Plus ? it : it.skip)( + 'should properly escape the single quotes in coordinates', + async function () { + expect( + this.Model.create({ + location: { + type: 'Point', + properties: { + exploit: "'); DELETE YOLO INJECTIONS; -- " + }, + coordinates: [39.807222, "'); DELETE YOLO INJECTIONS; --"] + } + }) + ).to.eventually.throw(); + } + ); }); }); diff --git a/tests/model_test.js b/tests/model_test.js index 4bce2e5..bc0624a 100644 --- a/tests/model_test.js +++ b/tests/model_test.js @@ -3,9 +3,11 @@ require('./helper'); const semver = require('semver'); -const version_helper = require('../source/version_helper.js') -const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig() -const isCRDBVersion22_1Plus = crdbVersion ? semver.gte(crdbVersion, "22.1.0") : false +const version_helper = require('../source/version_helper.js'); +const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig(); +const isCRDBVersion22_1Plus = crdbVersion + ? semver.gte(crdbVersion, '22.1.0') + : false; const { expect } = require('chai'), { Sequelize, DataTypes } = require('../source'), @@ -62,7 +64,7 @@ describe('Model', () => { expect(indexes).to.have.length(2); // This expected indexes[1] originally. In CRDB <=v21.2 the index we want is // at position 0, and in v22.1 it is at position 1. - const pos = isCRDBVersion22_1Plus ? 1 : 0 + const pos = isCRDBVersion22_1Plus ? 1 : 0; idxUnique = indexes[pos]; expect(idxUnique.primary).to.equal(false); expect(idxUnique.unique).to.equal(true); diff --git a/tests/pool_test.js b/tests/pool_test.js index a403374..2a46030 100644 --- a/tests/pool_test.js +++ b/tests/pool_test.js @@ -7,7 +7,8 @@ const delay = require('delay'); const Support = require('./support'); function assertSameConnection(newConnection, oldConnection) { - expect(oldConnection.processID).to.be.equal(newConnection.processID).and.to.be.ok; + expect(oldConnection.processID).to.be.equal(newConnection.processID).and.to.be + .ok; } function assertNewConnection(newConnection, oldConnection) { @@ -24,11 +25,11 @@ function attachMSSQLUniqueId(connection) { describe.skip('Pooling', () => { if (dialect === 'sqlite' || process.env.DIALECT === 'postgres-native') return; - beforeEach(function() { + beforeEach(function () { this.sinon = sinon.createSandbox(); }); - afterEach(function() { + afterEach(function () { this.sinon.restore(); }); diff --git a/tests/query_interface_test.js b/tests/query_interface_test.js index 6117cbc..3dfd6fa 100644 --- a/tests/query_interface_test.js +++ b/tests/query_interface_test.js @@ -463,10 +463,11 @@ describe('QueryInterface', () => { }); it('should get a list of foreign key references details for the table', async function () { - const references = await this.queryInterface.getForeignKeyReferencesForTable( - 'hosts', - this.sequelize.options - ); + const references = + await this.queryInterface.getForeignKeyReferencesForTable( + 'hosts', + this.sequelize.options + ); expect(references).to.have.length(3); for (const ref of references) { expect(ref.tableName).to.equal('hosts'); diff --git a/tests/run_tests/getTestsToIgnore.js b/tests/run_tests/getTestsToIgnore.js index 25b727e..b299f88 100644 --- a/tests/run_tests/getTestsToIgnore.js +++ b/tests/run_tests/getTestsToIgnore.js @@ -1,7 +1,7 @@ const fs = require('fs'), path = require('path'), readline = require('readline'); -const version_helper = require ('../../source/version_helper.js') +const version_helper = require('../../source/version_helper.js'); const semver = require('semver'); const sharedIgnoredTestsPath = './../.github/workflows/ignore_tests/shared'; @@ -20,7 +20,7 @@ function parseFilesForTests(files) { } return arr; - }) + }); } function getTestNames() { @@ -28,10 +28,10 @@ function getTestNames() { return path.join(sharedIgnoredTestsPath, f); }); - const sequelizeVersion = version_helper.GetSequelizeVersion() + const sequelizeVersion = version_helper.GetSequelizeVersion(); if (semver.satisfies(sequelizeVersion, '<=5')) { const v5IgnoredTestsPath = './../.github/workflows/ignore_tests/v5'; - var v5files = fs.readdirSync(v5IgnoredTestsPath) + var v5files = fs.readdirSync(v5IgnoredTestsPath); files = files.concat( v5files.map(f => { return path.join(v5IgnoredTestsPath, f); @@ -39,7 +39,9 @@ function getTestNames() { ); } - return Promise.all(parseFilesForTests(files)).then(arr => arr.flat().join('|')) + return Promise.all(parseFilesForTests(files)).then(arr => + arr.flat().join('|') + ); } module.exports = getTestNames; diff --git a/tests/scope_test.js b/tests/scope_test.js index 78b96cf..30812f4 100644 --- a/tests/scope_test.js +++ b/tests/scope_test.js @@ -200,21 +200,15 @@ describe('associations', () => { await this.ItemTag.sync({ force: true }); - const [ - post0, - image0, - question0, - tagA, - tagB, - tagC - ] = await Promise.all([ - this.Post.create(), - this.Image.create(), - this.Question.create(), - this.Tag.create({ name: 'tagA' }), - this.Tag.create({ name: 'tagB' }), - this.Tag.create({ name: 'tagC' }) - ]); + const [post0, image0, question0, tagA, tagB, tagC] = + await Promise.all([ + this.Post.create(), + this.Image.create(), + this.Question.create(), + this.Tag.create({ name: 'tagA' }), + this.Tag.create({ name: 'tagB' }), + this.Tag.create({ name: 'tagC' }) + ]); this.post = post0; this.image = image0; diff --git a/tests/sequelize_query_test.js b/tests/sequelize_query_test.js index 6ee9166..0b3cd9d 100644 --- a/tests/sequelize_query_test.js +++ b/tests/sequelize_query_test.js @@ -5,9 +5,11 @@ const { Sequelize, DataTypes } = require('../source'); const dialect = 'postgres'; const sinon = require('sinon'); const semver = require('semver'); -const version_helper = require('../source/version_helper.js') -const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig() -const isCRDBVersion21_1Plus = crdbVersion ? semver.gte(crdbVersion, "21.1.0") : false +const version_helper = require('../source/version_helper.js'); +const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig(); +const isCRDBVersion21_1Plus = crdbVersion + ? semver.gte(crdbVersion, '21.1.0') + : false; const qq = str => { if (dialect === 'postgres' || dialect === 'mssql') { @@ -22,16 +24,43 @@ const qq = str => { const Support = { // Copied from helper, to attend to a specific Sequelize instance creation. createSequelizeInstance: options => { - return new Sequelize('sequelize_test', 'root', '', { - dialect: 'postgres', - port: process.env.COCKROACH_PORT || 26257, - logging: false, - typeValidation: true, - benchmark: options.benchmark || false, - logQueryParameters: options.logQueryParameters || false, - minifyAliases: options.minifyAliases || false, - dialectOptions: {cockroachdbTelemetryDisabled : true} - }); + const fs = require('fs'); + const dialectOptions = { + cockroachdbTelemetryDisabled: true, + ...(options.dialectOptions || {}) + }; + if (process.env.DB_SSL === 'true') { + dialectOptions.ssl = { + rejectUnauthorized: false, + require: true + }; + if (process.env.CA_CERT_PATH) { + try { + dialectOptions.ssl.ca = fs + .readFileSync(process.env.CA_CERT_PATH) + .toString(); + } catch (e) { + console.error('Failed to read CA certificate:', e.message); + } + } + } + + return new Sequelize( + process.env.DB_NAME || 'sequelize_test', + process.env.DB_USER || 'root', + process.env.DB_PASS || '', + { + dialect: 'postgres', + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 26257, + logging: options.logging !== undefined ? options.logging : false, + typeValidation: true, + benchmark: options.benchmark || false, + logQueryParameters: options.logQueryParameters || false, + minifyAliases: options.minifyAliases || false, + dialectOptions: dialectOptions + } + ); } }; @@ -101,9 +130,9 @@ describe('Sequelize', () => { ).to.be.rejectedWith(Sequelize.UniqueConstraintError); if (isCRDBVersion21_1Plus) { - expect(spy.callCount).to.eql(1); + expect(spy.callCount).to.eql(1); } else { - expect(spy.callCount).to.eql(3); + expect(spy.callCount).to.eql(3); } }); }); diff --git a/tests/support.js b/tests/support.js index e405bfe..9d9160f 100644 --- a/tests/support.js +++ b/tests/support.js @@ -1,17 +1,45 @@ const { isDeepStrictEqual } = require('util'); const Sequelize = require('../source'); +const fs = require('fs'); + const Support = { createSequelizeInstance: function (options = {}) { - return new Sequelize('sequelize_test', 'root', '', { - dialect: 'postgres', - port: process.env.COCKROACH_PORT || 26257, - logging: console.log, - typeValidation: true, - minifyAliases: options.minifyAliases || false, - dialectOptions: {cockroachdbTelemetryDisabled : true}, - ...options - }); + const dialectOptions = { + cockroachdbTelemetryDisabled: true, + ...(options.dialectOptions || {}) + }; + if (process.env.DB_SSL === 'true') { + dialectOptions.ssl = { + rejectUnauthorized: false, + require: true + }; + if (process.env.CA_CERT_PATH) { + try { + dialectOptions.ssl.ca = fs + .readFileSync(process.env.CA_CERT_PATH) + .toString(); + } catch (e) { + console.error('Failed to read CA certificate:', e.message); + } + } + } + + return new Sequelize( + process.env.DB_NAME || 'sequelize_test', + process.env.DB_USER || 'root', + process.env.DB_PASS || '', + { + dialect: 'postgres', + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 26257, + logging: options.logging !== undefined ? options.logging : console.log, + typeValidation: true, + minifyAliases: options.minifyAliases || false, + dialectOptions: dialectOptions, + ...options + } + ); }, isDeepEqualToOneOf: function (actual, expectedOptions) { @@ -37,7 +65,7 @@ const Support = { await Promise.all(schemasPromise.map(p => p.catch(e => e))); } -} +}; Support.sequelize = Support.createSequelizeInstance();