From 9d03b8cde86bbe1b96f8b32e9542b77179efbf49 Mon Sep 17 00:00:00 2001 From: Agnes Lin Date: Sun, 22 Mar 2020 21:22:07 -0400 Subject: [PATCH] push column/idcolumn to connector level and renam --- lib/connector.js | 56 +++++++++++++ lib/sql.js | 50 ----------- test/connectors/test-connector.js | 36 ++++++++ test/name-mapping.test.js | 135 ++++++++++++++++++++++++++++++ test/sql.test.js | 40 +-------- 5 files changed, 229 insertions(+), 88 deletions(-) create mode 100644 test/connectors/test-connector.js create mode 100644 test/name-mapping.test.js diff --git a/lib/connector.js b/lib/connector.js index 604da3c..a733d5f 100644 --- a/lib/connector.js +++ b/lib/connector.js @@ -218,6 +218,62 @@ Connector.prototype.id = function(model, prop) { return p && p.id; }; +/** + * Return the database name of the property of the model if it exists. + * Otherwise return the property name. + * Some connectors allow the column/field name to be customized + * at the model property definition level as `column`, + * `columnName`, or `field`. For example, + * + * ```json + * "name": { + * "type": "string", + * "mysql": { + * "column": "NAME" + * } + * } + * ``` + * @param {String} model The target model name + * @param {String} prop The property name + * + * @returns {String} The database mapping name of the property of the model if it exists + */ +Connector.prototype.getPropertyDbName = Connector.prototype.column = +function(model, property) { + const prop = this.getPropertyDefinition(model, property); + let mappingName; + if (prop && prop[this.name]) { + mappingName = prop[this.name].column || prop[this.name].columnName || + prop[this.name].field || prop[this.name].fieldName; + if (mappingName) { + // Explicit column name, return as-is + return mappingName; + } + } + + // Check if name attribute provided for column name + if (prop && prop.name) { + return prop.name; + } + mappingName = property; + if (typeof this.dbName === 'function') { + mappingName = this.dbName(mappingName); + } + return mappingName; +}; + +/** + * Return the database name of the id property of the model if it exists. + * Otherwise return the name of the id property. + * @param {String} model The target model name + * @param {String} prop The property name + * @returns {String} the database mapping name of the id property of the model if it exists. + */ +Connector.prototype.getIdDbName = Connector.prototype.idColumn = function(model) { + const idName = this.getDataSource(model).getModelDefinition(model).idName(); + return this.getPropertyDbName(model, idName); +}; + /** * Hook to be called by DataSource for defining a model * @param {Object} modelDefinition The model definition diff --git a/lib/sql.js b/lib/sql.js index e73685f..80856d4 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -354,46 +354,6 @@ SQLConnector.prototype.table = function(model) { return tableName; }; -/** - * Get the column name for the given model property. The column name can be - * customized at the model property definition level as `column` or - * `columnName`. For example, - * - * ```json - * "name": { - * "type": "string", - * "mysql": { - * "column": "NAME" - * } - * } - * ``` - * - * @param {String} model The model name - * @param {String} property The property name - * @returns {String} The column name - */ -SQLConnector.prototype.column = function(model, property) { - const prop = this.getPropertyDefinition(model, property); - let columnName; - if (prop && prop[this.name]) { - columnName = prop[this.name].column || prop[this.name].columnName; - if (columnName) { - // Explicit column name, return as-is - return columnName; - } - } - - // Check if name attribute provided for column name - if (prop && prop.name) { - return prop.name; - } - columnName = property; - if (typeof this.dbName === 'function') { - columnName = this.dbName(columnName); - } - return columnName; -}; - /** * Get the column metadata for the given model property * @param {String} model The model name @@ -420,16 +380,6 @@ SQLConnector.prototype.propertyName = function(model, column) { return null; }; -/** - * Get the id column name. - * @param {String} model The model name - * @returns {String} The id column name - */ -SQLConnector.prototype.idColumn = function(model) { - const idName = this.getDataSource(model).getModelDefinition(model).idName(); - return this.column(model, idName); -}; - /** * Get the escaped id column name * @param {String} model The model name diff --git a/test/connectors/test-connector.js b/test/connectors/test-connector.js new file mode 100644 index 0000000..bbb6772 --- /dev/null +++ b/test/connectors/test-connector.js @@ -0,0 +1,36 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: loopback-connector +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +/* + * A mockup connector that extends NoSQL/SQL connector + * to check property name mapping. + */ +const util = require('util'); +const Connector = require('../../lib/connector'); +const debug = require('debug')('loopback:connector:test-connector'); + +exports.initialize = function initializeDataSource(dataSource, callback) { + process.nextTick(function() { + if (callback) { + const connector = new TestConnector(dataSource.settings); + connector.dataSource = dataSource; + dataSource.connector = connector; + callback(null, connector); + } + }); +}; + +function TestConnector(settings) { + Connector.call(this, 'testdb', settings); + this._tables = {}; + this.data = {}; +} + +util.inherits(TestConnector, Connector); + +TestConnector.prototype.dbName = function(name) { + return name.toUpperCase(); +}; diff --git a/test/name-mapping.test.js b/test/name-mapping.test.js new file mode 100644 index 0000000..2d79f56 --- /dev/null +++ b/test/name-mapping.test.js @@ -0,0 +1,135 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: loopback-connector +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const expect = require('chai').expect; +const Connector = require('../lib/connector'); +const testConnector = require('./connectors/test-connector'); + +const juggler = require('loopback-datasource-juggler'); +const ds = new juggler.DataSource({ + connector: testConnector, + debug: true, +}); + +/* eslint-disable one-var */ +let connector; +let Customer; +let Order; +/* eslint-enable one-var */ + +describe('Name mapping', function() { + let connector, builder; + + before(() => { + connector = ds.connector; + connector._tables = {}; + connector._models = {}; + Customer = ds.createModel( + 'Customer', + { + name: { + id: true, + type: String, + testdb: { + column: 'FIRSTNAME', + dataType: 'VARCHAR', + dataLength: 32, + }, + }, + middleName: { + type: Boolean, + name: 'middle_name', + postgresql: { + column: 'MIDDLENAME', + }, + }, + lastName: { + type: Boolean, + testdb: { + column: 'LASTNAME', + }, + }, + primaryAddress: { + type: String, + name: 'primary_address', + }, + address: String, + }, + {testdb: {table: 'CUSTOMER'}}, + ); + + // use field in this model to mock NoSQL DB + Order = ds.createModel( + 'Order', + { + id: { + id: true, + type: Number, + testdb: { + field: 'my_id', + dataType: 'ObjectId', + }, + }, + des: { + type: String, + testdb: { + field: 'DESCRIPTION', + }, + }, + }, + {testdb: {table: 'ORDER'}}, + ); + }); + + context('getIdDbName', function() { + it('should map id column name', function() { + const idCol = connector.getIdDbName('Customer'); + expect(idCol).to.eql('FIRSTNAME'); + }); + + it('alias idColumn should map id column name', function() { + const idCol = connector.idColumn('Customer'); + expect(idCol).to.eql('FIRSTNAME'); + }); + + it('should map id field name', function() { + const idCol = connector.getIdDbName('Order'); + expect(idCol).to.eql('my_id'); + }); + }); + context('getPropertyDbName', function() { + it('prefers property name if the database name is not matched', function() { + const column = connector.getPropertyDbName('Customer', 'middleName'); + expect(column).to.eql('middle_name'); + }); + + it('prefers database-specific column name over property name', function() { + const column = connector.getPropertyDbName('Customer', 'lastName'); + expect(column).to.eql('LASTNAME'); + }); + + it('alias column should map the column name', function() { + const column = connector.column('Customer', 'lastName'); + expect(column).to.eql('LASTNAME'); + }); + + it('propertyMapping should map column name from name attribute', function() { + const column = connector.getPropertyDbName('Customer', 'primaryAddress'); + expect(column).to.eql('primary_address'); + }); + + it('connector-preffered configuration (UPPERCASE) is applied if no columm/field name is provided', function() { + const column = connector.getPropertyDbName('Customer', 'address'); + expect(column).to.eql('ADDRESS'); + }); + + it('prefers database-specific field name over property name', function() { + const column = connector.getPropertyDbName('Order', 'des'); + expect(column).to.eql('DESCRIPTION'); + }); + }); +}); diff --git a/test/sql.test.js b/test/sql.test.js index 57d9725..f8a31af 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -79,37 +79,13 @@ describe('sql connector', function() { {testdb: {table: 'ORDER'}}); }); + // tests for column names mapping are moved to name-mapping.test.js + it('should map table name', function() { const table = connector.table('customer'); expect(table).to.eql('CUSTOMER'); }); - it('should map column name', function() { - const column = connector.column('customer', 'name'); - expect(column).to.eql('NAME'); - }); - - it('should map column name from name attribute', function() { - const column = connector.column('customer', 'primaryAddress'); - expect(column).to.eql('primary_address'); - }); - - it('prefers database-specific column name over property name', function() { - const column = connector.column('customer', 'lastName'); - expect(column).to.eql('LASTNAME'); - }); - - it('uses database-specific column name over property name even if the column name \ - does not follow the connector-specific configuration (UPPERCASE)', function() { - const column = connector.column('order', 'des'); - expect(column).to.eql('description'); - }); - - it('prefers property name when database is different', function() { - const column = connector.column('customer', 'middleName'); - expect(column).to.eql('middle_name'); - }); - it('should find column metadata', function() { const column = connector.columnMetadata('customer', 'name'); expect(column).to.eql({ @@ -124,18 +100,6 @@ describe('sql connector', function() { expect(prop).to.eql('name'); }); - it('should map id column name', function() { - const idCol = connector.idColumn('customer'); - expect(idCol).to.eql('NAME'); - }); - - it('should map id column name even if the column name does not \ - follow the connector-specific configuration (UPPERCASE)', function() { - const idCol = connector.idColumn('order'); - // shouldn't be converted to ORDERID - expect(idCol).to.eql('orderId'); - }); - it('should find escaped id column name', function() { const idCol = connector.idColumnEscaped('customer'); expect(idCol).to.eql('`NAME`');