├── .circleci
└── config.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── index.js
├── package.json
├── src
├── defaults.js
├── get.js
├── helpers
│ ├── getOwnProperty.js
│ ├── isPOJO.js
│ └── matchType.js
├── index.js
├── path.js
├── required.js
├── specialProperties.js
├── symbols.js
├── to.js
├── type.js
├── unmarshal
│ ├── error.js
│ ├── index.js
│ └── util.js
└── util.js
└── test
├── defaults.test.js
├── helpers.test.js
├── to.test.js
├── unit.test.js
└── util.test.js
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | setup_environment: &setup_environment
2 | docker:
3 | - image: circleci/node:6.11.1
4 | environment:
5 | TZ: "/usr/share/zoneinfo/America/Los_Angeles"
6 | working_directory: ~/project
7 |
8 | version: 2
9 | jobs:
10 | install:
11 | <<: *setup_environment
12 | steps:
13 | - checkout
14 | - restore_cache:
15 | key: dependency-cache-{{ checksum "package.json" }}
16 | - run:
17 | name: Install dependencies
18 | command: npm install
19 | - save_cache:
20 | key: dependency-cache-{{ checksum "package.json" }}
21 | paths:
22 | - node_modules
23 | - persist_to_workspace:
24 | root: ~/project
25 | paths:
26 | - node_modules
27 |
28 | test:
29 | <<: *setup_environment
30 | steps:
31 | - checkout
32 | - attach_workspace:
33 | at: ~/project
34 | - run:
35 | name: Run tests
36 | command: npm i && npm test
37 |
38 | workflows:
39 | version: 2
40 | install_test:
41 | jobs:
42 | - install
43 | - test:
44 | requires:
45 | - install
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | package-lock.json
36 |
37 | #IDES
38 | .idea
39 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | CHANGELOG.md
2 | .circleci/
3 | package-lock.json
4 | test/
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## [0.11.3](https://github.com/boosterfuels/archetype/compare/v0.11.2...v0.11.3) (2020-05-18)
3 |
4 |
5 | ### Bug Fixes
6 |
7 | * **import:** Remove unused import of index.js in src/unmarshal/index.js ([71eac99](https://github.com/boosterfuels/archetype/commit/71eac99))
8 |
9 |
10 |
11 |
12 | ## [0.11.1](https://github.com/boosterfuels/archetype/compare/v0.11.0...v0.11.1) (2020-02-27)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * throw non-empty object $default error if passing in a date or other non-POJO ([76e7bfa](https://github.com/boosterfuels/archetype/commit/76e7bfa))
18 |
19 |
20 |
21 |
22 | # [0.11.0](https://github.com/boosterfuels/archetype/compare/v0.10.2...v0.11.0) (2020-02-27)
23 |
24 |
25 | ### Features
26 |
27 | * make `new Type()` clone by default, but allow passing in an option to disable cloning for perf ([3638b57](https://github.com/boosterfuels/archetype/commit/3638b57))
28 |
29 |
30 |
31 |
32 | ## [0.10.2](https://github.com/boosterfuels/archetype/compare/v0.10.0...v0.10.2) (2020-02-25)
33 |
34 |
35 | ### Bug Fixes
36 |
37 | * clone empty arrays / objects if set as `$default`, throw if not an empty object ([312aa14](https://github.com/boosterfuels/archetype/commit/312aa14))
38 |
39 |
40 |
41 |
42 | ## [0.10.1](https://github.com/boosterfuels/archetype/compare/v0.10.0...v0.10.1) (2020-02-25)
43 |
44 |
45 | ### Bug Fixes
46 |
47 | * clone empty arrays / objects if set as `$default`, throw if not an empty object ([312aa14](https://github.com/boosterfuels/archetype/commit/312aa14))
48 |
49 |
50 |
51 |
52 | # [0.10.0](https://github.com/boosterfuels/archetype/compare/v0.9.1...v0.10.0) (2019-08-09)
53 |
54 |
55 | ### Performance Improvements
56 |
57 | * remove `cloneDeep()` to reduce memory usage for huge objects ([117c083](https://github.com/boosterfuels/archetype/commit/117c083))
58 |
59 |
60 |
61 |
62 | ## [0.9.1](https://github.com/boosterfuels/archetype/compare/v0.9.0...v0.9.1) (2019-06-10)
63 |
64 |
65 | ### Bug Fixes
66 |
67 | * export core Type class ([3e1bdf2](https://github.com/boosterfuels/archetype/commit/3e1bdf2))
68 |
69 |
70 |
71 |
72 | # [0.9.0](https://github.com/boosterfuels/archetype/compare/v0.8.8...v0.9.0) (2019-02-12)
73 |
74 |
75 | ### Bug Fixes
76 |
77 | * ignore special properties ([7b99d33](https://github.com/boosterfuels/archetype/commit/7b99d33))
78 | * **to:** disallow casting POJOs to numbers ([5548fc2](https://github.com/boosterfuels/archetype/commit/5548fc2))
79 |
80 |
81 | ### Features
82 |
83 | * make `compile()` return a full ES6 class for improved inheritance ([d3e873e](https://github.com/boosterfuels/archetype/commit/d3e873e)), closes [#9](https://github.com/boosterfuels/archetype/issues/9)
84 |
85 |
86 |
87 |
88 | ## [0.8.8](https://github.com/boosterfuels/archetype/compare/v0.8.7...v0.8.8) (2019-01-30)
89 |
90 |
91 | ### Bug Fixes
92 |
93 | * remove leftover reference to lodash ([34c6519](https://github.com/boosterfuels/archetype/commit/34c6519))
94 |
95 |
96 |
97 |
98 | ## [0.8.7](https://github.com/boosterfuels/archetype/compare/v0.8.6...v0.8.7) (2019-01-29)
99 |
100 |
101 | ### Bug Fixes
102 |
103 | * clean up unnecessary _.each() usage ([ab32c15](https://github.com/boosterfuels/archetype/commit/ab32c15))
104 |
105 |
106 |
107 |
108 | ## [0.8.6](https://github.com/boosterfuels/archetype/compare/v0.8.5...v0.8.6) (2019-01-28)
109 |
110 |
111 |
112 |
113 | ## [0.8.4](https://github.com/boosterfuels/archetype/compare/v0.8.3...v0.8.4) (2019-01-27)
114 |
115 |
116 | ### Bug Fixes
117 |
118 | * **defaults:** handle nested defaults correctly ([c02b89b](https://github.com/boosterfuels/archetype/commit/c02b89b)), closes [#16](https://github.com/boosterfuels/archetype/issues/16)
119 | * **to:** dont convert null -> undefined when casting strings ([92faef2](https://github.com/boosterfuels/archetype/commit/92faef2))
120 |
121 |
122 |
123 |
124 | ## [0.8.3](https://github.com/boosterfuels/archetype/compare/v0.8.2...v0.8.3) (2018-03-21)
125 |
126 |
127 | ### Features
128 |
129 | * support inPlace updates for recursive schemas ([8f61d32](https://github.com/boosterfuels/archetype/commit/8f61d32)), closes [#11](https://github.com/boosterfuels/archetype/issues/11)
130 |
131 |
132 |
133 |
134 | ## [0.8.2](https://github.com/boosterfuels/archetype/compare/v0.8.1...v0.8.2) (2017-12-03)
135 |
136 |
137 | ### Bug Fixes
138 |
139 | * **unmarshal:** don't use $transform on array if nested in child element ([78bc272](https://github.com/boosterfuels/archetype/commit/78bc272)), closes [#13](https://github.com/boosterfuels/archetype/issues/13)
140 |
141 |
142 |
143 |
144 | ## [0.8.1](https://github.com/boosterfuels/archetype/compare/v0.8.0...v0.8.1) (2017-10-19)
145 |
146 |
147 | ### Bug Fixes
148 |
149 | * handle passing array of paths to `Type.omit()` ([0cfc96f](https://github.com/boosterfuels/archetype/commit/0cfc96f))
150 |
151 |
152 |
153 |
154 | # [0.8.0](https://github.com/boosterfuels/archetype/compare/v0.7.0...v0.8.0) (2017-09-21)
155 |
156 |
157 | ### Features
158 |
159 | * export CastError type ([1c3a971](https://github.com/boosterfuels/archetype/commit/1c3a971)), closes [#7](https://github.com/boosterfuels/archetype/issues/7)
160 | * **helpers:** add matchType ([7f5e1e8](https://github.com/boosterfuels/archetype/commit/7f5e1e8))
161 | * **unmarshal:** add rudimentary support for $transform ([8bdf8db](https://github.com/boosterfuels/archetype/commit/8bdf8db))
162 | * **unmarshal:** report errors from $transform ([a8c9e3d](https://github.com/boosterfuels/archetype/commit/a8c9e3d))
163 | * **unmarshal:** support $transform in arrays ([2d11cf5](https://github.com/boosterfuels/archetype/commit/2d11cf5))
164 |
165 |
166 |
167 |
168 | ## [0.6.1](https://github.com/vkarpov15/archetype-js/compare/v0.6.0...v0.6.1) (2017-03-17)
169 |
170 |
171 | ### Bug Fixes
172 |
173 | * **unmarshal:** handle $type: Array with non-arrays ([f65c063](https://github.com/vkarpov15/archetype-js/commit/f65c063))
174 |
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # archetype
2 |
3 |
4 |
5 | Archetype is a library for casting and validating objects. It has exceptional support for deeply nested objects, type composition, custom types, and geoJSON.
6 |
7 | [](https://circleci.com/gh/boosterfuels/archetype)
8 |
9 | ```javascript
10 | const { ObjectId } = require('mongodb');
11 | const moment = require('moment');
12 |
13 | // `Person` is now a constructor
14 | const Person = new Archetype({
15 | name: 'string',
16 | bandId: {
17 | $type: ObjectId,
18 | $required: true
19 | },
20 | createdAt: {
21 | $type: moment,
22 | $default: () => moment()
23 | }
24 | }).compile('Person');
25 |
26 | const test = new Person({
27 | name: 'test',
28 | bandId: '507f191e810c19729de860ea'
29 | });
30 |
31 | test.bandId; // Now a mongodb ObjectId
32 | test.createdAt; // moment representing now
33 | ```
34 |
35 | If casting fails, archetype throws a nice clean exception:
36 |
37 | ```javascript
38 | try {
39 | new Person({
40 | name: 'test',
41 | bandId: 'ImNotAValidObjectId'
42 | });
43 | } catch(error) {
44 | error.errors['bandId'].message; // Mongodb ObjectId error
45 | }
46 | ```
47 |
48 | [Archetypes are composable, inspectable, and extendable via `extends`.](http://thecodebarbarian.com/casting-mongodb-queries-with-archetype.html)
49 |
50 | ## Connect
51 |
52 | Follow [archetype on Twitter](https://twitter.com/archetype_js) for updates, including our gists of the week. Here's some older gists of the week:
53 |
54 | * 20180112: [Virtual Types with Archetype](https://gist.github.com/vkarpov15/32abd9f72dfb2ba1558e9e6a1060c768)
55 | * 20180105: [Transform an Archetype So All Properties are Not Required](https://gist.github.com/vkarpov15/6c49c0bc5ed0eab42633645bf03e8fa7)
56 | * 20171124: [Embedding Only Certain Fields in an Embedded Archetype](https://gist.github.com/vkarpov15/37622608b33eb144acfda5d3ad936be6)
57 | * 20171117: [Async/Await with Archetype](https://gist.github.com/vkarpov15/f10b560468f49166bcc14c6e41bb755e)
58 | * 20171110: [Configuring Custom Validators for Individual Paths in Archetype](https://gist.github.com/vkarpov15/dcf7c490c69e625764c0dc1453555524)
59 | * 20171103: [Conditionally Required Properties with Archetype](https://gist.github.com/vkarpov15/b4a9cb225699b3bf852c3fa8ca2c56e2)
60 | * 20171027: [Custom Types with Archetype](https://gist.github.com/vkarpov15/d4cbd7941b40346741cf791d379001e5)
61 | * 20171020: [Embedding a Subset of One Archetype's Properties in another Archetype](https://gist.github.com/vkarpov15/0dc21e98acfb96e72d0bb9b602adb3ad)
62 | * 20171013: [Embedded Objects vs Embedded Types in Archetype](https://gist.github.com/vkarpov15/1520cfb604972e81198db028e4606809)
63 | * 20171006: [Consistent Arrays from Query Params with Express and Archetype](https://gist.github.com/vkarpov15/e03dafb2ac478cb38ff3fbe4c36139d6)
64 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src').Archetype;
2 |
3 | module.exports.Any = require('./src/symbols').Any;
4 | module.exports.CastError = require('./src').CastError;
5 | module.exports.Type = require('./src/type');
6 | module.exports.matchType = require('./src/helpers/matchType');
7 | module.exports.to = require('./src/to');
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "archetype",
3 | "version": "0.13.1",
4 | "author": "Valeri Karpov ",
5 | "dependencies": {
6 | "lodash": "4.x",
7 | "mpath": "0.8.x"
8 | },
9 | "devDependencies": {
10 | "acquit": "1.x",
11 | "acquit-ignore": "0.1.x",
12 | "acquit-markdown": "0.1.x",
13 | "co": "4.6.0",
14 | "mocha": "5.x",
15 | "mongodb": "3.1.13",
16 | "standard-changelog": "1.0.1"
17 | },
18 | "engines": {
19 | "node": ">= 4.0.0"
20 | },
21 | "license": "Apache 2.0",
22 | "main": "./index.js",
23 | "repository": {
24 | "type": "git",
25 | "url": "git://github.com/boosterfuels/archetype.git"
26 | },
27 | "scripts": {
28 | "changelog": "standard-changelog -i CHANGELOG.md -w",
29 | "docs": "node docs.js",
30 | "test": "mocha test/*.test.js"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/defaults.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = applyDefaults;
4 |
5 | const get = require('./get');
6 | const getOwnProperty = require('./helpers/getOwnProperty');
7 | const join = require('./unmarshal/util').join;
8 | const realPathToSchemaPath = require('./unmarshal/util').realPathToSchemaPath;
9 | const shouldSkipPath = require('./util').shouldSkipPath;
10 | const util = require('util');
11 |
12 | function applyDefaults(obj, schema, projection) {
13 | const keys = Object.keys(schema._obj);
14 | for (const key of keys) {
15 | const def = defaults(obj, obj[key], schema, key, projection);
16 | if (def !== void 0 && obj[key] == null) {
17 | obj[key] = def;
18 | }
19 | }
20 | }
21 |
22 | function defaults(root, v, schema, path, projection) {
23 | if (shouldSkipPath(projection, path) || projection.$noDefaults) {
24 | return;
25 | }
26 |
27 | const fakePath = realPathToSchemaPath(path);
28 | const schemaPath = schema._paths[fakePath];
29 |
30 | if (!schemaPath) {
31 | return;
32 | }
33 |
34 | if ('$default' in schemaPath) {
35 | return handleDefault(schemaPath.$default, root, path);
36 | }
37 |
38 | if (schemaPath.$type === Object && getOwnProperty(schemaPath, '$schema') != null) {
39 | for (const key of Object.keys(schemaPath.$schema)) {
40 | const fullPath = join(fakePath, key);
41 | const value = get(v, key);
42 | // Might have nested defaults even if this level isn't nullish
43 | const def = defaults(root, value, schema, fullPath, projection);
44 | if (def !== void 0 && value == null) {
45 | if (v == null) {
46 | v = {};
47 | }
48 | v[key] = def;
49 | }
50 | }
51 |
52 | return v;
53 | }
54 | if (schemaPath.$type === Array) {
55 | const arr = v || [];
56 | for (let index = 0; index < arr.length; ++index) {
57 | const value = v[index];
58 | const def = defaults(root, value, schema,
59 | join(fakePath, index.toString()), projection);
60 | if (def !== void 0 && value == null) {
61 | v[index] = def;
62 | }
63 | }
64 |
65 | return v;
66 | }
67 | }
68 |
69 | function handleDefault(obj, ctx, path) {
70 | if (typeof obj === 'function') {
71 | return obj(ctx);
72 | }
73 | // If default is an object type, be very careful -
74 | // returning an object default would be by reference,
75 | // which means user data could modify the default.
76 | if (typeof obj === 'object' && obj != null) {
77 | if (Array.isArray(obj) && obj.length === 0) {
78 | return [];
79 | } else if (!Array.isArray(obj) &&
80 | Object.keys(obj).length === 0 &&
81 | [void 0, Object].indexOf(obj.constructor) !== -1) {
82 | return {};
83 | }
84 | throw new Error('Default at path `' + path + '` is a non-empty ' +
85 | 'object `' + util.inspect(obj) + '`. Please make `$default` ' +
86 | 'a function that returns an object instead');
87 | }
88 | return obj;
89 | }
90 |
--------------------------------------------------------------------------------
/src/get.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /*!
4 | * Simplified lodash.get to work around the annoying null quirk. See:
5 | * https://github.com/lodash/lodash/issues/3659
6 | */
7 |
8 | module.exports = function get(obj, path, def) {
9 | const parts = path.split('.');
10 | let rest = path;
11 | let cur = obj;
12 | for (const part of parts) {
13 | if (cur == null) {
14 | return def;
15 | }
16 |
17 | cur = getProperty(cur, part);
18 |
19 | rest = rest.substr(part.length + 1);
20 | }
21 |
22 | return cur == null ? def : cur;
23 | };
24 |
25 | function getProperty(obj, prop) {
26 | if (obj == null) {
27 | return obj;
28 | }
29 | if (obj instanceof Map) {
30 | return obj.get(prop);
31 | }
32 | return obj[prop];
33 | }
34 |
--------------------------------------------------------------------------------
/src/helpers/getOwnProperty.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function getOwnProperty(obj, prop) {
4 | if (obj == null) {
5 | return null;
6 | }
7 | if (typeof obj !== 'object') {
8 | return null;
9 | }
10 | if (!obj.hasOwnProperty(prop)) {
11 | return null;
12 | }
13 | return obj[prop];
14 | };
--------------------------------------------------------------------------------
/src/helpers/isPOJO.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function isPOJO(arg) {
4 | if (arg == null || typeof arg !== 'object') {
5 | return false;
6 | }
7 | const proto = Object.getPrototypeOf(arg);
8 | // Prototype may be null if you used `Object.create(null)`
9 | // Checking `proto`'s constructor is safe because `getPrototypeOf()`
10 | // explicitly crosses the boundary from object data to object metadata
11 | return !proto || proto.constructor.name === 'Object';
12 | };
--------------------------------------------------------------------------------
/src/helpers/matchType.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Given a mapping from types ('object', 'string', 'boolean', 'undefined',
5 | * 'number', 'symbol', or 'function') to functions, returns a function that
6 | * takes a `value` and replaces it with the return value from the function
7 | * mapped to by `typeof value`.
8 | *
9 | * Example: `matchType({ 'string': JSON.parse })` will return a function that
10 | * takes a value and only calls `JSON.parse()` if that value is a string.
11 | *
12 | * @param {Object} obj Maps from `typeof` value to function
13 | * @return {Function} transforms value based on `typeof`
14 | */
15 |
16 | module.exports = function(obj) {
17 | return function(v) {
18 | const type = typeof v;
19 | if (typeof obj[type] === 'function') {
20 | return obj[type](v);
21 | }
22 | return v;
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Path = require('./path');
4 | const Type = require('./type');
5 | const { cloneDeep } = require('lodash');
6 | const get = require('./get');
7 | const { set } = require('lodash');
8 | const specialProperties = require('./specialProperties');
9 | const unmarshal = require('./unmarshal');
10 |
11 | class Archetype {
12 | constructor(obj) {
13 | this._obj = cloneDeep(obj);
14 | this._paths = {};
15 | }
16 |
17 | compile(name) {
18 | const _this = this;
19 | this._paths = visitor(this._obj);
20 |
21 | class _Type extends Type {
22 | static paths() {
23 | return _this.paths();
24 | }
25 |
26 | static path(path, props, opts) {
27 | return _this.path(path, props, opts);
28 | }
29 |
30 | static omit(paths) {
31 | return _this.omit(paths);
32 | }
33 |
34 | static pick(paths) {
35 | return _this.pick(paths);
36 | }
37 |
38 | static transform(fn) {
39 | return _this.transform(fn);
40 | }
41 |
42 | static eachPath(fn) {
43 | return _this.eachPath(fn);
44 | }
45 | }
46 |
47 | _Type.schema = this;
48 | if (name != null) {
49 | _Type.toString = () => name;
50 | Object.defineProperty(_Type, 'name', { value: name });
51 | }
52 |
53 | return _Type;
54 | }
55 |
56 | json() {
57 | return this._obj;
58 | }
59 |
60 | path(path, props, options) {
61 | if (!props) {
62 | return get(this._obj, path);
63 | }
64 | if (get(options, 'inPlace')) {
65 | set(this._obj, path, props);
66 | return this;
67 | }
68 | const newSchema = new Archetype(this._obj);
69 | set(newSchema._obj, path, props);
70 | return newSchema;
71 | }
72 |
73 | omit(paths) {
74 | const newSchema = new Archetype(this._obj);
75 | if (Array.isArray(paths)) {
76 | for (const path of paths) {
77 | unset(newSchema._obj, path);
78 | }
79 | } else {
80 | unset(newSchema._obj, paths);
81 | }
82 | return newSchema;
83 | }
84 |
85 | pick(paths) {
86 | const obj = {};
87 | paths = Array.isArray(paths) ? paths : [paths];
88 | for (const path of paths) {
89 | obj[path] = this._obj[path];
90 | }
91 | const newSchema = new Archetype(obj);
92 | return newSchema;
93 | }
94 |
95 | transform(fn) {
96 | const newSchema = new Archetype(this._obj);
97 | newSchema._transform(fn, newSchema._obj, []);
98 | return newSchema;
99 | }
100 |
101 | _transform(fn, obj, path) {
102 | Object.keys(obj).forEach(key => {
103 | obj[key] = fn(path.concat([key]).join('.'), obj[key]);
104 | if (typeof obj[key] === 'object' && obj[key] && !('$type' in obj[key])) {
105 | this._transform(fn, obj[key], path.concat([key]));
106 | }
107 | });
108 | }
109 |
110 | eachPath(fn) {
111 | this._eachPath(fn, this._obj, []);
112 | }
113 |
114 | _eachPath(fn, obj, path) {
115 | Object.keys(obj).forEach(key => {
116 | fn(path.concat([key]).join('.'), obj[key]);
117 | if (typeof obj[key] === 'object' && obj[key] && !('$type' in obj[key])) {
118 | this._eachPath(fn, obj[key], path.concat([key]));
119 | }
120 | });
121 | }
122 |
123 | paths() {
124 | return Object.keys(this._paths).
125 | map(path => Object.assign({}, this._paths[path], { path }));
126 | }
127 |
128 | unmarshal(obj, projection) {
129 | return unmarshal(obj, this, projection);
130 | }
131 | }
132 |
133 | function visitor(obj) {
134 | var paths = paths || {};
135 |
136 | visitObject(obj, '', paths);
137 | return paths;
138 | }
139 |
140 | function visitArray(arr, path, paths) {
141 | paths[path] = new Path({ $type: Array });
142 | if (arr.length > 0) {
143 | if (Array.isArray(arr[0])) {
144 | visitArray(arr[0], path + '.$', paths);
145 | } else if (typeof arr[0] === 'object') {
146 | visitObject(arr[0], path + '.$', paths);
147 | } else {
148 | paths[path + '.$'] = new Path({ $type: arr[0] });
149 | }
150 | } else {
151 | paths[path + '.$'] = new Path({ $type: null });
152 | }
153 | return paths[path];
154 | }
155 |
156 | function visitObject(obj, path, paths) {
157 | if ('$type' in obj) {
158 | if (Array.isArray(obj.$type)) {
159 | visitArray(obj.$type, path, paths);
160 | const withoutType = Object.keys(obj).
161 | filter(key => key !== '$type').
162 | reduce((clone, key) => {
163 | clone[key] = obj[key];
164 | return clone;
165 | }, {});
166 | paths[path] = new Path(Object.assign(paths[path], withoutType));
167 | return;
168 | }
169 |
170 | if (obj.$type == null) {
171 | throw new Error(`Path ${path} has nullish $type. Use Archetype.Any to skip casting`);
172 | }
173 |
174 | paths[path] = new Path(obj);
175 | return;
176 | }
177 |
178 | if (path) {
179 | paths[path] = new Path({ $type: Object, $schema: obj });
180 | }
181 | Object.keys(obj).forEach(function(key) {
182 | const value = obj[key];
183 | if (Array.isArray(value)) {
184 | visitArray(value, join(path, key), paths);
185 | } else if (typeof value === 'object') {
186 | visitObject(value, join(path, key), paths);
187 | } else {
188 | paths[join(path, key)] = new Path({ $type: value });
189 | }
190 | });
191 | }
192 |
193 | function join(path, key) {
194 | if (path) {
195 | return path + '.' + key;
196 | }
197 | return key;
198 | }
199 |
200 | function unset(obj, path) {
201 | const pieces = path.split('.');
202 | let i = 0;
203 | for (i = 0; i < pieces.length - 1; ++i) {
204 | if (specialProperties.has(pieces[i])) {
205 | continue;
206 | }
207 | if (obj == null) {
208 | return;
209 | }
210 | obj = obj[pieces[i]];
211 | }
212 | delete obj[pieces[pieces.length - 1]];
213 | }
214 |
215 | exports.Archetype = Archetype;
216 | exports.CastError = require('./unmarshal/error');
217 |
--------------------------------------------------------------------------------
/src/path.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const util = require('util');
4 |
5 | class Path {
6 | constructor(obj) {
7 | if (obj != null && obj.$default != null && typeof obj.$default === 'object') {
8 | const $default = obj.$default;
9 | const numKeys = Array.isArray($default) ?
10 | $default.length :
11 | Object.keys($default).length;
12 | const isInvalidType = Array.isArray($default) ?
13 | false :
14 | [void 0, Object].indexOf($default.constructor) === -1;
15 | if (numKeys > 0 || isInvalidType) {
16 | throw new Error('Default is a non-empty object `' +
17 | util.inspect($default) + '`. Please make `$default` a function ' +
18 | 'that returns an object instead');
19 | }
20 | }
21 | Object.assign(this, obj);
22 | }
23 | }
24 |
25 | module.exports = Path;
--------------------------------------------------------------------------------
/src/required.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = checkRequired;
4 |
5 | const ValidateError = require('./unmarshal/error');
6 | const get = require('./get');
7 | const join = require('./unmarshal/util').join;
8 | const realPathToSchemaPath = require('./unmarshal/util').realPathToSchemaPath;
9 | const shouldSkipPath = require('./util').shouldSkipPath;
10 |
11 | function check(root, v, schema, path, error, projection) {
12 | if (shouldSkipPath(projection, path) || projection.$noRequired) {
13 | return;
14 | }
15 |
16 | const fakePath = realPathToSchemaPath(path);
17 | const schemaPath = schema._paths[fakePath];
18 | if (isRequired(root, schemaPath) && v == null) {
19 | return error.markError(path, new Error(`Path "${path}" is required`));
20 | }
21 |
22 | if (!path) {
23 | for (const key of Object.keys(schema._obj)) {
24 | check(root, v[key], schema, join(fakePath, key), error, projection);
25 | }
26 | } else if (schemaPath) {
27 | if (schemaPath.$type === Object && schemaPath.$schema) {
28 | for (const key of Object.keys(schemaPath.$schema)) {
29 | check(root, get(v, key), schema, join(fakePath, key), error, projection);
30 | }
31 | } else if (schemaPath.$type === Array) {
32 | const arr = v || [];
33 | for (let index = 0; index < arr.length; ++index) {
34 | check(root, arr[index], schema, join(fakePath, index.toString()),
35 | error, projection);
36 | }
37 | }
38 | }
39 | }
40 |
41 | function isRequired(root, schemaPath) {
42 | if (!schemaPath) {
43 | return false;
44 | }
45 | if (typeof schemaPath.$required === 'function') {
46 | return schemaPath.$required(root);
47 | }
48 | return schemaPath.$required;
49 | }
50 |
51 | function checkRequired(obj, schema, projection) {
52 | const error = new ValidateError();
53 | check(obj, obj, schema, '', error, projection);
54 | return error;
55 | }
56 |
--------------------------------------------------------------------------------
/src/specialProperties.js:
--------------------------------------------------------------------------------
1 | module.exports = new Set(['__proto__', 'constructor', 'prototype']);
2 |
--------------------------------------------------------------------------------
/src/symbols.js:
--------------------------------------------------------------------------------
1 | exports.Any = Symbol.for('Archetype.Any');
--------------------------------------------------------------------------------
/src/to.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Any = require('./symbols').Any;
4 | const inspect = require('util').inspect;
5 | const isPOJO = require('./helpers/isPOJO');
6 |
7 | module.exports = to;
8 |
9 | const CAST_PRIMITIVES = {
10 | number: v => {
11 | // Nasty edge case: Number converts '', ' ', and `{ toString: () => '' }` to 0
12 | if (typeof v !== 'number' && v.toString().trim() === '') {
13 | throw new Error(`Could not cast "${inspect(v)}" to number`);
14 | }
15 |
16 | if (isPOJO(v)) {
17 | throw new Error(`Could not cast "${inspect(v)}" to number`);
18 | }
19 |
20 | const res = Number(v).valueOf();
21 | if (Number.isNaN(res)) {
22 | throw new Error(`Could not cast "${inspect(v)}" to number`);
23 | }
24 | return res;
25 | },
26 | string: v => {
27 | if (isPOJO(v)) {
28 | throw new Error(`Could not cast "${inspect(v)}" to string`);
29 | }
30 | return String(v);
31 | },
32 | boolean: v => {
33 | const str = String(v);
34 | if (str === '1' || str === 'true' || str === 'yes') {
35 | return true;
36 | }
37 | if (str === '0' || str === 'false' || str === 'no') {
38 | return false;
39 | }
40 | throw new Error(`Could not cast "${inspect(v)}" to boolean`);
41 | }
42 | }
43 |
44 | function to(v, type) {
45 | if (v == null) {
46 | return v;
47 | }
48 |
49 | if (type === Any) {
50 | return v;
51 | }
52 |
53 | if (typeof type === 'string') {
54 | if (!CAST_PRIMITIVES[type]) {
55 | throw new Error(`"${type}" is not a valid primitive type`);
56 | }
57 |
58 | if (type === 'number' && Number.isNaN(v)) {
59 | return CAST_PRIMITIVES[type](v);
60 | }
61 | if (typeof v === type) {
62 | return v;
63 | }
64 | return CAST_PRIMITIVES[type](v);
65 | }
66 |
67 | if (!(v instanceof type)) {
68 | return new type(v);
69 | }
70 | return v;
71 | }
72 |
--------------------------------------------------------------------------------
/src/type.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { cloneDeep } = require('lodash');
4 | const unmarshal = require('./unmarshal');
5 |
6 | class Type {
7 | constructor(obj, projection, options) {
8 | options = options || {};
9 | if (options.clone !== false) {
10 | obj = cloneDeep(obj);
11 | }
12 |
13 | Object.assign(this, unmarshal(obj, this.constructor.schema, projection));
14 | }
15 | }
16 |
17 | module.exports = Type;
18 |
--------------------------------------------------------------------------------
/src/unmarshal/error.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class CastError extends Error {
4 | constructor() {
5 | super();
6 | this.errors = {};
7 | this.hasError = false;
8 | this._isArchetypeError = true;
9 | }
10 |
11 | markError(path, error) {
12 | const standardized = new ValidatorError(error.message);
13 | standardized.stack = error.stack;
14 | this.errors[path] = standardized;
15 | this.hasError = true;
16 | this.message = this.toString();
17 | return this;
18 | }
19 |
20 | merge(error) {
21 | if (!error) {
22 | return this;
23 | }
24 | for (const key of Object.keys(error.errors || {})) {
25 | this.errors[key] = error.errors[key];
26 | }
27 | this.hasError = Object.keys(this.errors).length > 0;
28 | this.message = this.toString();
29 | return this;
30 | }
31 |
32 | toString() {
33 | let str = [];
34 | for (const key of Object.keys(this.errors)) {
35 | str.push(`${key}: ${this.errors[key].message || value}`);
36 | }
37 | return str.join(', ');
38 | }
39 | }
40 |
41 | class ValidatorError extends Error {
42 | toJSON() {
43 | return { message: this.message };
44 | }
45 | }
46 |
47 | module.exports = CastError;
48 |
--------------------------------------------------------------------------------
/src/unmarshal/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ValidateError = require('./error');
4 | const applyDefaults = require('../defaults');
5 | const checkRequired = require('../required');
6 | const getOwnProperty = require('../helpers/getOwnProperty');
7 | const handleCast = require('./util').handleCast;
8 | const inspect = require('util').inspect;
9 | const join = require('./util').join;
10 | const mpath = require('mpath');
11 | const realPathToSchemaPath = require('./util').realPathToSchemaPath;
12 | const shouldSkipPath = require('../util').shouldSkipPath;
13 |
14 | module.exports = castDocument;
15 |
16 | function markSubpaths(projection, path, val) {
17 | const pieces = path.split('.');
18 | let cur = pieces[0];
19 | projection[cur] = val;
20 | for (let i = 1; i < pieces.length; ++i) {
21 | cur += `.${pieces[i]}`;
22 | projection[cur] = val;
23 | }
24 | }
25 |
26 | function handleProjection(projection) {
27 | if (!projection) {
28 | return { $inclusive: true };
29 | }
30 | projection.$hasExclusiveChild = {};
31 | let inclusive = null;
32 | for (const key of Object.keys(projection)) {
33 | if (key.startsWith('$')) {
34 | continue;
35 | }
36 | if (projection[key] > 0) {
37 | if (inclusive === true) {
38 | throw new Error("Can't mix inclusive and exclusive in projection");
39 | }
40 | markSubpaths(projection.$hasExclusiveChild, key, projection[key]);
41 | inclusive = false;
42 | } else {
43 | if (inclusive === false) {
44 | throw new Error("Can't mix inclusive and exclusive in projection");
45 | }
46 | inclusive = true;
47 | }
48 | }
49 |
50 | if (inclusive === null) {
51 | inclusive = true;
52 | }
53 | projection.$inclusive = inclusive;
54 |
55 | return projection;
56 | }
57 |
58 | function castDocument(obj, schema, projection) {
59 | projection = handleProjection(projection);
60 | if (obj == null) {
61 | throw new Error(`Can't cast null or undefined`);
62 | }
63 | applyDefaults(obj, schema, projection);
64 | const error = new ValidateError();
65 | error.merge(visitObject(obj, schema, projection, '').error);
66 | error.merge(checkRequired(obj, schema, projection));
67 | error.merge(runValidation(obj, schema, projection));
68 | if (error.hasError) {
69 | throw error;
70 | }
71 | return obj;
72 | }
73 |
74 | function visitArray(arr, schema, projection, path) {
75 | let error = new ValidateError();
76 | let curPath = realPathToSchemaPath(path);
77 | let newPath = join(curPath, '$');
78 |
79 | if (arr == null) {
80 | return {
81 | value: arr,
82 | error: null
83 | };
84 | }
85 |
86 | if (!Array.isArray(arr)) {
87 | arr = [arr];
88 | }
89 |
90 | if (!schema._paths[newPath] || !schema._paths[newPath].$type) {
91 | return {
92 | value: arr,
93 | error: null
94 | };
95 | }
96 |
97 | const pathOptions = schema._paths[newPath];
98 | arr.forEach(function(value, index) {
99 | if (getOwnProperty(pathOptions, '$transform') != null) {
100 | try {
101 | arr[index] = pathOptions.$transform(arr[index]);
102 | } catch (err) {
103 | error.markError(`newPath.${index}`, err);
104 | return;
105 | }
106 | value = arr[index];
107 | }
108 | if (pathOptions.$type === Array ||
109 | Array.isArray(schema._paths[newPath].$type)) {
110 | let res = visitArray(value, schema, projection, join(path, index, true));
111 | if (res.error) {
112 | error.merge(res.error);
113 | }
114 | arr[index] = res.value;
115 | return;
116 | } else if (pathOptions.$type === Object) {
117 | let res = visitObject(value, schema, projection, join(path, index, true));
118 | if (res.error) {
119 | error.merge(res.error);
120 | }
121 | arr[index] = res.value;
122 | return;
123 | }
124 |
125 | try {
126 | handleCast(arr, index, pathOptions.$type);
127 | } catch(err) {
128 | error.markError(join(path, index, true), err);
129 | }
130 | });
131 |
132 | return {
133 | value: arr,
134 | error: (error.hasError ? error : null)
135 | };
136 | }
137 |
138 | function visitObject(obj, schema, projection, path) {
139 | let error = new ValidateError();
140 | if (typeof obj !== 'object' || Array.isArray(obj)) {
141 | let err = new Error('Could not cast ' + require('util').inspect(obj) +
142 | ' to Object');
143 | error.markError(path, err);
144 | return {
145 | value: null,
146 | error: error
147 | };
148 | }
149 |
150 | let fakePath = realPathToSchemaPath(path);
151 | const curSchema = schema._paths[fakePath];
152 | if (fakePath && !curSchema.$schema) {
153 | return {
154 | value: obj,
155 | error: (error.hasError ? error : null)
156 | };
157 | }
158 |
159 | Object.keys(obj).forEach(function(key) {
160 | let value = obj[key];
161 | let newPath = join(fakePath, key);
162 | if (!schema._paths.hasOwnProperty(newPath) || shouldSkipPath(projection, newPath)) {
163 | delete obj[key];
164 | return;
165 | }
166 | const newSchema = schema._paths[newPath];
167 | const pathOptions = schema._paths[newPath];
168 | if (getOwnProperty(pathOptions, '$transform') != null) {
169 | try {
170 | obj[key] = pathOptions.$transform(obj[key]);
171 | } catch (err) {
172 | error.markError(newPath, err);
173 | return;
174 | }
175 | value = obj[key];
176 | }
177 | if (newSchema.$type == null) {
178 | // If type not specified, no type casting
179 | return;
180 | }
181 |
182 | if (pathOptions.$type === Array ||
183 | Array.isArray(schema._paths[newPath].$type)) {
184 | let res = visitArray(value, schema, projection, newPath);
185 | if (res.error) {
186 | error.merge(res.error);
187 | }
188 | obj[key] = res.value;
189 | return;
190 | } else if (pathOptions.$type === Object) {
191 | if (value == null) {
192 | delete obj[key];
193 | return;
194 | }
195 | let res = visitObject(value, schema, projection, newPath);
196 | if (res.error) {
197 | error.merge(res.error);
198 | }
199 | obj[key] = res.value;
200 | return;
201 | }
202 |
203 | try {
204 | handleCast(obj, key, pathOptions.$type, pathOptions.$transform);
205 | } catch(err) {
206 | error.markError(join(path, key, true), err);
207 | }
208 | });
209 |
210 | return {
211 | value: obj,
212 | error: (error.hasError ? error : null)
213 | };
214 | }
215 |
216 | function runValidation(obj, schema, projection) {
217 | const error = new ValidateError();
218 | for (const path of Object.keys(schema._paths)) {
219 | if (shouldSkipPath(projection, path)) {
220 | continue;
221 | }
222 |
223 | const _path = path.replace(/\.\$\./g, '.').replace(/\.\$$/g, '');
224 | const val = mpath.get(_path, obj);
225 | if (!schema._paths[path].$validate && !schema._paths[path].$enum) {
226 | continue;
227 | }
228 |
229 | if (val == null) {
230 | continue;
231 | }
232 |
233 | if (Array.isArray(schema._paths[path].$enum)) {
234 | if (Array.isArray(val)) {
235 | val.forEach((val, index) => {
236 | if (schema._paths[path].$enum.indexOf(val) === -1) {
237 | const msg = `Value "${val}" invalid, allowed values are ` +
238 | `"${inspect(schema._paths[path].$enum)}"`;
239 | error.markError([path, index].join('.'), new Error(msg));
240 | }
241 | });
242 | } else if (schema._paths[path].$enum.indexOf(val) === -1) {
243 | const msg = `Value "${val}" invalid, allowed values are ` +
244 | `"${inspect(schema._paths[path].$enum)}"`;
245 | error.markError(path, new Error(msg));
246 | continue;
247 | }
248 | }
249 |
250 | if (schema._paths[path].$validate) {
251 | if (Array.isArray(val)) {
252 | if (path.indexOf('$') === -1) {
253 | try {
254 | schema._paths[path].$validate(val, schema._paths[path], obj);
255 | } catch(_error) {
256 | error.markError(path, _error);
257 | }
258 | } else {
259 | val.forEach((val, index) => {
260 | try {
261 | schema._paths[path].$validate(val, schema._paths[path], obj);
262 | } catch(_error) {
263 | error.markError(`${path}.${index}`, _error);
264 | }
265 | });
266 | }
267 | } else {
268 | try {
269 | schema._paths[path].$validate(val, schema._paths[path], obj);
270 | } catch(_error) {
271 | error.markError(path, _error);
272 | }
273 | }
274 | }
275 | }
276 | return error;
277 | }
278 |
--------------------------------------------------------------------------------
/src/unmarshal/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const to = require('../to');
4 |
5 | const noop = x => x;
6 |
7 | exports.handleCast = function(obj, key, type, transform) {
8 | transform = transform == null ? noop : transform;
9 | obj[key] = to(transform(obj[key]), type);
10 | };
11 |
12 | exports.realPathToSchemaPath = function(path) {
13 | return path.replace(/\.\d+\./g, '.$.').replace(/\.\d+$/, '.$');
14 | };
15 |
16 | exports.join = function(path, key, real) {
17 | if (!real && typeof key === 'number') {
18 | key = '$';
19 | }
20 | if (path) {
21 | return path + '.' + key;
22 | }
23 | return key;
24 | };
25 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const getOwnProperty = require('./helpers/getOwnProperty');
4 |
5 | exports.shouldSkipPath = function(projection, path) {
6 | if (projection.$inclusive) {
7 | return getOwnProperty(projection, path) != null;
8 | } else {
9 | const parts = path.split('.');
10 | let cur = parts[0];
11 | for (let i = 0; i < parts.length - 1; ++i) {
12 | if (getOwnProperty(projection, cur) != null) {
13 | return false;
14 | }
15 | cur += '.' + parts[i + 1];
16 | }
17 | return projection[path] == null && projection.$hasExclusiveChild[path] == null;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/defaults.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Archetype = require('../');
4 | const assert = require('assert');
5 |
6 | describe('defaults', function() {
7 | it('basic $default', function() {
8 | const Breakfast = new Archetype({
9 | name: { $type: 'string', $required: true, $default: 'bacon' },
10 | names: [{ $type: 'string', $required: true, $default: 'eggs' }],
11 | title: { $type: 'string', $default: 'N/A' }
12 | }).compile();
13 |
14 | const val = new Breakfast({ names: [null, 'avocado'], title: 'test' });
15 | assert.deepEqual(val, {
16 | name: 'bacon',
17 | names: ['eggs', 'avocado'],
18 | title: 'test'
19 | });
20 | });
21 |
22 | it('default function', function() {
23 | const now = Date.now();
24 | const Model = new Archetype({
25 | createdAt: { $type: Date, $required: true, $default: Date.now }
26 | }).compile();
27 |
28 | const val = new Model({});
29 | assert.ok(val.createdAt.getTime() >= now, `${val.createdAt}, ${now}`);
30 | });
31 |
32 | it('deep defaults', function() {
33 | const C = new Archetype({
34 | firstName: {
35 | $type: 'string',
36 | $default: () => 'test'
37 | },
38 | name: {
39 | first: {
40 | $type: 'string',
41 | $default: () => 'test'
42 | }
43 | },
44 | multiple: {
45 | a: {
46 | $type: 'string',
47 | $default: () => 'test'
48 | },
49 | b: {
50 | $type: 'string'
51 | }
52 | }
53 | }).compile('c');
54 |
55 | let v = new C({ multiple: { b: 'foo' } });
56 | assert.equal(v.firstName, 'test');
57 | assert.equal(v.name.first, 'test');
58 | assert.equal(v.multiple.a, 'test');
59 | assert.equal(v.multiple.b, 'foo');
60 | });
61 |
62 | it('clones empty default array and object', function() {
63 | const T = new Archetype({
64 | myArr: {
65 | $type: ['string'],
66 | $default: []
67 | },
68 | myObj: {
69 | $type: Object,
70 | $default: {}
71 | }
72 | }).compile('T');
73 |
74 | const obj1 = new T({});
75 |
76 | obj1.myArr.push('test');
77 | obj1.myObj.hello = 'world';
78 |
79 | assert.equal(obj1.myArr[0], 'test');
80 | assert.equal(obj1.myObj.hello, 'world');
81 |
82 | const obj2 = new T({});
83 | assert.deepEqual(obj2.myArr, []);
84 | assert.deepEqual(obj2.myObj, {});
85 | });
86 |
87 | it('throws if default is non-empty object', function() {
88 | assert.throws(() => {
89 | new Archetype({
90 | myArr: {
91 | $type: ['string'],
92 | $default: ['test']
93 | }
94 | }).compile('T');
95 | }, /Default is a non-empty object/);
96 |
97 | assert.throws(() => {
98 | new Archetype({
99 | myArr: {
100 | $type: {},
101 | $default: { test: 42 }
102 | }
103 | }).compile('T');
104 | }, /Default is a non-empty object/);
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/test/helpers.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('assert');
4 | const matchType = require('../').matchType;
5 |
6 | describe('matchType', function() {
7 | it('works with JSON.parse()', function() {
8 | const parse = matchType({ string: JSON.parse });
9 |
10 | const obj = { hello: 'world' };
11 |
12 | // If given a string, will parse it
13 | assert.deepEqual(parse(JSON.stringify(obj)), obj);
14 |
15 | // If not, will do nothing
16 | assert.strictEqual(parse(obj), obj);
17 | assert.strictEqual(parse(null), null);
18 | assert.strictEqual(parse(undefined), undefined);
19 | });
20 |
21 | it('works with trimming strings', function() {
22 | const trim = matchType({ string: str => str.trim() });
23 |
24 | // If given a string, will trim it
25 | assert.equal(trim(' abc '), 'abc');
26 | assert.equal(trim('abc'), 'abc');
27 |
28 | // If not, will do nothing
29 | const obj = {};
30 | assert.strictEqual(trim(obj), obj);
31 | assert.strictEqual(trim(null), null);
32 | assert.strictEqual(trim(undefined), undefined);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/test/to.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('assert');
4 | const to = require('../src/to');
5 |
6 | describe('to()', function() {
7 | it('handles strings', function() {
8 | assert.strictEqual(to('foo', 'string'), 'foo');
9 | assert.strictEqual(to(42, 'string'), '42');
10 | assert.strictEqual(to(void 0, 'string'), void 0);
11 | assert.strictEqual(to(null, 'string'), null);
12 | });
13 |
14 | it('handles numbers', function() {
15 | assert.strictEqual(to('42 ', 'number'), 42);
16 | assert.strictEqual(to(' 42', 'number'), 42);
17 | assert.strictEqual(to(new Date(1), 'number'), 1);
18 | assert.throws(() => to({ valueOf: () => '' }, 'number'), /Could not cast/i);
19 | assert.throws(() => to({ valueOf: () => null }, 'number'), /Could not cast/i);
20 | assert.strictEqual(to(void 0, 'string'), void 0);
21 | assert.strictEqual(to(null, 'string'), null);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/unit.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Archetype = require('../');
4 | const Path = require('../src/path');
5 | const assert = require('assert');
6 | const mongodb = require('mongodb');
7 |
8 | describe('schema', function() {
9 | it('compiles paths', function() {
10 | let schema = new Archetype({
11 | test: 'number',
12 | nested: {
13 | a: {
14 | $type: 'number'
15 | }
16 | }
17 | });
18 |
19 | schema.compile();
20 |
21 | assert.deepEqual(schema._paths, {
22 | test: { $type: 'number' },
23 | nested: { $type: Object, $schema: { a: { $type: 'number' } } },
24 | 'nested.a': { $type: 'number' }
25 | });
26 | });
27 |
28 | it('handles arrays', function() {
29 | let schema = new Archetype({
30 | test: 'number',
31 | arrMixed: [],
32 | arrPlain: ['number'],
33 | arrNested: [['number']]
34 | });
35 |
36 | schema.compile();
37 |
38 | assert.deepEqual(schema._paths, {
39 | 'test': { $type: 'number' },
40 | 'arrMixed': { $type: Array },
41 | 'arrMixed.$': { $type: null },
42 | 'arrPlain': { $type: Array },
43 | 'arrPlain.$': { $type: 'number' },
44 | 'arrNested': { $type: Array },
45 | 'arrNested.$': { $type: Array },
46 | 'arrNested.$.$': { $type: 'number' }
47 | });
48 |
49 | assert.ok(schema._paths['test'] instanceof Path);
50 | });
51 |
52 | it('handles nested document arrays', function() {
53 | let schema = new Archetype({
54 | docs: [{ _id: 'number' }]
55 | });
56 |
57 | schema.compile();
58 |
59 | assert.deepEqual(schema._paths, {
60 | 'docs': { $type: Array },
61 | 'docs.$': { $type: Object, $schema: { _id: 'number' } },
62 | 'docs.$._id': { $type: 'number' }
63 | });
64 | });
65 |
66 | it('treats keys that contain $type as a terminus', function() {
67 | let schema = new Archetype({
68 | test: {
69 | $type: 1
70 | }
71 | });
72 |
73 | schema.compile();
74 |
75 | assert.deepEqual(schema._paths, {
76 | 'test': { $type: 1 }
77 | });
78 | });
79 |
80 | it('supports $ keys', function() {
81 | let schema = new Archetype({
82 | $lt: 'number',
83 | $gt: 'number'
84 | });
85 |
86 | schema.compile();
87 |
88 | assert.deepEqual(schema._paths, {
89 | '$lt': { $type: 'number' },
90 | '$gt': { $type: 'number' }
91 | });
92 | });
93 |
94 | it('adding paths with .path()', function() {
95 | let schema = new Archetype({
96 | docs: [{ _id: 'number' }]
97 | });
98 |
99 | assert.ok(!schema.path('_id'));
100 | const newSchema = schema.path('_id', { $type: 'number' });
101 | assert.deepEqual(newSchema.path('_id'), { $type: 'number' });
102 | });
103 |
104 | it('arrays with $type', function() {
105 | const schema = new Archetype({
106 | docs: { $type: [{ _id: 'number' }] }
107 | });
108 |
109 | schema.compile();
110 |
111 | assert.deepEqual(schema._paths, {
112 | 'docs': { $type: Array },
113 | 'docs.$': { $type: Object, $schema: { _id: 'number' } },
114 | 'docs.$._id': { $type: 'number' }
115 | });
116 | });
117 | });
118 |
119 | describe('unmarshal()', function() {
120 | it('ignores paths not defined in the schema', function() {
121 | const Person = new Archetype({
122 | name: { $type: 'string' }
123 | }).compile();
124 |
125 | const axl = { name: 'Axl Rose', role: 'Lead Singer' };
126 | const res = new Person(axl);
127 | assert.deepEqual(res, { name: 'Axl Rose' });
128 | });
129 |
130 | it('casts values to specified types', function() {
131 | const Person = new Archetype({
132 | _id: { $type: mongodb.ObjectId },
133 | name: { $type: 'string' },
134 | born: { $type: 'number' }
135 | }).compile();
136 |
137 | const axl = {
138 | _id: '000000000000000000000001',
139 | name: 'Axl Rose',
140 | born: '1962'
141 | };
142 |
143 | const res = new Person(axl);
144 |
145 | assert.deepEqual(res, {
146 | _id: mongodb.ObjectId('000000000000000000000001'),
147 | name: 'Axl Rose',
148 | born: 1962
149 | });
150 | assert.ok(res instanceof Person);
151 | });
152 |
153 | it('only casts if necessary', function() {
154 | const Person = new Archetype({
155 | _id: { $type: mongodb.ObjectId },
156 | name: { $type: 'string' },
157 | born: { $type: 'number' }
158 | }).compile();
159 |
160 | const axl = {
161 | _id: new mongodb.ObjectId('000000000000000000000001'),
162 | name: 'Axl Rose',
163 | born: 1962
164 | };
165 |
166 | const res = new Person(axl);
167 |
168 | assert.deepEqual(res, {
169 | _id: new mongodb.ObjectId('000000000000000000000001'),
170 | name: 'Axl Rose',
171 | born: 1962
172 | });
173 | assert.ok(res instanceof Person);
174 | assert.ok(res._id instanceof mongodb.ObjectId);
175 | assert.equal(res._id.toHexString(), '000000000000000000000001');
176 | });
177 |
178 | it('casts into arrays', function() {
179 | let Band = new Archetype({
180 | members: [{ $type: mongodb.ObjectId }]
181 | }).compile();
182 |
183 | const band = {
184 | members: '000000000000000000000001'
185 | };
186 |
187 | const res = new Band(band);
188 |
189 | assert.deepEqual(res, {
190 | members: [mongodb.ObjectId('000000000000000000000001')]
191 | });
192 | });
193 |
194 | it('boolean to array', function() {
195 | let Band = new Archetype({
196 | test: { $type: Array }
197 | }).compile();
198 |
199 | const res = new Band({ test: true });
200 | assert.deepEqual(res.test, [true]);
201 | });
202 |
203 | it('casts deeply nested arrays', function() {
204 | const Graph = new Archetype({
205 | points: [[{ $type: 'number' }]]
206 | }).compile();
207 |
208 | const obj = { points: 1 };
209 | const res = new Graph(obj);
210 |
211 | assert.deepEqual(res, {
212 | points: [[1]]
213 | });
214 | });
215 |
216 | it('does not cast $type: Object', function() {
217 | const Test = new Archetype({
218 | nested: { $type: Object }
219 | }).compile();
220 |
221 | const obj = { nested: { hello: 'world' }, removed: 'field' };
222 | const res = new Test(obj);
223 |
224 | assert.deepEqual(res, {
225 | nested: { hello: 'world' }
226 | });
227 | });
228 |
229 | it('error if you cast an object to a primitive', function() {
230 | const Person = new Archetype({
231 | name: {
232 | first: { $type: 'string' },
233 | last: { $type: 'string' }
234 | }
235 | }).compile();
236 |
237 | let user = { name: 'Axl Rose' };
238 | let errored = false;
239 | try {
240 | new Person(user);
241 | } catch(error) {
242 | errored = true;
243 | assert.deepEqual(error.errors, {
244 | name: new Error("Could not cast 'Axl Rose' to Object")
245 | });
246 |
247 | const stringified = JSON.parse(JSON.stringify(error));
248 | assert.deepEqual(stringified.errors, {
249 | name: { message: "Could not cast 'Axl Rose' to Object" }
250 | });
251 | }
252 | assert.ok(errored);
253 | });
254 |
255 | it('ignores if $type not specified', function() {
256 | const Band = new Archetype({
257 | members: { $lookUp: { ref: 'Test' }, $type: Archetype.Any },
258 | tags: { $type: Array }
259 | }).compile();
260 |
261 | const band = { members: { x: 1 } };
262 | const res = new Band(band);
263 | assert.deepEqual(res, { members: { x: 1 } })
264 | });
265 |
266 | it('array of objects to primitive', function() {
267 | const Band = new Archetype({
268 | names: [{
269 | first: { $type: 'string' },
270 | last: { $type: 'string' }
271 | }]
272 | }).compile();
273 |
274 | const user = { names: ['Axl Rose'] };
275 | let errored = false;
276 | try {
277 | new Band(user);
278 | } catch(error) {
279 | errored = true;
280 | assert.deepEqual(error.errors, {
281 | 'names.0': new Error("Could not cast 'Axl Rose' to Object")
282 | });
283 | }
284 | assert.ok(errored);
285 | });
286 |
287 | it('array of objects', function() {
288 | const Band = new Archetype({
289 | people: [{name: { $type: 'string', $required: true } }]
290 | }).compile();
291 |
292 | const v = { people: [{ name: 'Axl Rose', other: 'field' }] };
293 | const res = new Band(v);
294 | assert.deepEqual(res, {
295 | people: [{ name: 'Axl Rose' }]
296 | });
297 | });
298 |
299 | it('disallows __proto__, constructor', function() {
300 | const Test = new Archetype({
301 | test: { $type: 'string' }
302 | }).compile();
303 |
304 | let res = new Test(JSON.parse('{"__proto__":"foo","test":"bar"}'));
305 | assert.deepEqual(res, {
306 | test: 'bar'
307 | });
308 | assert.equal(res.__proto__, Test.prototype);
309 |
310 | res = new Test(JSON.parse('{"constructor":"foo","test":"bar"}'));
311 | assert.deepEqual(res, {
312 | test: 'bar'
313 | });
314 | assert.equal(res.constructor, Test);
315 | });
316 |
317 | it('required', function() {
318 | const Person = new Archetype({
319 | name: { $type: 'string', $required: true }
320 | }).compile();
321 |
322 | let errored = false;
323 | try {
324 | new Person({});
325 | } catch(error) {
326 | errored = true;
327 | assert.deepEqual(error.errors, {
328 | name: new Error('Path "name" is required')
329 | });
330 | }
331 | assert.ok(errored);
332 |
333 | errored = false;
334 | try {
335 | new Person({ name: undefined });
336 | } catch(error) {
337 | errored = true;
338 | assert.deepEqual(error.errors, {
339 | name: new Error('Path "name" is required')
340 | });
341 |
342 | console.log(JSON.stringify(error, null, ' '))
343 | assert.deepEqual(JSON.parse(JSON.stringify(error)).errors, {
344 | name: { message: 'Path "name" is required' }
345 | });
346 | }
347 | assert.ok(errored);
348 |
349 | new Person({}, { $noRequired: 1 });
350 | });
351 |
352 | it('required with ObjectIds', function() {
353 | const Person = new Archetype({
354 | name: { $type: mongodb.ObjectId, $required: true }
355 | }).compile();
356 |
357 | let errored = false;
358 | try {
359 | new Person({ name: undefined });
360 | } catch(error) {
361 | errored = true;
362 | assert.deepEqual(error.errors, {
363 | name: new Error('Path "name" is required')
364 | });
365 | }
366 | assert.ok(errored);
367 | });
368 |
369 | it('recursive', function() {
370 | let NodeType = new Archetype({
371 | value: { $type: 'string' }
372 | }).compile('NodeType');
373 | NodeType.
374 | path('left', { $type: NodeType }, { inPlace: true }).
375 | path('right', { $type: NodeType }, { inPlace: true }).
376 | compile('NodeType');
377 |
378 | const raw = {
379 | value: 'root',
380 | left: {
381 | value: 'left'
382 | },
383 | right: {
384 | left: {
385 | value: 'right->left'
386 | },
387 | value: 'right'
388 | }
389 | };
390 | assert.deepEqual(raw, new NodeType(raw));
391 | assert.equal(raw.right.left.value, 'right->left');
392 | });
393 |
394 | it('required function', function() {
395 | const Person = new Archetype({
396 | requireName: 'boolean',
397 | name: { $type: 'string', $required: doc => doc.requireName }
398 | }).compile();
399 |
400 | // works
401 | new Person({});
402 |
403 | let errored = false;
404 | try {
405 | new Person({ requireName: true });
406 | } catch(error) {
407 | errored = true;
408 | assert.deepEqual(error.errors, {
409 | name: new Error('Path "name" is required')
410 | });
411 | }
412 | assert.ok(errored);
413 |
414 | // Works
415 | new Person({ requireName: true }, { $noRequired: 1 });
416 | });
417 |
418 | it('required in array', function() {
419 | const Person = new Archetype({
420 | names: [{ $type: 'string', $required: true }]
421 | }).compile();
422 |
423 | let errored = false;
424 | try {
425 | new Person({ names: ['test', null] });
426 | } catch(error) {
427 | errored = true;
428 | assert.deepEqual(error.errors, {
429 | 'names.1': new Error('Path "names.1" is required')
430 | });
431 | }
432 | assert.ok(errored);
433 | });
434 |
435 | it('no defaults for projecton', function() {
436 | const now = Date.now();
437 | const Model = new Archetype({
438 | name: { $type: 'string', $default: 'test' },
439 | createdAt: { $type: Date, $required: true, $default: Date.now }
440 | }).compile();
441 |
442 | const val = new Model({}, { createdAt: false });
443 | assert.deepEqual(val, { name: 'test' });
444 | });
445 |
446 | it('no defaults for projecton', function() {
447 | const now = Date.now();
448 | const Model = new Archetype({
449 | name: { $type: 'string', $default: 'test' },
450 | createdAt: { $type: Date, $required: true, $default: Date.now }
451 | }).compile();
452 |
453 | const val = new Model({}, { $noDefaults: true, $noRequired: true });
454 | assert.deepEqual(val, {});
455 | });
456 |
457 | it('projections', function() {
458 | const Person = new Archetype({
459 | name: {
460 | first: { $type: 'string' },
461 | last: { $type: 'string' }
462 | }
463 | }).compile();
464 |
465 | const justFirst = new Person({ name: { first: 'Axl', last: 'Rose' } },
466 | { 'name.first': 1 });
467 | assert.deepEqual(justFirst, { name: { first: 'Axl' } });
468 | const justLast = new Person({ name: { first: 'Axl', last: 'Rose' } },
469 | { 'name.first': 0 });
470 | assert.deepEqual(justLast, { name: { last: 'Rose' } });
471 | });
472 |
473 | it('validation', function() {
474 | const Breakfast = new Archetype({
475 | bacon: {
476 | $type: 'number',
477 | $required: true,
478 | $validate: v => {
479 | if (v < 3) {
480 | throw new Error('Need more bacon');
481 | }
482 | }
483 | }
484 | }).compile();
485 |
486 | assert.throws(function() {
487 | new Breakfast({ bacon: 2 });
488 | }, /Need more bacon/);
489 | });
490 |
491 | it('enum', function() {
492 | const Breakfast = new Archetype({
493 | type: {
494 | $type: 'string',
495 | $enum: ['steak and eggs', 'bacon and eggs']
496 | },
497 | addOns: [
498 | { name: { $type: 'string', $enum: ['cheese', 'sour cream'] } }
499 | ]
500 | }).compile();
501 |
502 | assert.throws(function() {
503 | new Breakfast({ type: 'waffles' });
504 | }, /Value "waffles" invalid/);
505 |
506 | assert.throws(function() {
507 | new Breakfast({ addOns: [{ name: 'maple syrup' }] });
508 | }, /Value "maple syrup" invalid/);
509 |
510 | // works
511 | new Breakfast({ type: 'steak and eggs' });
512 | new Breakfast({});
513 | new Breakfast({ addOns: [{ name: 'cheese' }] });
514 | });
515 |
516 | it('validation with arrays', function() {
517 | const Band = new Archetype({
518 | name: 'string',
519 | members: {
520 | $type: ['string'],
521 | $validate: v => {
522 | if (v.length !== 5) {
523 | throw new Error('Must have 5 members');
524 | }
525 | }
526 | }
527 | }).compile();
528 |
529 | assert.throws(function() {
530 | new Band({ name: "Guns N' Roses", members: ['Axl Rose'] });
531 | }, /Must have 5 members/);
532 |
533 | new Band({
534 | name: "Guns N' Roses",
535 | members: ['Axl Rose', 'Slash', 'Izzy', 'Duff', 'Adler']
536 | });
537 | });
538 |
539 | it('supports nested types', function() {
540 | const Person = new Archetype({
541 | name: 'string'
542 | }).compile();
543 | const Band = new Archetype({
544 | name: 'string',
545 | singer: {
546 | $type: Person
547 | }
548 | }).compile();
549 |
550 | const gnr = new Band({
551 | name: "Guns N' Roses",
552 | singer: {
553 | name: 'Axl Rose'
554 | }
555 | });
556 | assert.deepEqual(gnr, {
557 | name: "Guns N' Roses",
558 | singer: {
559 | name: 'Axl Rose'
560 | }
561 | });
562 | });
563 |
564 | it('compile takes a name param', function() {
565 | const Person = new Archetype({
566 | name: 'string'
567 | }).compile('PersonModel');
568 | assert.equal(Person.toString(), 'PersonModel');
569 | assert.ok(new Person({}) instanceof Person);
570 | assert.equal(new Person({}).constructor.name, 'PersonModel');
571 | });
572 |
573 | it('handles inheritance correctly with path(), etc.', function() {
574 | const ABase = new Archetype({ x: { $type: 'string' } }).compile('ABase');
575 |
576 | class A extends ABase {}
577 |
578 | const B = A.path('y', { $type: 'string' }).compile('B');
579 |
580 | assert.deepEqual(new B({ x: 1, y: 2 }), { x: '1', y: '2' });
581 | });
582 |
583 | it('validation with arrays and nested objects', function() {
584 | const Band = new Archetype({
585 | name: 'string',
586 | members: [{
587 | name: {
588 | $type: 'string',
589 | $validate: v => {
590 | if (['Axl Rose', 'Slash'].indexOf(v) === -1) {
591 | throw new Error('Invalid name!');
592 | }
593 | }
594 | }
595 | }]
596 | }).compile();
597 |
598 | assert.throws(function() {
599 | new Band({
600 | name: "Guns N' Roses",
601 | members: [{ name: 'Vince Neil' }]
602 | });
603 | }, /Invalid name!/);
604 |
605 | new Band({
606 | name: "Guns N' Roses",
607 | members: [{ name: 'Axl Rose' }]
608 | });
609 | });
610 |
611 | it('arrays with null', function() {
612 | const Nest = new Archetype({ name: { $type: 'string', $required: true } }).compile();
613 | const Test = new Archetype({
614 | members: { $type: [Nest] }
615 | }).compile();
616 |
617 | new Test({ members: null });
618 | });
619 |
620 | it('get paths as array', function() {
621 | const Test = new Archetype({
622 | str: 'string',
623 | num: {
624 | $type: 'number',
625 | $description: 'this is a number'
626 | }
627 | }).compile();
628 |
629 | assert.deepEqual(Test.paths().filter(v => !v.$description), [
630 | { path: 'str', $type: 'string' }
631 | ]);
632 | assert.deepEqual(Test.paths().filter(v => !!v.$description), [
633 | {
634 | path: 'num',
635 | $type: 'number',
636 | $description: 'this is a number'
637 | }
638 | ]);
639 | });
640 |
641 | it('$transform', function() {
642 | const Test = new Archetype({
643 | str: {
644 | $type: Object,
645 | $transform: JSON.parse
646 | },
647 | nums: {
648 | $type: ['number'],
649 | $transform: JSON.parse
650 | },
651 | objs: {
652 | $type: [{ $type: Object, $transform: JSON.parse }]
653 | }
654 | }).compile();
655 |
656 | const doc = new Test({
657 | str: JSON.stringify({ hello: 'world' }),
658 | nums: JSON.stringify([1, 2, 3]),
659 | objs: [JSON.stringify({ a: 1 }), JSON.stringify({ b: 2 })]
660 | });
661 | assert.deepEqual(doc, {
662 | str: { hello: 'world' },
663 | nums: [1, 2, 3],
664 | objs: [{ a: 1 }, { b: 2 }]
665 | });
666 | });
667 |
668 | it('$transform', function() {
669 | const Name = new Archetype({
670 | first: { $type: 'string' },
671 | last: { $type: 'string' }
672 | }).compile('Name');
673 | const Test = new Archetype({
674 | names: [{ $type: Name, $transform: JSON.parse }]
675 | }).compile();
676 |
677 | const doc = new Test({
678 | names: [
679 | JSON.stringify({ first: 'James', last: 'Kirk' }),
680 | JSON.stringify({ first: 'Leonard', last: 'McCoy' })
681 | ]
682 | });
683 | assert.deepEqual(doc, {
684 | names: [
685 | { first: 'James', last: 'Kirk' },
686 | { first: 'Leonard', last: 'McCoy' }
687 | ]
688 | });
689 | });
690 |
691 | it('$transform errors', function() {
692 | const Test = new Archetype({
693 | str: {
694 | $type: Object,
695 | $transform: JSON.parse
696 | }
697 | }).compile();
698 |
699 | let threw = false;
700 | try {
701 | new Test({
702 | str: { already: 'object' }
703 | });
704 | } catch (error) {
705 | assert.ok(error.errors['str']);
706 | threw = true;
707 | }
708 | assert.ok(threw);
709 | });
710 |
711 | it('to()', function() {
712 | const n = Archetype.to('2', 'number');
713 | assert.strictEqual(n, 2)
714 | });
715 |
716 | it('handles NaN', function() {
717 | const Test = new Archetype({ num: 'number' }).compile('Test');
718 | assert.throws(function() {
719 | new Test({ num: 'a' * 2 });
720 | }, /to number/);
721 | });
722 |
723 | it('handles casting whitespace to number', function() {
724 | const Test = new Archetype({ num: 'number' }).compile('Test');
725 | assert.throws(function() {
726 | new Test({ num: ' ' });
727 | }, /to number/);
728 | });
729 |
730 | it('required underneath array', function() {
731 | const Test = new Archetype({
732 | products: [{ name: { $type: 'string', $required: true } }]
733 | }).compile('Test');
734 | assert.throws(function() {
735 | new Test({ products: [{ name: null }] });
736 | }, /required/);
737 | });
738 |
739 | it('object array under projection', function() {
740 | const Test = new Archetype({
741 | name: 'string',
742 | arr: {
743 | $type: [{ el: { $type: 'string' } }],
744 | $required: true
745 | }
746 | }).compile('Test');
747 |
748 | assert.deepEqual(new Test({ name: '1', arr: [{ el: '2' }]}, { arr: 1 }), {
749 | arr: [{ el: '2' }]
750 | });
751 | });
752 |
753 | it('can optionally skip cloning', function() {
754 | const Test = new Archetype({
755 | name: 'string'
756 | }).compile('Test');
757 |
758 | const obj = { name: 'test', otherProp: 'foo' };
759 | new Test(obj);
760 | assert.equal(obj.otherProp, 'foo');
761 |
762 | const casted = new Test(obj, null, { clone: false });
763 | assert.equal(casted.name, 'test');
764 | assert.strictEqual(casted.otherProp, void 0);
765 | assert.strictEqual(obj.otherProp, void 0);
766 | });
767 |
768 | it('throws if $default is a date', function() {
769 | assert.throws(() => {
770 | const Test = new Archetype({
771 | name: {
772 | $type: Date,
773 | $default: new Date()
774 | }
775 | }).compile('Test');
776 | }, /non-empty object/);
777 | });
778 | });
779 |
780 | describe('schema modifications', function() {
781 | it('path() adds new paths', function() {
782 | const Test = new Archetype({
783 | str: 'string'
784 | }).compile();
785 |
786 | const Test2 = Test.path('num', { $type: 'number' }).compile('Test2');
787 | assert.deepEqual(new Test2({ str: 123, num: '123' }), {
788 | str: '123',
789 | num: 123
790 | });
791 | });
792 |
793 | it('omit() removes paths', function() {
794 | const Test = new Archetype({
795 | str: 'string',
796 | num: 'number'
797 | }).compile();
798 |
799 | const Test2 = Test.omit('num').compile('Test2');
800 | assert.deepEqual(new Test2({ str: 123, num: '123' }), {
801 | str: '123'
802 | });
803 | });
804 |
805 | it('omit() multiple paths', function() {
806 | const Test = new Archetype({
807 | str: 'string',
808 | num: 'number',
809 | bool: 'boolean'
810 | }).compile();
811 |
812 | const Test2 = Test.omit(['num', 'str']).compile('Test2');
813 | assert.deepEqual(new Test2({ str: 123, num: '123', bool: 'yes' }), {
814 | bool: true
815 | });
816 | });
817 |
818 | it('pick() creates a new schema with a subset of paths', function() {
819 | const Test = new Archetype({
820 | str: 'string',
821 | num: 'number'
822 | }).compile();
823 |
824 | const Test2 = Test.pick('num').compile('Test2');
825 | assert.deepEqual(new Test2({ str: 123, num: '123' }), {
826 | num: 123
827 | });
828 | });
829 |
830 | it('transform() loops over top-level paths and transforms them', function () {
831 | const Test = new Archetype({
832 | str: { $type: 'string', $required: true },
833 | num: { $type: 'number', $required: true }
834 | }).compile();
835 |
836 | const Test2 = Test.transform((path, props) => {
837 | if (path === 'num') {
838 | delete props.$required;
839 | }
840 | return props;
841 | }).compile('Test2');
842 |
843 | // Should work
844 | new Test2({ str: '123' });
845 | });
846 |
847 | it('transform() loops over nested paths and transforms them', function () {
848 | const Test = new Archetype({
849 | nested: {
850 | str: { $type: 'string', $required: true },
851 | num: { $type: 'number', $required: true }
852 | }
853 | }).compile();
854 |
855 | const Test2 = Test.transform((path, props) => {
856 | if (path === 'nested.num') {
857 | delete props.$required;
858 | }
859 | return props;
860 | }).compile('Test2');
861 |
862 | // Should work
863 | new Test2({ nested: { str: '123' } });
864 | });
865 |
866 | it('pick() and transform() create a new schema with a subset of paths with altered props', function() {
867 | const Test = new Archetype({
868 | str: 'string',
869 | num: 'number',
870 | didRun: {
871 | $type : 'boolean',
872 | $required: true,
873 | $default: false
874 | }
875 | }).compile();
876 |
877 | const Test2 = Test
878 | .pick(['num', 'didRun'])
879 | .transform((path, props) => {
880 | assert.ok(props, 'transform() props should be present.')
881 | if (path === 'didRun') {
882 | delete props.$required;
883 | delete props.$default;
884 | }
885 | return props;
886 | })
887 | .compile('Test2');
888 |
889 | assert.deepEqual(new Test2({ str: 123, num: '123' }), {
890 | num: 123
891 | });
892 | });
893 |
894 | it('eachPath() loops over nested paths', function () {
895 | const Test = new Archetype({
896 | nested: {
897 | str: { $type: 'string', $required: true },
898 | num: { $type: 'number', $required: true }
899 | }
900 | }).compile();
901 |
902 | const arr = [];
903 | Test.eachPath(path => {
904 | arr.push(path);
905 | });
906 |
907 | assert.deepEqual(arr, ['nested', 'nested.str', 'nested.num']);
908 | });
909 | });
910 |
--------------------------------------------------------------------------------
/test/util.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('assert');
4 | const shouldSkipPath = require('../src/util').shouldSkipPath;
5 |
6 | describe('shouldSkipPath', function() {
7 | it('returns false for __proto__', function() {
8 | assert.ok(!shouldSkipPath({}, '__proto__'));
9 | });
10 | });
--------------------------------------------------------------------------------