├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── gulpfile.js ├── index.js ├── lib ├── intl-phone-number.js ├── mongoose-intl-phone-number.js └── mongoose-intl-phone-number.spec.js ├── package-lock.json ├── package.json └── readme.hbs /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "validateIndentation": 4, 4 | "requireTrailingComma": false, 5 | "requireCamelCaseOrUpperCaseIdentifiers": false, 6 | "safeContextKeyword": false, 7 | "requireSpacesInsideObjectBrackets": false, 8 | "requireSpacesInAnonymousFunctionExpression": { 9 | "beforeOpeningCurlyBrace": true 10 | }, 11 | "maximumLineLength": false 12 | } 13 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "undef": true, 4 | "unused": "vars", 5 | "node": true, 6 | "predef": [ 7 | "module", 8 | "require", 9 | "process", 10 | "describe", 11 | "before", 12 | "beforeEach", 13 | "after", 14 | "it", 15 | "console", 16 | "setTimeout" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '9' 10 | - '8' 11 | - '4' 12 | services: 13 | - mongodb 14 | script: 15 | - npm run travis 16 | before_script: 17 | - npm prune 18 | after_success: 19 | - 'curl -Lo travis_after_all.py https://git.io/travis_after_all' 20 | - python travis_after_all.py 21 | - export $(cat .to_export_back) &> /dev/null 22 | - npm run semantic-release 23 | branches: 24 | except: 25 | - /^v\d+\.\d+\.\d+$/ 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mongoose-intl-phone-number 2 | ==================== 3 | [![Build Status](https://travis-ci.org/Dashride/mongoose-intl-phone-number.svg?branch=master)](https://travis-ci.org/Dashride/mongoose-intl-phone-number) 4 | [![Coverage Status](https://coveralls.io/repos/Dashride/mongoose-intl-phone-number/badge.svg?branch=master&service=github)](https://coveralls.io/github/Dashride/mongoose-intl-phone-number?branch=master) 5 | [![Dependency Status](https://david-dm.org/Dashride/mongoose-intl-phone-number.svg)](https://david-dm.org/Dashride/mongoose-intl-phone-number) 6 | [![npm version](https://badge.fury.io/js/mongoose-intl-phone-number.svg)](http://badge.fury.io/js/mongoose-intl-phone-number) 7 | 8 | This module takes a string of numbers and determines their validity as well as returns data about the phone numbers. This module is based on Google's [libphonenumber](https://github.com/mattbornski/libphonenumber). 9 | 10 | ## How it works 11 | A phone number is provided on the document, during the pre-save/validate hook (you can specify), it runs the phone number through [libphonenumber](https://github.com/mattbornski/libphonenumber) and stores the data returned onto fields in the document model. 12 | 13 | ## Use Case 14 | Applications that accept international phone numbers should use this plugin to gather and store information about the number such as country code, national format, etc. 15 | 16 | ## Installation 17 | 18 | `npm install --save mongoose-intl-phone-number` 19 | 20 | ## API Reference 21 | 22 | 23 | ## mongooseIntlPhoneNumber 24 | Validates a phone number against google's libphonenumber, otherwise returns a validation error. 25 | 26 | **Example** 27 | ```js 28 | var mongooseIntlPhoneNumber = require('mongoose-intl-phone-number'); 29 | var schema = Schema({...}); 30 | 31 | schema.plugin(mongooseIntlPhoneNumber, { 32 | hook: 'validate', 33 | phoneNumberField: 'phoneNumber', 34 | nationalFormatField: 'nationalFormat', 35 | internationalFormat: 'internationalFormat', 36 | countryCodeField: 'countryCode', 37 | }); 38 | ``` 39 | Use it with a model... 40 | ```js 41 | var Customer = mongoose.model('Customer'); 42 | 43 | var customer = new Customer({ 44 | firstName: 'test', 45 | lastName: 'customer', 46 | customerType: 'testing', 47 | phoneNumber: '+18888675309', 48 | email: 'test@testing.com' 49 | }); 50 | 51 | customer.save(); 52 | ``` 53 | 54 | Resulting document... 55 | ```js 56 | { 57 | "firstName": "test", 58 | "lastName": "customer", 59 | "customerType": "testing", 60 | "phoneNumber": "+18888675309", 61 | "nationalFormat": "(888) 867-5309", 62 | "internationalFormat": "+1 888-867-5309" 63 | "countryCode": "US" 64 | } 65 | ``` 66 | 67 | 68 | ### mongooseIntlPhoneNumber~mongooseIntlPhoneNumber(schema, [options]) 69 | Attaches the mongoose document hook and parses the phone number that is provided. 70 | 71 | **Kind**: inner method of [mongooseIntlPhoneNumber](#module_mongooseIntlPhoneNumber) 72 | 73 | | Param | Type | Default | Description | 74 | | --- | --- | --- | --- | 75 | | schema | object | | Mongoose schema | 76 | | [options] | object | | | 77 | | [options.hook] | string | "validate" | | 78 | | [options.phoneNumberField] | string | "phoneNumber" | | 79 | | [options.nationalFormatField] | string | "nationalFormat" | | 80 | | [options.internationalFormatField] | string | "internationalFormat" | | 81 | | [options.countryCodeField] | string | "countryCode" | | 82 | 83 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const gulp = require('gulp'); 5 | const gutil = require('gulp-util'); 6 | const mocha = require('gulp-mocha'); 7 | const istanbul = require('gulp-istanbul'); 8 | const jshint = require('gulp-jshint'); 9 | const jsdoc2md = require('gulp-jsdoc-to-markdown'); 10 | const concat = require('gulp-concat'); 11 | const isparta = require('isparta'); 12 | 13 | const paths = { 14 | js: ['./**/*.js', '!./**/*.spec.js', '!./coverage/**/*.js', '!./node_modules/**/*.js'], 15 | specs: ['./lib/*.spec.js'], 16 | coverage: ['./lib/*.js', '!./lib/*spec.js'], 17 | all: ['./**/*.js', '!./coverage/**/*.js', '!./node_modules/**/*.js'] 18 | }; 19 | 20 | gulp.task('watch', ['test'], function (done) { 21 | const glob = paths.es6.all; 22 | return gulp.watch(glob, ['test']); 23 | }); 24 | 25 | gulp.task('lint', function() { 26 | return gulp.src(paths.all) 27 | .pipe(jshint()) 28 | .pipe(jshint.reporter('jshint-stylish')) 29 | .pipe(jshint.reporter('fail')); 30 | }); 31 | 32 | gulp.task('coveralls', function(done) { 33 | return gulp.src(paths.coverage) 34 | .pipe(istanbul({ 35 | instrumenter: isparta.Instrumenter, 36 | includeUntested: true 37 | })) 38 | .pipe(istanbul.hookRequire()) 39 | .on('finish', function() { 40 | return gulp.src(paths.specs) 41 | .pipe(mocha({ 42 | reporter: 'spec' 43 | })) 44 | .pipe(istanbul.writeReports({ 45 | reporters: ['lcovonly', 'text'], 46 | })); 47 | }); 48 | }); 49 | 50 | gulp.task('coverage', function(done) { 51 | return gulp.src(paths.coverage) 52 | .pipe(istanbul({ 53 | instrumenter: isparta.Instrumenter, 54 | includeUntested: false 55 | })) 56 | .pipe(istanbul.hookRequire()) 57 | .on('finish', function() { 58 | return gulp.src(paths.specs) 59 | .pipe(mocha({ 60 | reporter: 'spec' 61 | })) 62 | .pipe(istanbul.writeReports({ 63 | reporters: ['text', 'html'], 64 | })); 65 | }); 66 | }); 67 | 68 | gulp.task('test', [/*'lint'*/], function(done) { 69 | return gulp.src(paths.specs) 70 | .pipe(mocha({ 71 | reporter: 'spec' 72 | })); 73 | }); 74 | 75 | gulp.task('docs', function() { 76 | return gulp.src(paths.js) 77 | .pipe(concat('README.md')) 78 | .pipe(jsdoc2md({template: fs.readFileSync('./readme.hbs', 'utf8')})) 79 | .on('error', function(err) { 80 | gutil.log('jsdoc2md failed:', err.message); 81 | }) 82 | .pipe(gulp.dest('.')); 83 | }); 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/mongoose-intl-phone-number').mongooseIntlPhoneNumber; 4 | -------------------------------------------------------------------------------- /lib/intl-phone-number.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PhoneNumberUtil = require('google-libphonenumber').PhoneNumberUtil; 4 | 5 | const phoneUtil = PhoneNumberUtil.getInstance(); 6 | 7 | const PhoneNumberFormat = { 8 | E164: 0, 9 | INTERNATIONAL: 1, 10 | NATIONAL: 2, 11 | RFC3966: 3 12 | }; 13 | 14 | // https://github.com/seegno/google-libphonenumber/blob/4a210137662fd1d4c2d9d37f54c3f6c366458985/src/phonenumberutil.js#L965 15 | const PhoneNumberErrorCodes = { 16 | IS_POSSIBLE: 0, 17 | INVALID_COUNTRY_CODE: 1, 18 | TOO_SHORT: 2, 19 | TOO_LONG: 3, 20 | IS_POSSIBLE_LOCAL_ONLY: 4, 21 | INVALID_LENGTH: 5, 22 | }; 23 | 24 | const PhoneNumberErrorReasons = [ 25 | 'Number is unknown.', 26 | 'Country code is invalid.', 27 | 'Number is too short.', 28 | 'Number is too long.', 29 | 'Number is an unknown local number.', 30 | 'Number length is invalid for this region.', 31 | ]; 32 | 33 | /** 34 | * @class IntlPhoneNumber 35 | */ 36 | class IntlPhoneNumber { 37 | /** 38 | * @param {string} phoneNumber 39 | */ 40 | constructor(phoneNumber) { 41 | this.phoneNumber = phoneNumber; 42 | this.number = phoneUtil.parseAndKeepRawInput(phoneNumber); 43 | } 44 | 45 | /** 46 | * Determines if the number is valid. 47 | * @return {boolean} 48 | */ 49 | get isValid() { 50 | return phoneUtil.isValidNumber(this.number) && phoneUtil.isPossibleNumber(this.number); 51 | } 52 | 53 | /** 54 | * Returns the country code for the parsed number. 55 | * @return {string} 56 | */ 57 | get countryCode() { 58 | return phoneUtil.getRegionCodeForNumber(this.number); 59 | } 60 | 61 | /** 62 | * Returns the e164 format for the parsed number. 63 | * @return {string} 64 | */ 65 | get e164Format() { 66 | return phoneUtil.format(this.number, PhoneNumberFormat.E164); 67 | } 68 | 69 | /** 70 | * Returns the national format for the parsed number. 71 | * @return {string} 72 | */ 73 | get nationalFormat() { 74 | return phoneUtil.format(this.number, PhoneNumberFormat.NATIONAL); 75 | } 76 | 77 | /** 78 | * Returns the international format for the parsed number. 79 | * @return {string} 80 | */ 81 | get internationalFormat() { 82 | return phoneUtil.format(this.number, PhoneNumberFormat.INTERNATIONAL); 83 | } 84 | 85 | /** 86 | * Determines the proper error message based on the error code. 87 | * @return {string} 88 | */ 89 | get errorMsg() { 90 | let message = 'Phone number is not valid.'; 91 | 92 | const errorCode = this.errorCode; 93 | const reason = PhoneNumberErrorReasons[errorCode]; 94 | 95 | if (reason) { 96 | message += ' ' + reason; 97 | } 98 | 99 | return message; 100 | } 101 | 102 | /** 103 | * Determines the error code for a number that was not able to be parsed. 104 | * @return {number} 105 | */ 106 | get errorCode() { 107 | return phoneUtil.isPossibleNumberWithReason(this.number); 108 | } 109 | } 110 | 111 | exports.IntlPhoneNumber = IntlPhoneNumber; 112 | -------------------------------------------------------------------------------- /lib/mongoose-intl-phone-number.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const IntlPhoneNumber = require('./intl-phone-number').IntlPhoneNumber; 4 | const _ = require('lodash'); 5 | 6 | /** 7 | * @module mongooseIntlPhoneNumber 8 | * @desc Validates a phone number against google's libphonenumber, otherwise returns a validation error. 9 | * @example 10 | ```js 11 | var mongooseIntlPhoneNumber = require('mongoose-intl-phone-number'); 12 | var schema = Schema({...}); 13 | 14 | schema.plugin(mongooseIntlPhoneNumber, { 15 | hook: 'validate', 16 | phoneNumberField: 'phoneNumber', 17 | nationalFormatField: 'nationalFormat', 18 | internationalFormat: 'internationalFormat', 19 | countryCodeField: 'countryCode', 20 | }); 21 | ``` 22 | Use it with a model... 23 | ```js 24 | var Customer = mongoose.model('Customer'); 25 | 26 | var customer = new Customer({ 27 | firstName: 'test', 28 | lastName: 'customer', 29 | customerType: 'testing', 30 | phoneNumber: '+18888675309', 31 | email: 'test@testing.com' 32 | }); 33 | 34 | customer.save(); 35 | ``` 36 | 37 | Resulting document... 38 | ```js 39 | { 40 | "firstName": "test", 41 | "lastName": "customer", 42 | "customerType": "testing", 43 | "phoneNumber": "+18888675309", 44 | "nationalFormat": "(888) 867-5309", 45 | "internationalFormat": "+1 888-867-5309" 46 | "countryCode": "US" 47 | } 48 | ``` 49 | */ 50 | /** 51 | * Attaches the mongoose document hook and parses the phone number that is provided. 52 | * @param {object} schema - Mongoose schema 53 | * @param {object} [options] 54 | * @param {string} [options.hook=validate] 55 | * @param {string} [options.phoneNumberField=phoneNumber] 56 | * @param {string} [options.nationalFormatField=nationalFormat] 57 | * @param {string} [options.internationalFormatField=internationalFormat] 58 | * @param {string} [options.countryCodeField=countryCode] 59 | */ 60 | function mongooseIntlPhoneNumber(schema, options) { 61 | if (_.isUndefined(options)) options = {}; 62 | 63 | _.defaults(options, { 64 | hook: 'validate', 65 | phoneNumberField: 'phoneNumber', 66 | nationalFormatField: 'nationalFormat', 67 | internationalFormatField: 'internationalFormat', 68 | countryCodeField: 'countryCode', 69 | }); 70 | 71 | const hook = options.hook; 72 | const phoneNumberField = options.phoneNumberField; 73 | const nationalFormatField = options.nationalFormatField; 74 | const internationalFormatField = options.internationalFormatField; 75 | const countryCodeField = options.countryCodeField; 76 | 77 | // If paths don't exist in schema add them 78 | [phoneNumberField, nationalFormatField, internationalFormatField, countryCodeField].forEach(function(path) { 79 | if (!schema.path(path)) { 80 | schema.add({ 81 | [path]: { type: String } 82 | }); 83 | } 84 | }); 85 | 86 | schema.pre(hook, function parsePhoneNumber(next) { 87 | const isRequired = schema.path(phoneNumberField).isRequired; 88 | const hasPhoneNumber = this.get(phoneNumberField).length > 0; 89 | 90 | // Only return validation errors if the document is new or phone number has been modified 91 | // and if the field is required. If not required, then only run it if the field has length. 92 | if ((isRequired || hasPhoneNumber) && (this.isNew || this.isDirectModified(phoneNumberField))) { 93 | try { 94 | const phoneNumber = this.get(phoneNumberField); 95 | const intlPhoneNumber = new IntlPhoneNumber(phoneNumber); 96 | if (intlPhoneNumber.isValid) { 97 | this.set(phoneNumberField, intlPhoneNumber.e164Format); 98 | this.set(nationalFormatField, intlPhoneNumber.nationalFormat); 99 | this.set(internationalFormatField, intlPhoneNumber.internationalFormat); 100 | this.set(countryCodeField, intlPhoneNumber.countryCode); 101 | next(); 102 | 103 | } else { 104 | next(new Error(intlPhoneNumber.errorMsg)); 105 | } 106 | 107 | } catch (e) { 108 | next(e); 109 | } 110 | } else { 111 | next(); 112 | } 113 | }); 114 | 115 | } 116 | 117 | exports.mongooseIntlPhoneNumber = mongooseIntlPhoneNumber; 118 | -------------------------------------------------------------------------------- /lib/mongoose-intl-phone-number.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose'); 4 | const expect = require('chai').expect; 5 | const mongooseIntlPhoneNumber = require('./mongoose-intl-phone-number').mongooseIntlPhoneNumber; 6 | 7 | const Schema = mongoose.Schema; 8 | let connection; 9 | 10 | mongoose.Promise = global.Promise; 11 | 12 | function customerSchema() { 13 | return new Schema({ 14 | firstName: { type: String }, 15 | lastName: { type: String }, 16 | customerType: { type: String }, 17 | phoneNumber: { type: String }, 18 | countryCode: { type: String }, 19 | nationalFormat: { type: String }, 20 | internationalFormat: { type: String }, 21 | email: { type: String } 22 | }); 23 | } 24 | 25 | function customerSubocOverrideSchema() { 26 | return new Schema({ 27 | firstName: { type: String }, 28 | lastName: { type: String }, 29 | customerType: { type: String }, 30 | contact: { 31 | phoneNumber: { type: String }, 32 | email: { type: String } 33 | } 34 | }); 35 | } 36 | 37 | describe('Mongoose plugin: mongoose-intl-phone-number', function() { 38 | before((done) => { 39 | connection = mongoose.createConnection(process.env.MONGO_URL || 'mongodb://localhost/unit_test'); 40 | connection.once('connected', done); 41 | }); 42 | 43 | after((done) => { 44 | connection.db.dropDatabase(() => { 45 | connection.close(done); 46 | }); 47 | }); 48 | 49 | describe('with default settings', function() { 50 | let testSchema; 51 | let Customer; 52 | 53 | before(function() { 54 | testSchema = customerSchema(); 55 | testSchema.plugin(mongooseIntlPhoneNumber); 56 | Customer = connection.model('Customer', testSchema); 57 | }); 58 | 59 | it('should parse the phone number and store the data to their default fields', function() { 60 | const customer = new Customer({ 61 | firstName: 'test', 62 | lastName: 'customer', 63 | customerType: 'testing', 64 | phoneNumber: '+18888675309', 65 | email: 'test@testing.com' 66 | }); 67 | 68 | return customer.save(function(customer) { 69 | expect(customer.phoneNumber).to.equal('+18888675309'); 70 | expect(customer.nationalFormat).to.equal('(888) 867-5309'); 71 | expect(customer.internationalFormat).to.equal('+1 888-867-5309'); 72 | expect(customer.countryCode).to.equal('US'); 73 | }); 74 | }); 75 | 76 | it('should not throw an error if the number was incorrect and not directly modified', function() { 77 | const customer = new Customer({ 78 | firstName: 'test', 79 | lastName: 'customer', 80 | customerType: 'testing', 81 | phoneNumber: '+18888675309', 82 | email: 'test@testing.com' 83 | }); 84 | 85 | return customer.save().then((customer) => { 86 | return Customer.findOneAndUpdate({ 87 | _id: customer._id 88 | }, { 89 | $set: { 90 | phoneNumber: '+188886753099' 91 | } 92 | }, { 93 | new: true 94 | }).exec(); 95 | }).then((customer) => { 96 | customer.firstName = 'testing'; 97 | return customer.save(); 98 | }).then((customer) => { 99 | expect(customer.phoneNumber).to.equal('+188886753099'); 100 | expect(customer.firstName).to.equal('testing'); 101 | }); 102 | }); 103 | 104 | it('should throw an error if the number was directly modified and incorrect', function() { 105 | const customer = new Customer({ 106 | firstName: 'test', 107 | lastName: 'customer', 108 | customerType: 'testing', 109 | phoneNumber: '+18888675309', 110 | email: 'test@testing.com' 111 | }); 112 | 113 | return customer.save().then((customer) => { 114 | customer.phoneNumber = '+188886753099'; 115 | return customer.save(); 116 | }).then((customer) => { 117 | throw new Error('No error was thrown'); 118 | }).catch((err) => { 119 | expect(err.message).to.equal('Phone number is not valid. Number is too long.'); 120 | }); 121 | }); 122 | 123 | it('should throw an error if the number is too long', function() { 124 | const customer = new Customer({ 125 | firstName: 'test', 126 | lastName: 'customer', 127 | customerType: 'testing', 128 | phoneNumber: '+188886753099', 129 | email: 'test@testing.com' 130 | }); 131 | 132 | return customer.save().then((customer) => { 133 | throw new Error('no error was thrown'); 134 | }).catch((err) => { 135 | expect(err.message).to.equal('Phone number is not valid. Number is too long.'); 136 | }); 137 | }); 138 | 139 | it('should throw an error if the number is too short', function() { 140 | const customer = new Customer({ 141 | firstName: 'test', 142 | lastName: 'customer', 143 | customerType: 'testing', 144 | phoneNumber: '+1888867', 145 | email: 'test@testing.com' 146 | }); 147 | 148 | return customer.save().then((customer) => { 149 | throw new Error('no error was thrown'); 150 | }).catch((err) => { 151 | expect(err.message).to.equal('Phone number is not valid. Number is too short.'); 152 | }); 153 | }); 154 | 155 | it('should throw an error if the number does not have a valid country code', function() { 156 | const customer = new Customer({ 157 | firstName: 'test', 158 | lastName: 'customer', 159 | customerType: 'testing', 160 | phoneNumber: '8888675309', 161 | email: 'test@testing.com' 162 | }); 163 | 164 | return customer.save().then((customer) => { 165 | throw new Error('no error was thrown'); 166 | }).catch((err) => { 167 | expect(err.message).to.equal('Invalid country calling code'); 168 | }); 169 | }); 170 | 171 | it('should throw an error if the number is unknown', function() { 172 | const customer = new Customer({ 173 | firstName: 'test', 174 | lastName: 'customer', 175 | customerType: 'testing', 176 | phoneNumber: '+19999999999', 177 | email: 'test@testing.com' 178 | }); 179 | 180 | return customer.save().then((customer) => { 181 | throw new Error('no error was thrown'); 182 | }).catch((err) => { 183 | expect(err.message).to.equal('Phone number is not valid. Number is unknown.'); 184 | }); 185 | }); 186 | 187 | it('should throw an error if the number is an unknown local number', function() { 188 | const customer = new Customer({ 189 | firstName: 'test', 190 | lastName: 'customer', 191 | customerType: 'testing', 192 | phoneNumber: '+1 2530000', 193 | email: 'test@testing.com' 194 | }); 195 | 196 | return customer.save().then((customer) => { 197 | throw new Error('no error was thrown'); 198 | }).catch((err) => { 199 | expect(err.message).to.equal('Phone number is not valid. Number is an unknown local number.'); 200 | }); 201 | }); 202 | }); 203 | 204 | describe('with default overrides', function() { 205 | let testSchema; 206 | let CustomerOverrides; 207 | 208 | before(function() { 209 | testSchema = customerSchema(); 210 | testSchema.plugin(mongooseIntlPhoneNumber, { 211 | hook: 'save', 212 | phoneNumberField: 'phoneNumber', 213 | nationalFormatField: 'ntlFormat', 214 | internationalFormatField: 'intlFormat', 215 | countryCodeField: 'ccode', 216 | }); 217 | CustomerOverrides = connection.model('CustomerOverrides', testSchema); 218 | }); 219 | 220 | it('should parse the phone number and store the data to the specified fields', function() { 221 | const customer = new CustomerOverrides({ 222 | firstName: 'test', 223 | lastName: 'customer', 224 | customerType: 'testing', 225 | phoneNumber: '+18888675309', 226 | email: 'test@testing.com' 227 | }); 228 | 229 | return customer.save().then(() => { 230 | expect(customer.phoneNumber).to.equal('+18888675309'); 231 | expect(customer.ntlFormat).to.equal('(888) 867-5309'); 232 | expect(customer.intlFormat).to.equal('+1 888-867-5309'); 233 | expect(customer.ccode).to.equal('US'); 234 | }); 235 | }); 236 | }); 237 | 238 | describe('with subdoc and default overrides', function() { 239 | let testSchema; 240 | let CustomerSubdocOverrides; 241 | 242 | before(function() { 243 | testSchema = customerSubocOverrideSchema(); 244 | testSchema.plugin(mongooseIntlPhoneNumber, { 245 | hook: 'save', 246 | phoneNumberField: 'contact.phoneNumber', 247 | nationalFormatField: 'contact.nationalFormat', 248 | internationalFormatField: 'contact.internationalFormat', 249 | countryCodeField: 'contact.countryCode', 250 | }); 251 | CustomerSubdocOverrides = connection.model('CustomerSubdocOverrides', testSchema); 252 | }); 253 | 254 | it('should parse the phone number and store the data to the specified fields', function() { 255 | const customer = new CustomerSubdocOverrides({ 256 | firstName: 'test', 257 | lastName: 'customer', 258 | customerType: 'testing', 259 | contact: { 260 | phoneNumber: '+18888675309', 261 | email: 'test@testing.com' 262 | } 263 | }); 264 | 265 | return customer.save().then(() => { 266 | expect(customer.contact.phoneNumber).to.equal('+18888675309'); 267 | expect(customer.contact.nationalFormat).to.equal('(888) 867-5309'); 268 | expect(customer.contact.internationalFormat).to.equal('+1 888-867-5309'); 269 | expect(customer.contact.countryCode).to.equal('US'); 270 | }); 271 | }); 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-intl-phone-number", 3 | "version": "1.0.2", 4 | "description": "A configurable mongoose.js plugin that parses phone numbers and stores data about them in the document.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "gulp test", 8 | "coveralls": "gulp coveralls", 9 | "travis": "npm run coveralls -s && ((cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js) || exit 0)", 10 | "docs": "gulp docs", 11 | "prepublish": "npm run docs", 12 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/Dashride/mongoose-intl-phone-number" 17 | }, 18 | "keywords": [ 19 | "libphonenumber", 20 | "twilio", 21 | "phone", 22 | "lookup" 23 | ], 24 | "author": "Joseph Thibeault ", 25 | "license": "Apache-2.0", 26 | "bugs": { 27 | "url": "https://github.com/Dashride/mongoose-intl-phone-number/issues" 28 | }, 29 | "homepage": "https://github.com/Dashride/mongoose-intl-phone-number", 30 | "devDependencies": { 31 | "coveralls": "^3.0.0", 32 | "chai": "^4.1.2", 33 | "gulp": "^3.9.0", 34 | "gulp-concat": "^2.6.0", 35 | "gulp-istanbul": "^1.0.0", 36 | "gulp-jsdoc-to-markdown": "^1.1.1", 37 | "gulp-jshint": "^2.0.0", 38 | "gulp-mocha": "^3.0.1", 39 | "gulp-util": "^3.0.6", 40 | "isparta": "^4.0.0", 41 | "jshint": "^2.9.3", 42 | "jshint-stylish": "^2.0.1", 43 | "mocha": "^4.0.1", 44 | "mongoose": "^4.8.1", 45 | "semantic-release": "^8.2.0" 46 | }, 47 | "dependencies": { 48 | "google-libphonenumber": "^3.2.32", 49 | "lodash": "^4.17.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /readme.hbs: -------------------------------------------------------------------------------- 1 | mongoose-intl-phone-number 2 | ==================== 3 | [![Build Status](https://travis-ci.org/Dashride/mongoose-intl-phone-number.svg?branch=master)](https://travis-ci.org/Dashride/mongoose-intl-phone-number) 4 | [![Coverage Status](https://coveralls.io/repos/Dashride/mongoose-intl-phone-number/badge.svg?branch=master&service=github)](https://coveralls.io/github/Dashride/mongoose-intl-phone-number?branch=master) 5 | [![Dependency Status](https://david-dm.org/Dashride/mongoose-intl-phone-number.svg)](https://david-dm.org/Dashride/mongoose-intl-phone-number) 6 | [![npm version](https://badge.fury.io/js/mongoose-intl-phone-number.svg)](http://badge.fury.io/js/mongoose-intl-phone-number) 7 | 8 | This module takes a string of numbers and determines their validity as well as returns data about the phone numbers. This module is based on Google's [libphonenumber](https://github.com/mattbornski/libphonenumber). 9 | 10 | ## How it works 11 | A phone number is provided on the document, during the pre-save/validate hook (you can specify), it runs the phone number through [libphonenumber](https://github.com/mattbornski/libphonenumber) and stores the data returned onto fields in the document model. 12 | 13 | ## Use Case 14 | Applications that accept international phone numbers should use this plugin to gather and store information about the number such as country code, national format, etc. 15 | 16 | ## Installation 17 | 18 | `npm install --save mongoose-intl-phone-number` 19 | 20 | ## API Reference 21 | {{#module name="mongooseIntlPhoneNumber"}} 22 | {{>docs}} 23 | {{/module}} 24 | --------------------------------------------------------------------------------