├── .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 | [](https://travis-ci.org/Dashride/mongoose-intl-phone-number)
4 | [](https://coveralls.io/github/Dashride/mongoose-intl-phone-number?branch=master)
5 | [](https://david-dm.org/Dashride/mongoose-intl-phone-number)
6 | [](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 | [](https://travis-ci.org/Dashride/mongoose-intl-phone-number)
4 | [](https://coveralls.io/github/Dashride/mongoose-intl-phone-number?branch=master)
5 | [](https://david-dm.org/Dashride/mongoose-intl-phone-number)
6 | [](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 |
--------------------------------------------------------------------------------