├── .editorconfig
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug.md
│ ├── feature parity.md
│ ├── feature.md
│ └── question.md
├── Notes to Maintainers.md
├── PULL_REQUEST_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE
│ ├── docs.md
│ ├── enhancement.md
│ ├── feature parity.md
│ ├── feature.md
│ └── fix.md
└── SECURITY.md
├── .gitignore
├── .mocharc.yml
├── .npmignore
├── .nycrc.json
├── .prettierrc
├── .travis.yml
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── data.ts
├── errors.ts
├── hooks.ts
├── index.ts
├── method.ts
├── plugin.ts
├── prop.ts
├── typegoose.ts
├── typeguards.ts
└── utils.ts
├── test
├── config_default.json
├── enums
│ ├── genders.ts
│ └── role.ts
├── index.test.ts
├── models
│ ├── PersistentModel.ts
│ ├── alias.ts
│ ├── car.ts
│ ├── hook1.ts
│ ├── hook2.ts
│ ├── indexweigths.ts
│ ├── internet-user.ts
│ ├── inventory.ts
│ ├── job.ts
│ ├── nested-object.ts
│ ├── person.ts
│ ├── rating.ts
│ ├── select.ts
│ ├── stringValidators.ts
│ ├── user.ts
│ ├── userRefs.ts
│ └── virtualprop.ts
├── tests
│ ├── biguser.test.ts
│ ├── db_index.test.ts
│ ├── getClassForDocument.test.ts
│ ├── hooks.test.ts
│ ├── shouldAdd.test.ts
│ ├── stringValidator.test.ts
│ └── typeguards.test.ts
└── utils
│ ├── config.ts
│ └── mongooseConnect.ts
├── tsconfig.build.json
├── tsconfig.json
└── tslint.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.ts]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | indent_size = 2
7 | indent_style = space
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribue
2 |
3 | ## Before making a Pull Request
4 |
5 | - Make sure all tests pass locally
6 | - Make sure you have run tslint & if something should come up, fix it to keep the code in one style
7 | - When you add documentation please make sure to keep the existing style
8 | - Make sure you read [Mastering-Markdown](https://guides.github.com/features/mastering-markdown/), thanks
9 | - Make sure when you make documentation of a something, you use the [TSDoc standard](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/), not JSDoc, thanks
10 |
11 | ---
12 |
13 | ## How to structure Commits
14 |
15 | ```
16 | Some Title
17 | - moving fileA to folderB/
18 | - removing fileB
19 | - adding tests for FeatureX
20 | - adding `@prop({ optionA })`
21 | - adding tsdoc for FeatureX
22 | - modify README to include Docs about A
23 | -
24 | ```
25 | *Legend:*
26 | - add `[#1]` at the end when there is an issue for it (and modify it to the actual number)
27 | - the title should be a short introduction like (for small fixes)`Add @mapProp for Maps with tests` (for bigger)`Adding TSDoc`[preferably split the commits when they get to large with adding more features]
28 | - the first word should be "adding" "removing" "moving", expect if it cant be expressed with those
29 |
30 | *Note: if you make a Pull Request that dosnt conform with this structure, it will be first rebased and then merged*
31 |
32 | ---
33 | *this is just the base, changes will occure*
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug
3 | about: Create a bug report
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | ---
10 | *please remove the parts in "---"*
11 |
12 | - In Versions, only include the Typegoose version you use (NPM or from GIT)
13 | - in "Code Example" add as many code blocks as needed
14 | - in "Do you know *why* it happenes replace the "*no*" if you know why
15 |
16 | ---
17 |
18 | ## Versions
19 |
20 | - NodeJS: 0.0.0
21 | - Typegoose(NPM): 0.0.0
22 | - Typegoose(GIT): commithash
23 | - mongoose: 0.0.0
24 | - mongodb: 0.0.0
25 |
26 | ## Code Example
27 |
28 | ```ts
29 | code here
30 | ```
31 |
32 | ## Do you know *why* it happenes?
33 |
34 | *no*
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature parity.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature parity
3 | about: Ask for feature parity with mongoose
4 | title: '[FP] '
5 | labels: parity
6 | assignees: ''
7 | ---
8 |
9 | ---
10 | *please remove the parts in "---"*
11 |
12 | ## How to Structure your Feature Parity request
13 |
14 | - remove "not needed" from your parity below
15 | - Make sure you read [Mastering-Markdown](https://guides.github.com/features/mastering-markdown/), thanks
16 |
17 | ---
18 |
19 | ## Which feature
20 |
21 | - [Example Feature 1](documentation_link)
22 | - [Example Feature 2](documentation_link)
23 |
24 | ## Why do you want it
25 |
26 | just because
27 |
28 | ## Additional Notes
29 |
30 | - add it here
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature
3 | about: Request a feature
4 | title: '[Request] '
5 | labels: request
6 | assignees: ''
7 | ---
8 |
9 | ---
10 | *please remove the parts in "---"*
11 |
12 | ## How to Structure your Feature request
13 |
14 | - When you have an Implementation Idea, remove "*no*"
15 | - Make sure you read [Mastering-Markdown](https://guides.github.com/features/mastering-markdown/), thanks
16 |
17 | ---
18 |
19 | ## Describe what you need | want
20 |
21 | description here
22 |
23 | ## Do you have already an idea for the implementation?
24 |
25 | *no*
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question
4 | title: '[Question] '
5 | labels: question
6 | assignees: ''
7 | ---
8 |
9 | ---
10 | *please remove the parts in "---"*
11 |
12 | ## What to include in your Questions
13 |
14 | - Make sure you provide an understandable question
15 | - Make sure you provide all the needed code
16 | - Make sure you read [Mastering-Markdown](https://guides.github.com/features/mastering-markdown/), thanks
17 |
18 | ---
19 |
--------------------------------------------------------------------------------
/.github/Notes to Maintainers.md:
--------------------------------------------------------------------------------
1 | # Notes to Maintainers
2 |
3 | ## Before a Merge
4 |
5 | - Make sure Travis Builds a passing
6 | - Run the tests locally
7 | - Review the Pull-Request if something should be changed
8 |
9 | ## How to Merge
10 |
11 | Try to to use Fast-Forward merge if it fits with the styles
12 | -> if it dosnt fit, rebase it manually
13 |
14 | ## Versioning
15 |
16 | [look at README#Versioning](../README.md#versioning)
17 |
18 | ---
19 | *this is just the base, changes will occure*
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | try to use an template, found at .github/PULL_REQUEST_TEMPLATE/
2 | [here](https://github.com/szokodiakos/typegoose/tree/master/.github/PULL_REQUEST_TEMPLATE)
3 | please dont forget to click on raw, and copy that, not the already "compiled" one
4 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/docs.md:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | *please remove the parts in "---"*
4 |
5 | ## Make sure you have done these steps
6 |
7 | - Make sure you have Read & followed these steps in [CONTRIBUTING](.github/CONTRIBUTING.md)
8 | - in "Adds Documentation for" remove the "(#) when there is no issue for it
9 | - remove the parts that are not applicable
10 |
11 | ---
12 |
13 | ## Adds Documentation for
14 |
15 | - @prop({ something }) (#1)
16 | - @prop({ something }) (#2)
17 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/enhancement.md:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | *please remove the parts in "---"*
4 |
5 | ## Make sure you have done these steps
6 |
7 | - Make sure you have Read & followed these steps in [CONTRIBUTING](.github/CONTRIBUTING.md)
8 | - remove the parts that are not applicable
9 |
10 | ---
11 |
12 | ## What does the Enchancement do
13 |
14 | *description here*
15 |
16 | ## Related Issues/PR's
17 |
18 | - #1
19 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/feature parity.md:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | *please remove the parts in "---"*
4 |
5 | ## Make sure you have done these steps
6 |
7 | - Make sure you have Read & followed these steps in [CONTRIBUTING](.github/CONTRIBUTING.md)
8 | - remove the parts that are not applicable
9 |
10 | ---
11 |
12 | ## What Feature from mongoose
13 |
14 | *description here* [link](mongoose feature URL)
15 |
16 | ## Related Issues/PR's
17 |
18 | - #1
19 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/feature.md:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | *please remove the parts in "---"*
4 |
5 | ## Make sure you have done these steps
6 |
7 | - Make sure you have Read & followed these steps in [CONTRIBUTING](.github/CONTRIBUTING.md)
8 | - remove the parts that are not applicable
9 |
10 | ---
11 |
12 | ## What does the Feature do
13 |
14 | *description here*
15 |
16 | ## Related Issues/PR's
17 |
18 | - #1
19 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/fix.md:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | *please remove the parts in "---"*
4 |
5 | this template is meant for "bug fixes" and "pr fixes"(if its an old pr, and gets remade with todays standards)
6 |
7 | ## Make sure you have done these steps
8 |
9 | - Make sure you have Read & followed these steps in [CONTRIBUTING](.github/CONTRIBUTING.md)
10 | - remove the parts that are not applicable
11 |
12 | ---
13 |
14 | ## Collection of what it does / fixes
15 |
16 | - {mode} {description}
17 | - adds @prop({ alias })
18 | - moves filea to test/
19 |
20 | ## Fixes Issues/PR's
21 |
22 | - #1
23 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | --------- | ------------------ |
7 | | latest | :white_check_mark: |
8 | | < latest | :x: |
9 |
10 | ## Reporting a Vulnerability
11 |
12 | Because this is an open-source Project, it can be normally be reported as any other issue
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/linux,macos,windows,visualstudiocode,node,intellij,webstorm
3 |
4 | ### Intellij ###
5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
7 |
8 | # User-specific stuff:
9 | .idea/**/workspace.xml
10 | .idea/**/tasks.xml
11 |
12 | # Sensitive or high-churn files:
13 | .idea/**/dataSources/
14 | .idea/**/dataSources.ids
15 | .idea/**/dataSources.xml
16 | .idea/**/dataSources.local.xml
17 | .idea/**/sqlDataSources.xml
18 | .idea/**/dynamic.xml
19 | .idea/**/uiDesigner.xml
20 |
21 | # Gradle:
22 | .idea/**/gradle.xml
23 | .idea/**/libraries
24 |
25 | # Mongo Explorer plugin:
26 | .idea/**/mongoSettings.xml
27 |
28 | ## File-based project format:
29 | *.iws
30 |
31 | ## Plugin-specific files:
32 |
33 | # IntelliJ
34 | /out/
35 |
36 | # mpeltonen/sbt-idea plugin
37 | .idea_modules/
38 |
39 | # JIRA plugin
40 | atlassian-ide-plugin.xml
41 |
42 | # Crashlytics plugin (for Android Studio and IntelliJ)
43 | com_crashlytics_export_strings.xml
44 | crashlytics.properties
45 | crashlytics-build.properties
46 | fabric.properties
47 |
48 | ### Intellij Patch ###
49 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
50 |
51 | # *.iml
52 | # modules.xml
53 | # .idea/misc.xml
54 | # *.ipr
55 |
56 | ### Linux ###
57 | *~
58 |
59 | # temporary files which can be created if a process still has a handle open of a deleted file
60 | .fuse_hidden*
61 |
62 | # KDE directory preferences
63 | .directory
64 |
65 | # Linux trash folder which might appear on any partition or disk
66 | .Trash-*
67 |
68 | # .nfs files are created when an open file is removed but is still being accessed
69 | .nfs*
70 |
71 | ### macOS ###
72 | *.DS_Store
73 | .AppleDouble
74 | .LSOverride
75 |
76 | # Icon must end with two \r
77 | Icon
78 |
79 |
80 | # Thumbnails
81 | ._*
82 |
83 | # Files that might appear in the root of a volume
84 | .DocumentRevisions-V100
85 | .fseventsd
86 | .Spotlight-V100
87 | .TemporaryItems
88 | .Trashes
89 | .VolumeIcon.icns
90 | .com.apple.timemachine.donotpresent
91 |
92 | # Directories potentially created on remote AFP share
93 | .AppleDB
94 | .AppleDesktop
95 | Network Trash Folder
96 | Temporary Items
97 | .apdisk
98 |
99 | ### Node ###
100 | # Logs
101 | logs
102 | *.log
103 | npm-debug.log*
104 | yarn-debug.log*
105 | yarn-error.log*
106 |
107 | # Runtime data
108 | pids
109 | *.pid
110 | *.seed
111 | *.pid.lock
112 |
113 | # Directory for instrumented libs generated by jscoverage/JSCover
114 | lib-cov
115 |
116 | # Coverage directory used by tools like istanbul
117 | coverage
118 |
119 | # nyc test coverage
120 | .nyc_output
121 |
122 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
123 | .grunt
124 |
125 | # Bower dependency directory (https://bower.io/)
126 | bower_components
127 |
128 | # node-waf configuration
129 | .lock-wscript
130 |
131 | # Compiled binary addons (http://nodejs.org/api/addons.html)
132 | build/Release
133 |
134 | # Dependency directories
135 | node_modules/
136 | jspm_packages/
137 |
138 | # Typescript v1 declaration files
139 | typings/
140 |
141 | # Optional npm cache directory
142 | .npm
143 |
144 | # Optional eslint cache
145 | .eslintcache
146 |
147 | # Optional REPL history
148 | .node_repl_history
149 |
150 | # Output of 'npm pack'
151 | *.tgz
152 |
153 | # Yarn Integrity file
154 | .yarn-integrity
155 |
156 | # dotenv environment variables file
157 | .env
158 |
159 |
160 | ### VisualStudioCode ###
161 | .vscode/*
162 | !.vscode/settings.json
163 | !.vscode/tasks.json
164 | !.vscode/launch.json
165 | !.vscode/extensions.json
166 |
167 | ### WebStorm ###
168 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
169 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
170 |
171 | # User-specific stuff:
172 |
173 | # Sensitive or high-churn files:
174 |
175 | # Gradle:
176 |
177 | # Mongo Explorer plugin:
178 |
179 | ## File-based project format:
180 |
181 | ## Plugin-specific files:
182 |
183 | # IntelliJ
184 |
185 | # mpeltonen/sbt-idea plugin
186 |
187 | # JIRA plugin
188 |
189 | # Crashlytics plugin (for Android Studio and IntelliJ)
190 |
191 | ### WebStorm Patch ###
192 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
193 |
194 | # *.iml
195 | # modules.xml
196 | # .idea/misc.xml
197 | # *.ipr
198 |
199 | ### Windows ###
200 | # Windows thumbnail cache files
201 | Thumbs.db
202 | ehthumbs.db
203 | ehthumbs_vista.db
204 |
205 | # Folder config file
206 | Desktop.ini
207 |
208 | # Recycle Bin used on file shares
209 | $RECYCLE.BIN/
210 |
211 | # Windows Installer files
212 | *.cab
213 | *.msi
214 | *.msm
215 | *.msp
216 |
217 | # Windows shortcuts
218 | *.lnk
219 |
220 | # End of https://www.gitignore.io/api/linux,macos,windows,visualstudiocode,node,intellij,webstorm
221 |
222 | build
223 | .env
224 | lib
225 |
226 | # ts's build info
227 | .tsbuildinfo
228 |
229 | # the config
230 | test/config.json
231 |
232 | # typedoc docs folder
233 | doc/
234 |
--------------------------------------------------------------------------------
/.mocharc.yml:
--------------------------------------------------------------------------------
1 | color: true
2 | recursive: true
3 | require:
4 | - "ts-node/register"
5 | - "source-map-support/register"
6 | extension:
7 | - ts
8 | ui: "bdd"
9 | check-leaks: true
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/linux,macos,windows,visualstudiocode,node,intellij,webstorm
2 |
3 | ### Intellij ###
4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
6 |
7 | # User-specific stuff:
8 | .idea/**/workspace.xml
9 | .idea/**/tasks.xml
10 |
11 | # Sensitive or high-churn files:
12 | .idea/**/dataSources/
13 | .idea/**/dataSources.ids
14 | .idea/**/dataSources.xml
15 | .idea/**/dataSources.local.xml
16 | .idea/**/sqlDataSources.xml
17 | .idea/**/dynamic.xml
18 | .idea/**/uiDesigner.xml
19 |
20 | # Gradle:
21 | .idea/**/gradle.xml
22 | .idea/**/libraries
23 |
24 | # Mongo Explorer plugin:
25 | .idea/**/mongoSettings.xml
26 |
27 | ## File-based project format:
28 | *.iws
29 |
30 | ## Plugin-specific files:
31 |
32 | # IntelliJ
33 | /out/
34 |
35 | # mpeltonen/sbt-idea plugin
36 | .idea_modules/
37 |
38 | # JIRA plugin
39 | atlassian-ide-plugin.xml
40 |
41 | # Crashlytics plugin (for Android Studio and IntelliJ)
42 | com_crashlytics_export_strings.xml
43 | crashlytics.properties
44 | crashlytics-build.properties
45 | fabric.properties
46 |
47 | ### Intellij Patch ###
48 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
49 |
50 | # *.iml
51 | # modules.xml
52 | # .idea/misc.xml
53 | # *.ipr
54 |
55 | ### Linux ###
56 | *~
57 |
58 | # temporary files which can be created if a process still has a handle open of a deleted file
59 | .fuse_hidden*
60 |
61 | # KDE directory preferences
62 | .directory
63 |
64 | # Linux trash folder which might appear on any partition or disk
65 | .Trash-*
66 |
67 | # .nfs files are created when an open file is removed but is still being accessed
68 | .nfs*
69 |
70 | ### macOS ###
71 | *.DS_Store
72 | .AppleDouble
73 | .LSOverride
74 |
75 | # Icon must end with two \r
76 | Icon
77 |
78 |
79 | # Thumbnails
80 | ._*
81 |
82 | # Files that might appear in the root of a volume
83 | .DocumentRevisions-V100
84 | .fseventsd
85 | .Spotlight-V100
86 | .TemporaryItems
87 | .Trashes
88 | .VolumeIcon.icns
89 | .com.apple.timemachine.donotpresent
90 |
91 | # Directories potentially created on remote AFP share
92 | .AppleDB
93 | .AppleDesktop
94 | Network Trash Folder
95 | Temporary Items
96 | .apdisk
97 |
98 | ### Node ###
99 | # Logs
100 | logs
101 | *.log
102 | npm-debug.log*
103 | yarn-debug.log*
104 | yarn-error.log*
105 |
106 | # Runtime data
107 | pids
108 | *.pid
109 | *.seed
110 | *.pid.lock
111 |
112 | # Directory for instrumented libs generated by jscoverage/JSCover
113 | lib-cov
114 |
115 | # Coverage directory used by tools like istanbul
116 | coverage
117 |
118 | # nyc test coverage
119 | .nyc_output
120 |
121 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
122 | .grunt
123 |
124 | # Bower dependency directory (https://bower.io/)
125 | bower_components
126 |
127 | # node-waf configuration
128 | .lock-wscript
129 |
130 | # Compiled binary addons (http://nodejs.org/api/addons.html)
131 | build/Release
132 |
133 | # Dependency directories
134 | node_modules/
135 | jspm_packages/
136 |
137 | # Typescript v1 declaration files
138 | typings/
139 |
140 | # Optional npm cache directory
141 | .npm
142 |
143 | # Optional eslint cache
144 | .eslintcache
145 |
146 | # Optional REPL history
147 | .node_repl_history
148 |
149 | # Output of 'npm pack'
150 | *.tgz
151 |
152 | # Yarn Integrity file
153 | .yarn-integrity
154 |
155 | # dotenv environment variables file
156 | .env
157 |
158 |
159 | ### VisualStudioCode ###
160 | .vscode/*
161 | !.vscode/settings.json
162 | !.vscode/tasks.json
163 | !.vscode/launch.json
164 | !.vscode/extensions.json
165 |
166 | ### WebStorm ###
167 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
168 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
169 |
170 | # User-specific stuff:
171 |
172 | # Sensitive or high-churn files:
173 |
174 | # Gradle:
175 |
176 | # Mongo Explorer plugin:
177 |
178 | ## File-based project format:
179 |
180 | ## Plugin-specific files:
181 |
182 | # IntelliJ
183 |
184 | # mpeltonen/sbt-idea plugin
185 |
186 | # JIRA plugin
187 |
188 | # Crashlytics plugin (for Android Studio and IntelliJ)
189 |
190 | ### WebStorm Patch ###
191 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
192 |
193 | # *.iml
194 | # modules.xml
195 | # .idea/misc.xml
196 | # *.ipr
197 |
198 | ### Windows ###
199 | # Windows thumbnail cache files
200 | Thumbs.db
201 | ehthumbs.db
202 | ehthumbs_vista.db
203 |
204 | # Folder config file
205 | Desktop.ini
206 |
207 | # Recycle Bin used on file shares
208 | $RECYCLE.BIN/
209 |
210 | # Windows Installer files
211 | *.cab
212 | *.msi
213 | *.msm
214 | *.msp
215 |
216 | # Windows shortcuts
217 | *.lnk
218 |
219 | # End of https://www.gitignore.io/api/linux,macos,windows,visualstudiocode,node,intellij,webstorm
220 |
221 | build
222 | .env
223 |
224 | # ts's build info
225 | .tsbuildinfo
226 |
--------------------------------------------------------------------------------
/.nycrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@istanbuljs/nyc-config-typescript",
3 | "extension": [
4 | ".ts"
5 | ],
6 | "exclude": [
7 | "**/*.d.ts"
8 | ],
9 | "include": [
10 | "src/**/*.ts"
11 | ],
12 | "check-coverage": true,
13 | "per-file": true,
14 | "cache": true,
15 | "all": true,
16 | "reporter": [
17 | "text",
18 | "html"
19 | ],
20 | "sourceMap": true,
21 | "instrument": true,
22 | "_comment": "this below is just while porting to gitlab is going on",
23 | "lines": 0,
24 | "statements": 0,
25 | "functions": 0,
26 | "branches": 0
27 | }
28 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "useTabs": false,
7 | "arrowParens": "avoid",
8 | "endOfLine": "auto",
9 | "insertPragma": false,
10 | "printWidth": 120,
11 | "proseWrap": "always"
12 | }
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | # Test against oldest node version in package.json
4 | - "8"
5 | # Test against highest node version available
6 | - lts/*
7 | script:
8 | # Audit to make sure there are no vulnerabilities, but do not affect the whole build
9 | - (npm audit || exit 0)
10 | # Test everything
11 | - npm test
12 | deploy:
13 | provider: npm
14 | email: benedikt.huss@gmail.com
15 | skip_cleanup: true
16 | api_key:
17 | secure: pNyluNoFqt4/mH0afzIdGnDHsDqjJTafh766eUIB2jriaEQ0v6DQ/JaTnsx8v7S7W8ibXM4xgoidbAbqs1Y7ZPYTMwlZTIlEa19CUtOdUnP+cT2V4feQMYMi3jlSMoOm7k56CaufFz2yUyGwvqMJFHspXyVbLnpdJWX9OOnL0E42NSBHhs+jBB3QdyLKIiQQzGBHAAsr/Yl2ot3k9ndfyzSPhGkvQWYeRn7AwulWg9LB8qiTFsEXPxtVQOo/tkVbF4pWpA99CZbaY/ibDzm9OYCl73qHMX0N8FG4eB1ByGGUnxGjAJWfvB2QWkZcsnH7ywJCJRjHjaOpkXHmPQVZ0i3F6fGcTg0vyhuQ9W3staYO+kxZa+Ol6EzAZSsUsgWepOJYMfW4w31PKwQsoJrz/efFNORzPfX4GmaRkqXBRfIdYgnrl7SFV3S6ljADPLo3rbAuNYjgZ+Ui7Ged1fHr5OESuEzhue8ldoEcWJGoC78+6UE40XFB0CSnugRt/LVe6JM1fov5LcuwCjtLYTiDR/AsMb8pX6fVlYIbox/zGMVghZlRwc/iSMn6+3RrQGB8f30l1vx+/M/Tb3sHiiTFyXa58uIwGisaQ39//LuwezrYG0sNwzqrV8YEahZxIx9AvqqZ9nGjjp4cKRQ0vt5t58f6Cr/vz1VHhvEtlKMmOA8=
18 | on:
19 | branches:
20 | only:
21 | - master
22 | after_success:
23 | # Generate the Coverage only for one version
24 | - |
25 | if [[ $TRAVIS_NODE_VERSION == "lts/*" ]]; then
26 | npm run coverage
27 | fi
28 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "launch",
7 | "name": "Run Tests",
8 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
9 | "args": [
10 | "--colors",
11 | "${workspaceRoot}/lib/test/*.test.js"
12 | ],
13 | "sourceMaps": true,
14 | "preLaunchTask": "build"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "prettier.printWidth": 120,
4 | "editor.wordWrapColumn": 120,
5 | "editor.tabSize": 2,
6 | "editor.insertSpaces": true
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "script": "watch",
7 | "problemMatcher": [
8 | "$tsc-watch"
9 | ],
10 | "isBackground": true
11 | },
12 | {
13 | "type": "npm",
14 | "script": "build",
15 | "problemMatcher": [
16 | "$tsc"
17 | ]
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Akos Szokodi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # This Repository got moved
2 |
3 | Please use [hasezoey's fork](https://github.com/hasezoey/typegoose) to be up-to-date
4 | Please dont create new issues & pull request anymore, thanks
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # Typegoose
16 |
17 | [](https://travis-ci.org/szokodiakos/typegoose)
18 | [](https://coveralls.io/github/szokodiakos/typegoose?branch=master)
19 | []()
20 |
21 | Define Mongoose models using TypeScript classes.
22 |
23 | ## Basic usage
24 |
25 | ```ts
26 | import { prop, Typegoose, ModelType, InstanceType } from 'typegoose';
27 | import * as mongoose from 'mongoose';
28 |
29 | mongoose.connect('mongodb://localhost:27017/test');
30 |
31 | class User extends Typegoose {
32 | @prop()
33 | name?: string;
34 | }
35 |
36 | const UserModel = new User().getModelForClass(User);
37 |
38 | // UserModel is a regular Mongoose Model with correct types
39 | (async () => {
40 | const u = await UserModel.create({ name: 'JohnDoe' });
41 | const user = await UserModel.findOne();
42 |
43 | // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
44 | console.log(user);
45 | })();
46 | ```
47 |
48 | ## Motivation
49 |
50 | A common problem when using Mongoose with TypeScript is that you have to define both the Mongoose model and the TypeScript interface. If the model changes, you also have to keep the TypeScript interface file in sync or the TypeScript interface would not represent the real data structure of the model.
51 |
52 | Typegoose aims to solve this problem by defining only a TypeScript interface (class) which need to be enhanced with special Typegoose decorators.
53 |
54 | Under the hood it uses the [reflect-metadata](https://github.com/rbuckton/reflect-metadata) API to retrieve the types of the properties, so redundancy can be significantly reduced.
55 |
56 | Instead of:
57 |
58 | ```ts
59 | interface Car {
60 | model?: string;
61 | }
62 |
63 | interface Job {
64 | title?: string;
65 | position?: string;
66 | }
67 |
68 | interface User {
69 | name?: string;
70 | age: number;
71 | job?: Job;
72 | car: Car | string;
73 | }
74 |
75 | mongoose.model('User', {
76 | name: String,
77 | age: { type: Number, required: true },
78 | job: {
79 | title: String;
80 | position: String;
81 | },
82 | car: { type: Schema.Types.ObjectId, ref: 'Car' }
83 | });
84 |
85 | mongoose.model('Car', {
86 | model: string,
87 | });
88 | ```
89 |
90 | You can just:
91 |
92 | ```ts
93 | class Job {
94 | @prop()
95 | title?: string;
96 |
97 | @prop()
98 | position?: string;
99 | }
100 |
101 | class Car extends Typegoose {
102 | @prop()
103 | model?: string;
104 | }
105 |
106 | class User extends Typegoose {
107 | @prop()
108 | name?: string;
109 |
110 | @prop({ required: true })
111 | age!: number;
112 |
113 | @prop()
114 | job?: Job;
115 |
116 | @prop({ ref: Car })
117 | car?: Ref;
118 | }
119 | ```
120 |
121 | Please note that sub documents do not have to extend Typegoose. You can still give them default value in `prop` decorator, but you can't create static or instance methods on them.
122 |
123 | ## Requirements
124 |
125 | * TypeScript 3.2+
126 | * Node 8+
127 | * mongoose 5+
128 | * `emitDecoratorMetadata` and `experimentalDecorators` must be enabled in `tsconfig.json`
129 | * `reflect-metadata` must be installed
130 |
131 | ## Install
132 |
133 | `npm install typegoose -S`
134 |
135 | You also need to install `mongoose` and `reflect-metadata`, in versions < 5.0, these packages were listed as dependencies in `package.json`, starting with version 5.0 these packages are listed as peer dependencies.
136 |
137 | `npm install mongoose reflect-metadata -S`
138 |
139 | ## Testing
140 |
141 | `npm test`
142 |
143 | ## Versioning
144 |
145 | `Major.Minor.Fix` (or how npm expresses it `Major.Minor.Patch`)
146 |
147 | * `0.0.x` is for minor fixes, like hot-fixes
148 | * `0.x.0` is for Minor things like adding features, that are non-breaking (or at least should not be breaking anything)
149 | * `x.0.0` is for Major things like adding features that are breaking or refactoring which is a breaking change
150 | * `0.0.0-x` is for a Pre-Release, that are not yet ready to be published
151 |
152 | ## API Documentation
153 |
154 | ### Typegoose class
155 |
156 | This is the class which your schema defining classes must extend.
157 |
158 | #### Methods:
159 |
160 | `getModelForClass(t: T, options?: GetModelForClassOptions)`
161 |
162 | This method returns the corresponding Mongoose Model for the class (`T`). If no Mongoose model exists for this class yet, one will be created automatically (by calling the method `setModelForClass`).
163 |
164 |
165 | `setModelForClass(t: T, options?: GetModelForClassOptions)`
166 |
167 | This method assembles the Mongoose Schema from the decorated schema defining class, creates the Mongoose Model and returns it. For typing reasons, the schema defining class must be passed down to it.
168 |
169 | Hint: If a Mongoose Model already exists for this class, it will be overwritten.
170 |
171 |
172 | The `GetModelForClassOptions` provides multiple optional configurations:
173 | * `existingMongoose: mongoose`: An existing Mongoose instance can also be passed down. If given, Typegoose uses this Mongoose instance's `model` methods.
174 | * `schemaOptions: mongoose.SchemaOptions`: Additional [schema options](http://mongoosejs.com/docs/guide.html#options) can be passed down to the schema-to-be-created.
175 | * `existingConnection: mongoose.Connection`: An existing Mongoose connection can also be passed down. If given, Typegoose uses this Mongoose instance's `model` methods.
176 |
177 | ### Property decorators
178 |
179 | Typegoose comes with TypeScript decorators, which responsibility is to connect the Mongoose schema behind the TypeScript class.
180 |
181 | #### prop(options)
182 |
183 | The `prop` decorator adds the target class property to the Mongoose schema as a property. Typegoose checks the decorated property's type and sets the schema property accordingly. If another Typegoose extending class is given as the type, Typegoose will recognize this property as a sub document.
184 |
185 | The `options` object accepts multiple config properties:
186 | - `required`: Just like the [Mongoose required](http://mongoosejs.com/docs/api.html#schematype_SchemaType-required)
187 | it accepts a handful of parameters. Please note that it's the developer's responsibility to make sure that
188 | if `required` is set to `false` then the class property should be [optional](https://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties).
189 |
190 | Note: for coding style (and type completion) you should use `!` when it is marked as required
191 |
192 | ```ts
193 | // this is now required in the schema
194 | @prop({ required: true })
195 | firstName!: string;
196 |
197 | // by default, a property is not required
198 | @prop()
199 | lastName?: string; // using the ? optional property
200 | ```
201 |
202 | - `index`: Tells Mongoose whether to define an index for the property.
203 |
204 | ```ts
205 | @prop({ index: true })
206 | indexedField?: string;
207 | ```
208 |
209 | - `unique`: Just like the [Mongoose unique](http://mongoosejs.com/docs/api.html#schematype_SchemaType-unique), tells Mongoose to ensure a unique index is created for this path.
210 |
211 | ```ts
212 | // this field is now unique across the collection
213 | @prop({ unique: true })
214 | uniqueId?: string;
215 | ```
216 |
217 | - `enum`: The enum option accepts a string array. The class property which gets this decorator should have an enum-like type which values are from the provided string array. The way how the enum is created is delegated to the developer, Typegoose needs a string array which hold the enum values, and a TypeScript type which tells the possible values of the enum.
218 | However, if you use TS 2.4+, you can use string enum as well.
219 |
220 | ```ts
221 | enum Gender {
222 | MALE = 'male',
223 | FEMALE = 'female',
224 | }
225 |
226 | @prop({ enum: Gender })
227 | gender?: Gender;
228 | ```
229 |
230 | - `lowercase`: for strings only; whether to always call .toLowerCase() on the value.
231 |
232 | ```ts
233 | @prop({ lowercase: true })
234 | nickName?: string;
235 | ```
236 |
237 | - `uppercase`: for strings only; whether to always call .toUpperCase() on the value.
238 |
239 | ```ts
240 | @prop({ uppercase: true })
241 | nickName?: string;
242 | ```
243 |
244 | - `trim`: for strings only; whether to always call .trim() on the value.
245 |
246 | ```ts
247 | @prop({ trim: true })
248 | nickName?: string;
249 | ```
250 |
251 | - `default`: The provided value will be the default for that Mongoose property.
252 |
253 | ```ts
254 | @prop({ default: 'Nick' })
255 | nickName?: string;
256 | ```
257 |
258 | - `_id`: When false, no \_id is added to the subdocument
259 |
260 | ```ts
261 | class Car extends Typegoose {}
262 |
263 | @prop({ _id: false })
264 | car?: Car;
265 | ```
266 |
267 | - `ref`: By adding the `ref` option with another Typegoose class as value, a Mongoose reference property will be created. The type of the property on the Typegoose extending class should be `Ref` (see Types section).
268 |
269 | ```ts
270 | class Car extends Typegoose {}
271 |
272 | @prop({ ref: Car })
273 | car?: Ref;
274 | ```
275 |
276 | - `refPath`: Is the same as `ref`, only that it looks at the path specified, and this path decides which model to use
277 |
278 | ```ts
279 | class Car extends Typegoose {}
280 | class Shop extends Typegoose {}
281 |
282 | // in another class
283 | class Another extends Typegoose {
284 | @prop({ required: true, enum: 'Car' | 'Shop' })
285 | which!: string;
286 |
287 | @prop({ refPath: 'which' })
288 | kind?: Ref;
289 | }
290 | ```
291 |
292 | - `min` / `max` (numeric validators): Same as [Mongoose numberic validators](http://mongoosejs.com/docs/api.html#schema_number_SchemaNumber-max).
293 |
294 | ```ts
295 | @prop({ min: 10, max: 21 })
296 | age?: number;
297 | ```
298 |
299 | - `minlength` / `maxlength` / `match` (string validators): Same as [Mongoose string validators](http://mongoosejs.com/docs/api.html#schema_string_SchemaString-match).
300 |
301 | ```ts
302 | @prop({ minlength: 5, maxlength: 10, match: /[0-9a-f]*/ })
303 | favouriteHexNumber?: string;
304 | ```
305 |
306 |
307 | - `validate` (custom validators): You can define your own validator function/regex using this. The function has to return a `boolean` or a Promise (async validation).
308 |
309 | ```ts
310 | // you have to get your own `isEmail` function, this is a placeholder
311 |
312 | @prop({ validate: (value) => isEmail(value)})
313 | email?: string;
314 |
315 | // or
316 |
317 | @prop({ validate: (value) => { return new Promise(res => { res(isEmail(value)) }) })
318 | email?: string;
319 |
320 | // or
321 |
322 | @prop({ validate: {
323 | validator: val => isEmail(val),
324 | message: `{VALUE} is not a valid email`
325 | }})
326 | email?: string;
327 |
328 | // or
329 |
330 | @prop({ validate: /\S+@\S+\.\S+/ })
331 | email?: string;
332 |
333 | // you can also use multiple validators in an array.
334 |
335 | @prop({ validate:
336 | [
337 | {
338 | validator: val => isEmail(val),
339 | message: `{VALUE} is not a valid email`
340 | },
341 | {
342 | validator: val => isBlacklisted(val),
343 | message: `{VALUE} is blacklisted`
344 | }
345 | ]
346 | })
347 | email?: string;
348 | ```
349 |
350 | - `alias` (alias): Same as [Mongoose Alias](https://mongoosejs.com/docs/guide.html#aliases), only difference is the extra property for type completion
351 | ```ts
352 | class Dummy extends Typegoose {
353 | @prop({ alias: "helloWorld" })
354 | public hello: string; // will be included in the DB
355 | public helloWorld: string; // will NOT be included in the DB, just for type completion (gets passed as hello in the DB)
356 | }
357 | ```
358 |
359 | Mongoose gives developers the option to create [virtual properties](http://mongoosejs.com/docs/api.html#schema_Schema-virtual). This means that actual database read/write will not occur these are just 'calculated properties'. A virtual property can have a setter and a getter. TypeScript also has a similar feature which Typegoose uses for virtual property definitions (using the `prop` decorator).
360 |
361 | ```ts
362 | @prop()
363 | firstName?: string;
364 |
365 | @prop()
366 | lastName?: string;
367 |
368 | @prop() // this will create a virtual property called 'fullName'
369 | get fullName() {
370 | return `${this.firstName} ${this.lastName}`;
371 | }
372 | set fullName(full) {
373 | const [firstName, lastName] = full.split(' ');
374 | this.firstName = firstName;
375 | this.lastName = lastName;
376 | }
377 | ```
378 |
379 | TODO: add documentation for virtual population
380 |
381 | #### arrayProp(options)
382 |
383 | The `arrayProp` is a `prop` decorator which makes it possible to create array schema properties.
384 |
385 | The `options` object accepts `required`, `enum` and `default`, just like the `prop` decorator. In addition to these the following properties exactly one should be given:
386 |
387 | - `items`: This will tell Typegoose that this is an array which consists of primitives (if `String`, `Number`, or other primitive type is given) or this is an array which consists of subdocuments (if it's extending the `Typegoose` class).
388 |
389 | ```ts
390 | @arrayProp({ items: String })
391 | languages?: string[];
392 | ```
393 |
394 | Note that unfortunately the [reflect-metadata](https://github.com/rbuckton/reflect-metadata) API does not let us determine the type of the array, it only returns `Array` when the type of the property is queried. This is why redundancy is required here.
395 |
396 | - `itemsRef`: In mutual exclusion with `items`, this tells Typegoose that instead of a subdocument array, this is an array with references in it. On the Mongoose side this means that an array of Object IDs will be stored under this property. Just like with `ref` in the `prop` decorator, the type of this property should be `Ref[]`.
397 |
398 | ```ts
399 | class Car extends Typegoose {}
400 |
401 | // in another class
402 | @arrayProp({ itemsRef: Car })
403 | previousCars?: Ref[];
404 | ```
405 |
406 | - `itemsRefPath`(IRP): Is the same as `itemsRef` only that it looks at the specified path of the class which specifies which model to use
407 |
408 | ```ts
409 | class Car extends Typegoose {}
410 | class Shop extends Typegoose {}
411 |
412 | // in another class
413 | class Another extends Typegoose {
414 | @prop({ required: true, enum: 'Car' | 'Shop' })
415 | which!: string;
416 |
417 | @arrayProp({ itemsRefPath: 'which' })
418 | items?: Ref[];
419 | }
420 | ```
421 |
422 | #### mapProp(options)
423 |
424 | The `mapProp` is a `prop` decorator which makes it possible to create map schema properties.
425 |
426 | The options object accepts `enum` and `default`, just like `prop` decorator. In addition to these the following properties are accepted:
427 |
428 | - `of` : This will tell Typegoose that the Map value consists of primitives (if `String`, `Number`, or other primitive type is given) or this is an array which consists of subdocuments (if it's extending the `Typegoose` class).
429 |
430 | ```ts
431 | class Car extends Typegoose {
432 | @mapProp({ of: Car })
433 | public keys?: Map;
434 | }
435 | ```
436 |
437 | - `mapDefault` : This will set the default value for the map.
438 |
439 | ```ts
440 | enum ProjectState {
441 | WORKING = 'working',
442 | BROKEN = 'broken',
443 | MAINTAINANCE = 'maintainance',
444 | }
445 |
446 | class Car extends Typegoose {
447 | @mapProp({ of: String, enum: ProjectState,mapDefault: { 'MainProject' : ProjectState.WORKING }})
448 | public projects?: Map;
449 | }
450 | ```
451 |
452 | ### Method decorators
453 |
454 | In Mongoose we can attach two types of methods for our schemas: static (model) methods and instance methods. Both of them are supported by Typegoose.
455 |
456 | #### staticMethod
457 |
458 | Static Mongoose methods must be declared with `static` keyword on the Typegoose extending class. This will ensure, that these methods are callable on the Mongoose model (TypeScript won't throw development-time error for unexisting method on model object).
459 |
460 | If we want to use another static method of the model (built-in or created by us) we have to override the `this` in the method using the [type specifying of `this` for functions](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#specifying-the-type-of-this-for-functions). If we don't do this, TypeScript will throw development-time error on missing methods.
461 |
462 | ```ts
463 | @staticMethod
464 | static findByAge(this: ModelType & typeof User, age: number) {
465 | return this.findOne({ age });
466 | }
467 | ```
468 |
469 | Note that the `& typeof T` is only mandatory if we want to use the developer defined static methods inside this static method. If not then the `ModelType` is sufficient, which will be explained in the Types section.
470 |
471 | #### instanceMethod
472 |
473 | Instance methods are on the Mongoose document instances, thus they must be defined as non-static methods. Again if we want to call other instance methods the type of `this` must be redefined to `InstanceType` (see Types).
474 |
475 | ```ts
476 | @instanceMethod
477 | incrementAge(this: InstanceType) {
478 | const age = this.age || 1;
479 | this.age = age + 1;
480 | return this.save();
481 | }
482 | ```
483 |
484 | ### Class decorators
485 |
486 | Mongoose allows the developer to add pre and post [hooks / middlewares](http://mongoosejs.com/docs/middleware.html) to the schema. With this it is possible to add document transformations and observations before or after validation, save and more.
487 |
488 | Typegoose provides this functionality through TypeScript's class decorators.
489 |
490 | #### pre
491 |
492 | We can simply attach a `@pre` decorator to the Typegoose class and define the hook function like you normally would in Mongoose.
493 | (Method supports REGEXP)
494 |
495 | ```ts
496 | @pre('save', function(next) { // or @pre(this: Car, 'save', ...
497 | if (this.model === 'Tesla') {
498 | this.isFast = true;
499 | }
500 | next();
501 | })
502 | class Car extends Typegoose {
503 | @prop({ required: true })
504 | model!: string;
505 |
506 | @prop()
507 | isFast?: boolean;
508 | }
509 | ```
510 |
511 | This will execute the pre-save hook each time a `Car` document is saved. Inside the pre-hook Mongoose binds the actual document to `this`.
512 |
513 | Note that additional typing information is required either by passing the class itself as a type parameter `` or explicity telling TypeScript that `this` is a `Car` (`this: Car`). This will grant typing informations inside the hook function.
514 |
515 | #### post
516 |
517 | Same as `pre`, the `post` hook is also implemented as a class decorator. Usage is equivalent with the one Mongoose provides.
518 | (Method supports REGEXP)
519 |
520 | ```ts
521 | @post('save', (car) => {
522 | if (car.topSpeedInKmH > 300) {
523 | console.log(car.model, 'is fast!');
524 | }
525 | })
526 | class Car extends Typegoose {
527 | @prop({ required: true })
528 | model!: string;
529 |
530 | @prop({ required: true })
531 | topSpeedInKmH!: number;
532 | }
533 | ```
534 |
535 | Of course `this` is not the document in a post hook (see Mongoose docs). Again typing information is required either by explicit parameter typing or by providing a template type.
536 |
537 | #### plugin
538 |
539 | Using the `plugin` decorator enables the developer to attach various Mongoose plugins to the schema. Just like the regular `schema.plugin()` call, the decorator accepts 1 or 2 parameters: the plugin itself, and an optional configuration object. Multiple `plugin` decorator can be used for a single Typegoose class.
540 |
541 | If the plugin enhances the schema with additional properties or instance / static methods this typing information should be added manually to the Typegoose class as well.
542 |
543 | ```ts
544 | import * as findOrCreate from 'mongoose-findorcreate';
545 |
546 | @plugin(findOrCreate)
547 | class User extends Typegoose {
548 | // this isn't the complete method signature, just an example
549 | static findOrCreate(condition: InstanceType):
550 | Promise<{ doc: InstanceType, created: boolean }>;
551 | }
552 |
553 | const UserModel = new User().getModelForClass(User);
554 | UserModel.findOrCreate({ ... }).then(findOrCreateResult => {
555 | ...
556 | });
557 | ```
558 |
559 | #### index
560 |
561 | The `@index` decorator can be used to define advanced index types and index options not available via the
562 | `index` option of the `@prop` property decorator, such as compound indices, GeoJSON index types,
563 | partial indices, expiring documents, etc. Any values supported by
564 | [MongoDB's createIndex()](https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#db.collection.createIndex)
565 | are also valid for `@index`. For more info refer to interface `IndexOptions`
566 |
567 | ```ts
568 | @index({ article: 1, user: 1 }, { unique: true })
569 | @index({ location: '2dsphere' })
570 | @index({ article: 1 }, { partialFilterExpression: { stars: { $gte: 4.5 } } })
571 | export class Location extends Typegoose {
572 | @prop()
573 | article?: number;
574 |
575 | @prop()
576 | user?: number;
577 |
578 | @prop()
579 | stars?: number;
580 |
581 | @arrayProp({ items: Array })
582 | location?: [[Number]]
583 | }
584 | ```
585 |
586 | ### Types
587 |
588 | Some additional types were added to make Typegoose more user friendly.
589 |
590 | #### InstanceType
591 |
592 | This is basically the logical 'and' of the `T` and the `mongoose.Document`, so that both the Mongoose instance properties/functions and the user defined properties/instance methods are available on the instance.
593 |
594 | Note: TypeScript has its own InstanceType, you should import it from Typegoose
595 |
596 | #### ModelType
597 |
598 | This is the logical 'and' of `mongoose.Model>` and `T`, so that the Mongoose model creates `InstanceType` typed instances and all user defined static methods are available on the model.
599 |
600 | #### Ref
601 |
602 | For reference properties:
603 | `Ref` - `T` if populated and `ObjectID` if unpopulated.
604 |
605 | ## Improvements
606 |
607 | * Add frequently used (currently not present) features if needed
608 | * Create more tests (break down current huge one into multiple unit tests)
609 | * Add Tests for:
610 | - Hooks: add hook test for pre & post with error
611 | - test for the errors (if invalid arguments are given)
612 | - improve baseProp `required` handeling ()
613 |
614 | ### Notes
615 |
616 | * `mongoose` is a peer-dependency, and a dev dependency to install it for dev purposes
617 | * Please dont add comments with `+1` or something like that, use the Reactions
618 | * Typegoose **cannot** be used with classes of the same name, it will always return the first build class with that name
619 | * All Models in Typegoose are set to strict by default, and **cant** be changed!
620 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typegoose",
3 | "version": "5.9.1",
4 | "description": "Define Mongoose models using TypeScript classes.",
5 | "main": "lib/typegoose.js",
6 | "engines": {
7 | "node": ">=8.10.0"
8 | },
9 | "files": [
10 | "lib/*.js",
11 | "lib/*.d.ts"
12 | ],
13 | "scripts": {
14 | "start": "npm run build && node ./lib/typegoose.js",
15 | "build": "tsc -p tsconfig.build.json",
16 | "watch": "tsc -w -p tsconfig.build.json",
17 | "lint": "tslint --project tsconfig.json",
18 | "test": "npm run lint && nyc npm run mocha",
19 | "mocha": "npm run build && mocha \"./test/*.ts\" --timeout 15000",
20 | "coverage": "nyc report --reporter=text-lcov | coveralls",
21 | "clean": "rimraf lib && rm .tsbuildinfo && rimraf .nyc_output && rimraf coverage && rimraf doc",
22 | "doc": "typedoc --out ./doc ./src --mode modules"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/szokodiakos/typegoose.git"
27 | },
28 | "types": "lib/typegoose.d.ts",
29 | "typings": "lib/typegoose.d.ts",
30 | "author": "Akos Szokodi (http://codingsans.com)",
31 | "license": "MIT",
32 | "peerDependencies": {
33 | "mongoose": "^5.6.7"
34 | },
35 | "devDependencies": {
36 | "@istanbuljs/nyc-config-typescript": "^0.1.3",
37 | "@types/chai": "^4.1.7",
38 | "@types/chai-as-promised": "^7.1.0",
39 | "@types/mocha": "^5.2.7",
40 | "@types/mongoose": "^5.5.11",
41 | "@types/node": "^8.10.51",
42 | "chai": "^4.2.0",
43 | "chai-as-promised": "^7.1.1",
44 | "coveralls": "^3.0.5",
45 | "mocha": "^6.2.0",
46 | "mongodb-memory-server-global": "^5.1.9",
47 | "mongoose": "^5.6.7",
48 | "mongoose-findorcreate": "3.0.0",
49 | "nyc": "*14.1.1",
50 | "prettier": "^1.18.2",
51 | "prettier-tslint": "^0.4.2",
52 | "rimraf": "*2.6.3",
53 | "source-map-support": "^0.5.12",
54 | "ts-node": "^8.3.0",
55 | "tslint": "*5.18.0",
56 | "tslint-config-prettier": "^1.18.0",
57 | "tslint-eslint-rules": "^5.4.0",
58 | "typedoc": "*0.15.0",
59 | "typescript": "*3.5.3"
60 | },
61 | "dependencies": {
62 | "reflect-metadata": "^0.1.13"
63 | }
64 | }
--------------------------------------------------------------------------------
/src/data.ts:
--------------------------------------------------------------------------------
1 | export const methods = { staticMethods: {}, instanceMethods: {} };
2 | /** Schema Collection */
3 | export const schema = {};
4 | /** Models Collection */
5 | export const models = {};
6 | /** Virtuals Collection */
7 | export const virtuals = {};
8 | /** Hooks Collection */
9 | export const hooks = {};
10 | /** Plugins Collection */
11 | export const plugins = {};
12 | // tslint:disable-next-line: ban-types
13 | export const constructors: { [key: string]: Function } = {};
14 |
--------------------------------------------------------------------------------
/src/errors.ts:
--------------------------------------------------------------------------------
1 | export class InvalidPropError extends Error {
2 | constructor(typeName: string, key: string) {
3 | super(`In property ${key}: ${typeName} is not a primitive type nor a Typegoose schema (Not extending it).`);
4 | }
5 | }
6 |
7 | export class NotNumberTypeError extends Error {
8 | constructor(key: string) {
9 | super(`Type of ${key} property is not a number.`);
10 | }
11 | }
12 |
13 | export class NotStringTypeError extends Error {
14 | constructor(key: string) {
15 | super(`Type of ${key} property is not a string.`);
16 | }
17 | }
18 |
19 | export class NoMetadataError extends Error {
20 | constructor(key: string) {
21 | super(
22 | `There is no metadata for the "${key}" property. ` +
23 | 'Check if emitDecoratorMetadata is enabled in tsconfig.json ' +
24 | 'or check if you\'ve declared a sub document\'s class after usage.'
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/hooks.ts:
--------------------------------------------------------------------------------
1 | import { MongooseDocument } from 'mongoose';
2 |
3 | import { hooks as hooksData } from './data';
4 |
5 | type DocumentMethod = 'init' | 'validate' | 'save' | 'remove';
6 | type QueryMethod =
7 | | 'count'
8 | | 'find'
9 | | 'findOne'
10 | | 'findOneAndRemove'
11 | | 'findOneAndUpdate'
12 | | 'update'
13 | | 'updateOne'
14 | | 'updateMany';
15 | type ModelMethod = 'insertMany';
16 |
17 | type ClassDecorator = (constructor: any) => void;
18 | type HookNextFn = (err?: Error) => void;
19 |
20 | type PreDoneFn = () => void;
21 |
22 | type TypegooseDoc = T & MongooseDocument;
23 |
24 | type DocumentPreSerialFn = (this: TypegooseDoc, next: HookNextFn) => void;
25 | type DocumentPreParallelFn = (this: TypegooseDoc, next: HookNextFn, done: PreDoneFn) => void;
26 |
27 | type SimplePreSerialFn = (next: HookNextFn, docs?: any[]) => void;
28 | type SimplePreParallelFn = (next: HookNextFn, done: PreDoneFn) => void;
29 |
30 | type ModelPostFn = (result: any, next?: HookNextFn) => void;
31 |
32 | type PostNumberResponse = (result: number, next?: HookNextFn) => void;
33 | type PostSingleResponse = (result: TypegooseDoc, next?: HookNextFn) => void;
34 | type PostMultipleResponse = (result: TypegooseDoc[], next?: HookNextFn) => void;
35 |
36 | type PostNumberWithError = (error: Error, result: number, next: HookNextFn) => void;
37 | type PostSingleWithError = (error: Error, result: TypegooseDoc, next: HookNextFn) => void;
38 | type PostMultipleWithError = (error: Error, result: TypegooseDoc[], next: HookNextFn) => void;
39 |
40 | type NumberMethod = 'count';
41 | type SingleMethod = 'findOne' | 'findOneAndRemove' | 'findOneAndUpdate' | DocumentMethod;
42 | type MultipleMethod = 'find' | 'update';
43 |
44 | interface Hooks {
45 | pre(method: DocumentMethod | RegExp, fn: DocumentPreSerialFn): ClassDecorator;
46 | pre(method: DocumentMethod | RegExp, parallel: boolean, fn: DocumentPreParallelFn): ClassDecorator;
47 |
48 | pre(method: QueryMethod | ModelMethod | RegExp, fn: SimplePreSerialFn): ClassDecorator;
49 | pre(method: QueryMethod | ModelMethod | RegExp, parallel: boolean, fn: SimplePreParallelFn): ClassDecorator;
50 |
51 | // I had to disable linter to allow this. I only got proper code completion separating the functions
52 | post(method: NumberMethod | RegExp, fn: PostNumberResponse): ClassDecorator;
53 | // tslint:disable-next-line:unified-signatures
54 | post(method: NumberMethod | RegExp, fn: PostNumberWithError): ClassDecorator;
55 |
56 | post(method: SingleMethod | RegExp, fn: PostSingleResponse): ClassDecorator;
57 | // tslint:disable-next-line:unified-signatures
58 | post(method: SingleMethod | RegExp, fn: PostSingleWithError): ClassDecorator;
59 |
60 | post(method: MultipleMethod | RegExp, fn: PostMultipleResponse): ClassDecorator;
61 | // tslint:disable-next-line:unified-signatures
62 | post(method: MultipleMethod | RegExp, fn: PostMultipleWithError): ClassDecorator;
63 |
64 | post(method: ModelMethod | RegExp, fn: ModelPostFn | PostMultipleResponse): ClassDecorator;
65 | }
66 |
67 | // Note: Documentation for the hooks cant be added without adding it to *every* overload
68 | const hooks: Hooks = {
69 | pre(...args) {
70 | return (constructor: any) => {
71 | addToHooks(constructor.name, 'pre', args);
72 | };
73 | },
74 | post(...args) {
75 | return (constructor: any) => {
76 | addToHooks(constructor.name, 'post', args);
77 | };
78 | },
79 | };
80 |
81 | /**
82 | * Add a hook to the hooks Array
83 | * @param name With wich name should they be registered
84 | * @param hookType What type is it
85 | * @param args All Arguments, that should be passed-throught
86 | */
87 | function addToHooks(name: string, hookType: 'pre' | 'post', args: any) {
88 | if (!hooksData[name]) {
89 | hooksData[name] = { pre: [], post: [] };
90 | }
91 | hooksData[name][hookType].push(args);
92 | }
93 |
94 | export const pre = hooks.pre;
95 | export const post = hooks.post;
96 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | copy-paste from mongodb package (should be same as IndexOptions from 'mongodb')
3 |
4 | */
5 | export interface IndexOptions {
6 | /**
7 | * Mongoose-specific syntactic sugar, uses ms to convert
8 | * expires option into seconds for the expireAfterSeconds in the above link.
9 | */
10 | expires?: string;
11 | /**
12 | * Creates an unique index.
13 | */
14 | unique?: boolean;
15 | /**
16 | * Creates a sparse index.
17 | */
18 | sparse?: boolean;
19 | /**
20 | * Creates the index in the background, yielding whenever possible.
21 | */
22 | background?: boolean;
23 | /**
24 | * A unique index cannot be created on a key that has pre-existing duplicate values.
25 | * If you would like to create the index anyway, keeping the first document the database indexes and
26 | * deleting all subsequent documents that have duplicate value
27 | */
28 | dropDups?: boolean;
29 | /**
30 | * For geo spatial indexes set the lower bound for the co-ordinates.
31 | */
32 | min?: number;
33 | /**
34 | * For geo spatial indexes set the high bound for the co-ordinates.
35 | */
36 | max?: number;
37 | /**
38 | * Specify the format version of the indexes.
39 | */
40 | v?: number;
41 | /**
42 | * Allows you to expire data on indexes applied to a data (MongoDB 2.2 or higher)
43 | */
44 | expireAfterSeconds?: number;
45 | /**
46 | * Override the auto generated index name (useful if the resulting name is larger than 128 bytes)
47 | */
48 | name?: string;
49 | /**
50 | * Creates a partial index based on the given filter object (MongoDB 3.2 or higher)
51 | */
52 | partialFilterExpression?: any;
53 | collation?: object;
54 | default_language?: string;
55 |
56 | lowercase?: boolean; // whether to always call .toLowerCase() on the value
57 | uppercase?: boolean; // whether to always call .toUpperCase() on the value
58 | trim?: boolean; // whether to always call .trim() on the value
59 |
60 | weights?: {
61 | [P in keyof Partial]: number;
62 | };
63 | }
64 |
65 | /**
66 | * Defines an index (most likely compound) for this schema.
67 | * @param fields Wich fields to give the Options
68 | * @param options Options to pass to MongoDB driver's createIndex() function
69 | * @example Example:
70 | * ```
71 | * @index({ article: 1, user: 1 }, { unique: true })
72 | * class Name extends Typegoose {}
73 | * ```
74 | */
75 | export function index(fields: T, options?: IndexOptions) {
76 | return (constructor: any) => {
77 | const indices = Reflect.getMetadata('typegoose:indices', constructor) || [];
78 | indices.push({ fields, options });
79 | Reflect.defineMetadata('typegoose:indices', indices, constructor);
80 | };
81 | }
82 |
--------------------------------------------------------------------------------
/src/method.ts:
--------------------------------------------------------------------------------
1 | import { methods } from './data';
2 |
3 | type MethodType = 'instanceMethods' | 'staticMethods';
4 |
5 | /**
6 | * Base Function for staticMethod & instanceMethod
7 | * @param target
8 | * @param key
9 | * @param descriptor
10 | * @param methodType What type it is
11 | */
12 | function baseMethod(target: any, key: string, descriptor: TypedPropertyDescriptor, methodType: MethodType) {
13 | if (descriptor === undefined) {
14 | descriptor = Object.getOwnPropertyDescriptor(target, key);
15 | }
16 |
17 | let name: any;
18 | if (methodType === 'instanceMethods') {
19 | name = target.constructor.name;
20 | }
21 | if (methodType === 'staticMethods') {
22 | name = target.name;
23 | }
24 |
25 | if (!methods[methodType][name]) {
26 | methods[methodType][name] = {};
27 | }
28 |
29 | const method = descriptor.value;
30 | methods[methodType][name] = {
31 | ...methods[methodType][name],
32 | [key]: method,
33 | };
34 | }
35 |
36 | /**
37 | * Set the function below as a Static Method
38 | * Note: you need to add static before the name
39 | * @example Example:
40 | * ```
41 | * @staticMethod
42 | * public static hello() {}
43 | * ```
44 | * @param target
45 | * @param key
46 | * @param descriptor
47 | */
48 | export function staticMethod(target: any, key: string, descriptor: TypedPropertyDescriptor) {
49 | return baseMethod(target, key, descriptor, 'staticMethods');
50 | }
51 |
52 | /**
53 | * Set the function below as an Instance Method
54 | * @example Example:
55 | * ```
56 | * @instanceMethod
57 | * public hello() {}
58 | * ```
59 | * @param target
60 | * @param key
61 | * @param descriptor
62 | */
63 | export function instanceMethod(target: any, key: string, descriptor: TypedPropertyDescriptor) {
64 | return baseMethod(target, key, descriptor, 'instanceMethods');
65 | }
66 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import { plugins } from './data';
2 |
3 | /**
4 | * Add a Middleware-Plugin
5 | * @param mongoosePlugin The Plugin to plug-in
6 | * @param options Options for the Plugin, if any
7 | */
8 | export function plugin(mongoosePlugin: any, options?: any) {
9 | return (constructor: any) => {
10 | const name: string = constructor.name;
11 | if (!plugins[name]) {
12 | plugins[name] = [];
13 | }
14 | plugins[name].push({ mongoosePlugin, options });
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/src/prop.ts:
--------------------------------------------------------------------------------
1 | import * as mongoose from 'mongoose';
2 |
3 | import { isNullOrUndefined } from 'util';
4 | import { methods, schema, virtuals } from './data';
5 | import { InvalidPropError, NoMetadataError, NotNumberTypeError, NotStringTypeError } from './errors';
6 | import { initAsArray, initAsObject, isNumber, isObject, isPrimitive, isString } from './utils';
7 |
8 | export type Func = (...args: any[]) => any;
9 |
10 | export type RequiredType = boolean | [boolean, string] | string | Func | [Func, string];
11 |
12 | export type ValidatorFunction = (value: any) => boolean | Promise;
13 | export type Validator =
14 | | ValidatorFunction
15 | | RegExp
16 | | {
17 | validator: ValidatorFunction;
18 | message?: string;
19 | };
20 |
21 | export interface BasePropOptions {
22 | /** include this value?
23 | * @default true (Implicitly)
24 | */
25 | select?: boolean;
26 | /** is this value required?
27 | * @default false (Implicitly)
28 | */
29 | required?: RequiredType;
30 | /** Only accept Values from the Enum(|Array) */
31 | enum?: string[] | object;
32 | /** Give the Property a default Value */
33 | default?: any;
34 | /** Give an Validator RegExp or Function */
35 | validate?: Validator | Validator[];
36 | /** should this value be unique?
37 | * @link https://docs.mongodb.com/manual/indexes/#unique-indexes
38 | */
39 | unique?: boolean;
40 | /** should this value get an index?
41 | * @link https://docs.mongodb.com/manual/indexes
42 | */
43 | index?: boolean;
44 | /** @link https://docs.mongodb.com/manual/indexes/#sparse-indexes */
45 | sparse?: boolean;
46 | /** when should this property expire?
47 | * @link https://docs.mongodb.com/manual/tutorial/expire-data
48 | */
49 | expires?: string | number;
50 | /** should subdocuments get their own id?
51 | * @default true (Implicitly)
52 | */
53 | _id?: boolean;
54 | }
55 |
56 | export interface PropOptions extends BasePropOptions {
57 | /** Reference an other Document (you should use Ref as Prop type) */
58 | ref?: any;
59 | /** Take the Path and try to resolve it to a Model */
60 | refPath?: string;
61 | /**
62 | * Give the Property an alias in the output
63 | * Note: you should include the alias as a variable in the class, but not with a prop decorator
64 | * @example
65 | * ```ts
66 | * class Dummy extends Typegoose {
67 | * @prop({ alias: "helloWorld" })
68 | * public hello: string; // normal, with @prop
69 | * public helloWorld: string; // is just for type Completion, will not be included in the DB
70 | * }
71 | * ```
72 | */
73 | alias?: string;
74 | }
75 |
76 | export interface ValidateNumberOptions {
77 | /** The Number must be at least this high */
78 | min?: number | [number, string];
79 | /** The Number can only be lower than this */
80 | max?: number | [number, string];
81 | }
82 |
83 | export interface ValidateStringOptions {
84 | /** Only Allowes if the value matches an RegExp */
85 | match?: RegExp | [RegExp, string];
86 | /** Only Allowes if the value is in the Enum */
87 | enum?: string[];
88 | /** Only Allowes if the value is at least the lenght */
89 | minlength?: number | [number, string];
90 | /** Only Allowes if the value is not longer than the maxlenght */
91 | maxlength?: number | [number, string];
92 | }
93 |
94 | export interface TransformStringOptions {
95 | /** Should it be lowercased before save? */
96 | lowercase?: boolean;
97 | /** Should it be uppercased before save? */
98 | uppercase?: boolean;
99 | /** Should it be trimmed before save? */
100 | trim?: boolean;
101 | }
102 |
103 | export interface VirtualOptions {
104 | ref: string;
105 | localField: string;
106 | foreignField: string;
107 | justOne: boolean;
108 | /** Set to true, when it is an "virtual populate-able" */
109 | overwrite: boolean;
110 | }
111 |
112 | export type PropOptionsWithNumberValidate = PropOptions & ValidateNumberOptions;
113 | export type PropOptionsWithStringValidate = PropOptions & TransformStringOptions & ValidateStringOptions;
114 | export type PropOptionsWithValidate = PropOptionsWithNumberValidate | PropOptionsWithStringValidate | VirtualOptions;
115 |
116 | /** This Enum is meant for baseProp to decide for diffrent props (like if it is an arrayProp or prop or mapProp) */
117 | enum WhatIsIt {
118 | ARRAY = 'Array',
119 | MAP = 'Map',
120 | NONE = ''
121 | }
122 |
123 | /**
124 | * Return true if there are Options
125 | * @param options The raw Options
126 | */
127 | function isWithStringValidate(options: PropOptionsWithStringValidate): boolean {
128 | return !isNullOrUndefined(
129 | options.match
130 | || options.enum
131 | || options.minlength
132 | || options.maxlength
133 | );
134 | }
135 |
136 | /**
137 | * Return true if there are Options
138 | * @param options The raw Options
139 | */
140 | function isWithStringTransform(options: PropOptionsWithStringValidate) {
141 | return !isNullOrUndefined(options.lowercase || options.uppercase || options.trim);
142 | }
143 |
144 | /**
145 | * Return true if there are Options
146 | * @param options The raw Options
147 | */
148 | function isWithNumberValidate(options: PropOptionsWithNumberValidate) {
149 | return !isNullOrUndefined(options.min || options.max);
150 | }
151 |
152 | /**
153 | * Base Function for prop & arrayProp
154 | * @param rawOptions The options (like require)
155 | * @param Type What Type it is
156 | * @param target
157 | * @param key
158 | * @param isArray is it an array?
159 | */
160 | function baseProp(rawOptions: any, Type: any, target: any, key: string, whatis: WhatIsIt = WhatIsIt.NONE): void {
161 | const name: string = target.constructor.name;
162 | const isGetterSetter = Object.getOwnPropertyDescriptor(target, key);
163 | if (isGetterSetter) {
164 | if (isGetterSetter.get) {
165 | if (!virtuals[name]) {
166 | virtuals[name] = {};
167 | }
168 | if (!virtuals[name][key]) {
169 | virtuals[name][key] = {};
170 | }
171 | virtuals[name][key] = {
172 | ...virtuals[name][key],
173 | get: isGetterSetter.get,
174 | options: rawOptions,
175 | };
176 | }
177 |
178 | if (isGetterSetter.set) {
179 | if (!virtuals[name]) {
180 | virtuals[name] = {};
181 | }
182 | if (!virtuals[name][key]) {
183 | virtuals[name][key] = {};
184 | }
185 | virtuals[name][key] = {
186 | ...virtuals[name][key],
187 | set: isGetterSetter.set,
188 | options: rawOptions,
189 | };
190 | }
191 | return;
192 | }
193 |
194 | if (whatis === WhatIsIt.ARRAY) {
195 | initAsArray(name, key);
196 | } else {
197 | initAsObject(name, key);
198 | }
199 |
200 | const ref = rawOptions.ref;
201 | if (typeof ref === 'string') {
202 | schema[name][key] = {
203 | ...schema[name][key],
204 | type: mongoose.Schema.Types.ObjectId,
205 | ref,
206 | };
207 | return;
208 | } else if (ref) {
209 | schema[name][key] = {
210 | ...schema[name][key],
211 | type: mongoose.Schema.Types.ObjectId,
212 | ref: ref.name,
213 | };
214 | return;
215 | }
216 |
217 | const itemsRef = rawOptions.itemsRef;
218 | if (typeof itemsRef === 'string') {
219 | schema[name][key][0] = {
220 | ...schema[name][key][0],
221 | type: mongoose.Schema.Types.ObjectId,
222 | ref: itemsRef,
223 | };
224 | return;
225 | } else if (itemsRef) {
226 | schema[name][key][0] = {
227 | ...schema[name][key][0],
228 | type: mongoose.Schema.Types.ObjectId,
229 | ref: itemsRef.name,
230 | };
231 | return;
232 | }
233 |
234 | const refPath = rawOptions.refPath;
235 | if (refPath && typeof refPath === 'string') {
236 | schema[name][key] = {
237 | ...schema[name][key],
238 | type: mongoose.Schema.Types.ObjectId,
239 | refPath,
240 | };
241 | return;
242 | }
243 |
244 | const itemsRefPath = rawOptions.itemsRefPath;
245 | if (itemsRefPath && typeof itemsRefPath === 'string') {
246 | schema[name][key][0] = {
247 | ...schema[name][key][0],
248 | type: mongoose.Schema.Types.ObjectId,
249 | refPath: itemsRefPath,
250 | };
251 | return;
252 | }
253 |
254 | const enumOption = rawOptions.enum;
255 | if (enumOption) {
256 | if (!Array.isArray(enumOption)) {
257 | rawOptions.enum = Object.keys(enumOption).map(propKey => enumOption[propKey]);
258 | }
259 | }
260 |
261 | const selectOption = rawOptions.select;
262 | if (typeof selectOption === 'boolean') {
263 | schema[name][key] = {
264 | ...schema[name][key],
265 | select: selectOption,
266 | };
267 | }
268 |
269 | // check for validation inconsistencies
270 | if (isWithStringValidate(rawOptions) && !isString(Type)) {
271 | throw new NotStringTypeError(key);
272 | }
273 |
274 | if (isWithNumberValidate(rawOptions) && !isNumber(Type)) {
275 | throw new NotNumberTypeError(key);
276 | }
277 |
278 | // check for transform inconsistencies
279 | if (isWithStringTransform(rawOptions) && !isString(Type)) {
280 | throw new NotStringTypeError(key);
281 | }
282 |
283 | const instance = new Type();
284 | const subSchema = schema[instance.constructor.name];
285 | if (!subSchema && !isPrimitive(Type) && !isObject(Type)) {
286 | throw new InvalidPropError(Type.name, key);
287 | }
288 |
289 | const { ['ref']: r, ['items']: i, ['of']: o, ...options } = rawOptions;
290 | if (isPrimitive(Type)) {
291 | if (whatis === WhatIsIt.ARRAY) {
292 | schema[name][key] = {
293 | ...schema[name][key][0],
294 | ...options,
295 | // HACK: replace this with "[Type]" if https://github.com/Automattic/mongoose/issues/8034 got fixed
296 | type: [Type.name === 'ObjectID' ? 'ObjectId' : Type]
297 | };
298 | return;
299 | }
300 | if (whatis === WhatIsIt.MAP) {
301 | const { mapDefault } = options;
302 | delete options.mapDefault;
303 | schema[name][key] = {
304 | ...schema[name][key],
305 | type: Map,
306 | default: mapDefault,
307 | of: { type: Type, ...options }
308 | };
309 | return;
310 | }
311 | schema[name][key] = {
312 | ...schema[name][key],
313 | ...options,
314 | type: Type
315 | };
316 | return;
317 | }
318 |
319 | // If the 'Type' is not a 'Primitive Type' and no subschema was found treat the type as 'Object'
320 | // so that mongoose can store it as nested document
321 | if (isObject(Type) && !subSchema) {
322 | schema[name][key] = {
323 | ...schema[name][key],
324 | ...options,
325 | type: Object
326 | };
327 | return;
328 | }
329 |
330 | if (whatis === WhatIsIt.ARRAY) {
331 | schema[name][key] = {
332 | ...schema[name][key][0], // [0] is needed, because "initasArray" adds this (empty)
333 | ...options,
334 | type: [{
335 | ...(typeof options._id !== 'undefined' ? { _id: options._id } : {}),
336 | ...subSchema,
337 | }]
338 | };
339 | return;
340 | }
341 |
342 | if (whatis === WhatIsIt.MAP) {
343 | schema[name][key] = {
344 | ...schema[name][key],
345 | type: Map,
346 | ...options
347 | };
348 | schema[name][key].of = {
349 | ...schema[name][key].of,
350 | ...subSchema
351 | };
352 | return;
353 | }
354 | const Schema = mongoose.Schema;
355 |
356 | const supressSubschemaId = rawOptions._id === false;
357 | const virtualSchema = new Schema({ ...subSchema }, supressSubschemaId ? { _id: false } : {});
358 |
359 | const schemaInstanceMethods = methods.instanceMethods[instance.constructor.name];
360 | if (schemaInstanceMethods) {
361 | virtualSchema.methods = schemaInstanceMethods;
362 | }
363 |
364 | schema[name][key] = {
365 | ...schema[name][key],
366 | ...options,
367 | type: virtualSchema
368 | };
369 | return;
370 | }
371 |
372 | /**
373 | * Set Property Options for the property below
374 | * @param options Options
375 | * @public
376 | */
377 | export function prop(options: PropOptionsWithValidate = {}) {
378 | return (target: any, key: string) => {
379 | const Type = (Reflect as any).getMetadata('design:type', target, key);
380 |
381 | if (!Type) {
382 | throw new NoMetadataError(key);
383 | }
384 |
385 | baseProp(options, Type, target, key, WhatIsIt.NONE);
386 | };
387 | }
388 |
389 | export interface ArrayPropOptions extends BasePropOptions {
390 | /** What array is it?
391 | * Note: this is only needed because Reflect & refelact Metadata cant give an accurate Response for an array
392 | */
393 | items?: any;
394 | /** Same as {@link PropOptions.ref}, only that it is for an array */
395 | itemsRef?: any;
396 | /** Same as {@link PropOptions.refPath}, only that it is for an array */
397 | itemsRefPath?: any;
398 | }
399 | export interface MapPropOptions extends BasePropOptions {
400 | of?: any;
401 | mapDefault?: any;
402 | }
403 |
404 | /**
405 | * Set Property(that are Maps) Options for the property below
406 | * @param options Options for the Map
407 | * @public
408 | */
409 | export function mapProp(options: MapPropOptions) {
410 | return (target: any, key: string) => {
411 | const Type = options.of;
412 | baseProp(options, Type, target, key, WhatIsIt.MAP);
413 | };
414 | }
415 | /**
416 | * Set Property(that are Arrays) Options for the property below
417 | * @param options Options
418 | * @public
419 | */
420 | export function arrayProp(options: ArrayPropOptions) {
421 | return (target: any, key: string) => {
422 | const Type = options.items;
423 | baseProp(options, Type, target, key, WhatIsIt.ARRAY);
424 | };
425 | }
426 |
427 | /**
428 | * Reference another Model
429 | */
430 | export type Ref = T | mongoose.Schema.Types.ObjectId;
431 |
--------------------------------------------------------------------------------
/src/typegoose.ts:
--------------------------------------------------------------------------------
1 | /* imports */
2 | import * as mongoose from 'mongoose';
3 | import 'reflect-metadata';
4 |
5 | import { deprecate } from 'util';
6 | import { constructors, hooks, methods, models, plugins, schema, virtuals } from './data';
7 |
8 | /* exports */
9 | export * from './method';
10 | export * from './prop';
11 | export * from './hooks';
12 | export * from './plugin';
13 | export * from '.';
14 | export * from './typeguards';
15 | export { getClassForDocument } from './utils';
16 |
17 | deprecate(() => undefined, 'This Package got moved, please use `@hasezoey/typegoose` | github:hasezoey/typegoose')();
18 |
19 | export type InstanceType = T & mongoose.Document;
20 | export type ModelType = mongoose.Model> & T;
21 |
22 | export interface GetModelForClassOptions {
23 | /** An Existing Mongoose Connection */
24 | existingMongoose?: mongoose.Mongoose;
25 | /** Supports all Mongoose's Schema Options */
26 | schemaOptions?: mongoose.SchemaOptions;
27 | /** An Existing Connection */
28 | existingConnection?: mongoose.Connection;
29 | }
30 |
31 | /**
32 | * Main Class
33 | */
34 | export class Typegoose {
35 | /**
36 | * Get a Model for a Class
37 | * Executes .setModelForClass if it cant find it already
38 | * @param t The uninitialized Class
39 | * @param __namedParameters The Options
40 | * @param existingMongoose An Existing Mongoose Connection
41 | * @param schemaOptions Supports all Mongoose's Schema Options
42 | * @param existingConnection An Existing Connection
43 | * @returns The Model
44 | * @public
45 | */
46 | public getModelForClass(
47 | t: T,
48 | { existingMongoose, schemaOptions, existingConnection }: GetModelForClassOptions = {}
49 | ) {
50 | const name = this.constructor.name;
51 | if (!models[name]) {
52 | this.setModelForClass(t, {
53 | existingMongoose,
54 | schemaOptions,
55 | existingConnection,
56 | });
57 | }
58 |
59 | return models[name] as ModelType & T;
60 | }
61 |
62 | /**
63 | * Builds the Schema & The Model
64 | * @param t The uninitialized Class
65 | * @param __namedParameters The Options
66 | * @param existingMongoose An Existing Mongoose Connection
67 | * @param schemaOptions Supports all Mongoose's Schema Options
68 | * @param existingConnection An Existing Connection
69 | * @returns The Model
70 | * @public
71 | */
72 | public setModelForClass(
73 | t: T,
74 | { existingMongoose, schemaOptions, existingConnection }: GetModelForClassOptions = {}
75 | ) {
76 | const name = this.constructor.name;
77 |
78 | const sch = this.buildSchema(t, { existingMongoose, schemaOptions });
79 |
80 | let model = mongoose.model.bind(mongoose);
81 | if (existingConnection) {
82 | model = existingConnection.model.bind(existingConnection);
83 | } else if (existingMongoose) {
84 | model = existingMongoose.model.bind(existingMongoose);
85 | }
86 |
87 | models[name] = model(name, sch);
88 | constructors[name] = this.constructor;
89 |
90 | return models[name] as ModelType & T;
91 | }
92 |
93 | /**
94 | * Generates a Mongoose schema out of class props, iterating through all parents
95 | * @param t The not initialized Class
96 | * @param schemaOptions Options for the Schema
97 | * @returns Returns the Build Schema
98 | */
99 | public buildSchema(t: T, { schemaOptions }: GetModelForClassOptions = {}) {
100 | const name = this.constructor.name;
101 |
102 | // get schema of current model
103 | let sch = _buildSchema(t, name, schemaOptions);
104 | /** Parent Constructor */
105 | let parentCtor = Object.getPrototypeOf(this.constructor.prototype).constructor;
106 | // iterate trough all parents
107 | while (parentCtor && parentCtor.name !== 'Typegoose' && parentCtor.name !== 'Object') {
108 | // extend schema
109 | sch = _buildSchema(t, parentCtor.name, schemaOptions, sch);
110 | // next parent
111 | parentCtor = Object.getPrototypeOf(parentCtor.prototype).constructor;
112 | }
113 | return sch;
114 | }
115 | }
116 |
117 | /**
118 | * Private schema builder out of class props
119 | * -> If you discover this, dont use this function, use Typegoose.buildSchema!
120 | * @param t The not initialized Class
121 | * @param name The Name to save the Schema Under (Mostly Constructor.name)
122 | * @param schemaOptions Options for the Schema
123 | * @param sch Already Existing Schema?
124 | * @returns Returns the Build Schema
125 | * @private
126 | */
127 | function _buildSchema(t: T, name: string, schemaOptions: any, sch?: mongoose.Schema) {
128 | /** Simplify the usage */
129 | const Schema = mongoose.Schema;
130 |
131 | if (!sch) {
132 | sch = schemaOptions ? new Schema(schema[name], schemaOptions) : new Schema(schema[name]);
133 | } else {
134 | sch.add(schema[name]);
135 | }
136 |
137 | /** Simplify the usage */
138 | const staticMethods = methods.staticMethods[name];
139 | if (staticMethods) {
140 | sch.statics = Object.assign(staticMethods, sch.statics || {});
141 | } else {
142 | sch.statics = sch.statics || {};
143 | }
144 |
145 | /** Simplify the usage */
146 | const instanceMethods = methods.instanceMethods[name];
147 | if (instanceMethods) {
148 | sch.methods = Object.assign(instanceMethods, sch.methods || {});
149 | } else {
150 | sch.methods = sch.methods || {};
151 | }
152 |
153 | if (hooks[name]) { // checking to just dont get errors like "hooks[name].pre is not defined"
154 | hooks[name].pre.forEach(preHookArgs => {
155 | (sch as any).pre(...preHookArgs);
156 | });
157 | hooks[name].post.forEach(postHookArgs => {
158 | (sch as any).post(...postHookArgs);
159 | });
160 | }
161 |
162 | if (plugins[name]) { // same as the "if (hooks[name])"
163 | for (const plugin of plugins[name]) {
164 | sch.plugin(plugin.mongoosePlugin, plugin.options);
165 | }
166 | }
167 |
168 | /** Simplify the usage */
169 | const getterSetters = virtuals[name];
170 | if (getterSetters) {
171 | for (const key of Object.keys(getterSetters)) {
172 | if (getterSetters[key].options && getterSetters[key].options.overwrite) {
173 | sch.virtual(key, getterSetters[key].options);
174 | } else {
175 | if (getterSetters[key].get) {
176 | sch.virtual(key, getterSetters[key].options).get(getterSetters[key].get);
177 | }
178 |
179 | if (getterSetters[key].set) {
180 | sch.virtual(key, getterSetters[key].options).set(getterSetters[key].set);
181 | }
182 | }
183 | }
184 | }
185 |
186 | /** Get Metadata for indices */
187 | const indices = Reflect.getMetadata('typegoose:indices', t) || [];
188 | for (const index of indices) {
189 | sch.index(index.fields, index.options);
190 | }
191 |
192 | return sch;
193 | }
194 |
--------------------------------------------------------------------------------
/src/typeguards.ts:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import * as mongoose from 'mongoose';
4 |
5 | import { Ref } from './prop';
6 | import { InstanceType } from './typegoose';
7 |
8 | /**
9 | * Check if the given document is already populated
10 | * @param doc The Ref with uncertain type
11 | */
12 | export function isDocument(doc: Ref): doc is InstanceType {
13 | return doc instanceof mongoose.Model;
14 | }
15 |
16 | /**
17 | * Check if the given array is already populated
18 | * @param docs The Array of Refs with uncertain type
19 | */
20 | export function isDocumentArray(docs: Ref[]): docs is InstanceType[] {
21 | return Array.isArray(docs) && docs.every(v => isDocument(v));
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import * as mongoose from 'mongoose';
2 |
3 | import { constructors, schema } from './data';
4 |
5 | /**
6 | * Returns true, if it includes the Type
7 | * @param Type The Type
8 | * @returns true, if it includes it
9 | */
10 | export function isPrimitive(Type: any): boolean {
11 | return ['String', 'Number', 'Boolean', 'Date', 'Decimal128', 'ObjectID'].includes(Type.name);
12 | }
13 |
14 | /**
15 | * Returns true, if it is an Object
16 | * @param Type The Type
17 | * @returns true, if it is an Object
18 | */
19 | export function isObject(Type: any): boolean {
20 | let prototype = Type.prototype;
21 | let name = Type.name;
22 | while (name) {
23 | if (name === 'Object') {
24 | return true;
25 | }
26 | prototype = Object.getPrototypeOf(prototype);
27 | name = prototype ? prototype.constructor.name : null;
28 | }
29 |
30 | return false;
31 | }
32 |
33 | /**
34 | * Returns true, if it is an Number
35 | * @param Type The Type
36 | * @returns true, if it is an Number
37 | */
38 | export function isNumber(Type: any): boolean {
39 | return Type.name === 'Number';
40 | }
41 |
42 | /**
43 | * Returns true, if it is an String
44 | * @param Type The Type
45 | * @returns true, if it is an String
46 | */
47 | export function isString(Type: any): boolean {
48 | return Type.name === 'String';
49 | }
50 |
51 | /**
52 | * Initialize as Object
53 | * @param name
54 | * @param key
55 | */
56 | export function initAsObject(name, key): void {
57 | if (!schema[name]) {
58 | schema[name] = {};
59 | }
60 | if (!schema[name][key]) {
61 | schema[name][key] = {};
62 | }
63 | }
64 |
65 | /**
66 | * Initialize as Array
67 | * @param name
68 | * @param key
69 | */
70 | export function initAsArray(name: any, key: any): void {
71 | if (!schema[name]) {
72 | schema[name] = {};
73 | }
74 | if (!schema[name][key]) {
75 | schema[name][key] = [{}];
76 | }
77 | }
78 |
79 | /**
80 | * Get the Class for a given Document
81 | * @param document The Document
82 | */
83 | export function getClassForDocument(document: mongoose.Document): any {
84 | const modelName = (document.constructor as mongoose.Model).modelName;
85 | return constructors[modelName];
86 | }
87 |
--------------------------------------------------------------------------------
/test/config_default.json:
--------------------------------------------------------------------------------
1 | {
2 | "Memory": true,
3 | "DataBase": "typegooseTest",
4 | "Port": 27017,
5 | "Auth": {
6 | "DB": "",
7 | "User": "",
8 | "Passwd": ""
9 | },
10 | "IP": "localhost"
11 | }
12 |
--------------------------------------------------------------------------------
/test/enums/genders.ts:
--------------------------------------------------------------------------------
1 | export enum Genders {
2 | MALE = 'male',
3 | FEMALE = 'female'
4 | }
5 |
--------------------------------------------------------------------------------
/test/enums/role.ts:
--------------------------------------------------------------------------------
1 | export enum Role {
2 | Admin = 'admin',
3 | User = 'user',
4 | Guest = 'guest',
5 | }
6 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { use } from 'chai';
2 | import * as cap from 'chai-as-promised';
3 |
4 | import { suite as BigUserTest } from './tests/biguser.test';
5 | import { suite as IndexTests } from './tests/db_index.test';
6 | import { suite as GCFDTest } from './tests/getClassForDocument.test';
7 | import { suite as HookTest } from './tests/hooks.test';
8 | import { suite as ShouldAddTest } from './tests/shouldAdd.test';
9 | import { suite as StringValidatorTests } from './tests/stringValidator.test';
10 | import { suite as TypeguardsTest } from './tests/typeguards.test';
11 |
12 | import { connect, disconnect } from './utils/mongooseConnect';
13 |
14 | use(cap);
15 |
16 | describe('Typegoose', () => {
17 | before(() => connect());
18 | after(() => disconnect());
19 |
20 | describe('BigUser', BigUserTest.bind(this));
21 |
22 | describe('Hooks', HookTest.bind(this));
23 |
24 | describe('Type guards', TypeguardsTest.bind(this));
25 |
26 | describe('Should add', ShouldAddTest.bind(this));
27 |
28 | describe('Indexes', IndexTests.bind(this));
29 |
30 | describe('String Validators', StringValidatorTests.bind(this));
31 |
32 | describe('getClassForDocument()', GCFDTest.bind(this));
33 | });
34 |
--------------------------------------------------------------------------------
/test/models/PersistentModel.ts:
--------------------------------------------------------------------------------
1 | import { arrayProp, instanceMethod, InstanceType, prop, Ref, staticMethod, Typegoose } from '../../src/typegoose';
2 | import { Car } from './car';
3 |
4 | export abstract class PersistentModel extends Typegoose {
5 | @prop()
6 | public createdAt: Date;
7 |
8 | @arrayProp({ itemsRef: Car })
9 | public cars?: Ref[];
10 |
11 | // define an 'instanceMethod' that will be overwritten
12 | @instanceMethod
13 | public getClassName() {
14 | return 'PersistentModel';
15 | }
16 |
17 | // define an 'instanceMethod' that will be overwritten
18 | @staticMethod
19 | public static getStaticName() {
20 | return 'PersistentModel';
21 | }
22 |
23 | // define an instanceMethod that is called by the derived class
24 | @instanceMethod
25 | public addCar(this: InstanceType, car: Car) {
26 | if (!this.cars) {
27 | this.cars = [];
28 | }
29 |
30 | this.cars.push(car);
31 |
32 | return this.save();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/models/alias.ts:
--------------------------------------------------------------------------------
1 | import { prop, Typegoose } from '../../src/typegoose';
2 |
3 | export class Alias extends Typegoose {
4 | @prop({ required: true })
5 | public normalProp: string;
6 |
7 | @prop({ required: true, alias: 'aliasProp' })
8 | public alias: string;
9 | public aliasProp: string; // its just for type completion
10 | }
11 |
12 | export const model = new Alias().getModelForClass(Alias);
13 |
--------------------------------------------------------------------------------
/test/models/car.ts:
--------------------------------------------------------------------------------
1 | import * as mongoose from 'mongoose';
2 |
3 | import { pre, prop, Typegoose } from '../../src/typegoose';
4 |
5 | @pre('save', function (next) {
6 | if (this.model === 'Trabant') {
7 | this.isSedan = true;
8 | }
9 | next();
10 | })
11 | export class Car extends Typegoose {
12 | @prop({ required: true })
13 | public model: string;
14 |
15 | @prop({ lowercase: true })
16 | public version: string;
17 |
18 | @prop()
19 | public isSedan?: boolean;
20 |
21 | @prop({ required: true })
22 | public price: mongoose.Types.Decimal128;
23 |
24 | @prop()
25 | public someId: mongoose.Types.ObjectId;
26 | }
27 |
28 | export const model = new Car().getModelForClass(Car);
29 |
--------------------------------------------------------------------------------
/test/models/hook1.ts:
--------------------------------------------------------------------------------
1 | import { isArray } from 'util';
2 | import { InstanceType, post, pre, prop, Typegoose } from '../../src/typegoose';
3 |
4 | @pre('save', function () {
5 | if (this.isModified('shape')) {
6 | this.shape = 'newShape';
7 | } else {
8 | this.shape = 'oldShape';
9 | }
10 | })
11 | @pre(/^update/, function () {
12 | if (isArray(this)) {
13 | this.forEach((v) => v.update({ shape: 'REGEXP_PRE' })); // i know this is inefficient
14 | } else {
15 | this.update({ shape: 'REGEXP_PRE' });
16 | }
17 | })
18 | @post(/^find/, (doc: InstanceType | InstanceType[]) => {
19 | if (isArray(doc)) {
20 | doc.forEach((v) => v.material = 'REGEXP_POST');
21 | } else {
22 | doc.material = 'REGEXP_POST';
23 | }
24 | })
25 | export class Hook extends Typegoose {
26 | @prop({ required: true })
27 | public material: string;
28 |
29 | @prop()
30 | public shape?: string;
31 | }
32 |
33 | export const model = new Hook().getModelForClass(Hook);
34 |
--------------------------------------------------------------------------------
/test/models/hook2.ts:
--------------------------------------------------------------------------------
1 | import { post, pre, prop, Typegoose } from '../../src/typegoose';
2 |
3 | @pre('save', function (next) {
4 | this.text = 'saved';
5 |
6 | next();
7 | })
8 | // eslint-disable-next-line only-arrow-functions (need `this` in hook)
9 | @pre('updateMany', async function () {
10 | this._update.text = 'updateManied';
11 | })
12 | @post('find', (result) => {
13 | result[0].text = 'changed in post find hook';
14 | })
15 | @post('findOne', (result) => {
16 | result.text = 'changed in post findOne hook';
17 | })
18 | export class Dummy extends Typegoose {
19 | @prop()
20 | public text: string;
21 | }
22 |
23 | export const model = new Dummy().getModelForClass(Dummy);
24 |
--------------------------------------------------------------------------------
/test/models/indexweigths.ts:
--------------------------------------------------------------------------------
1 | import { arrayProp, index, prop, Typegoose } from '../../src/typegoose';
2 |
3 | // using examples from https://docs.mongodb.com/manual/tutorial/control-results-of-text-search/
4 | @index({ content: 'text', about: 'text', keywords: 'text' }, {
5 | weights: {
6 | content: 10,
7 | keywords: 3
8 | },
9 | name: 'TextIndex'
10 | })
11 | export class IndexWeights extends Typegoose {
12 | @prop({ required: true })
13 | public content: string;
14 |
15 | @prop({ required: true })
16 | public about: string;
17 |
18 | @arrayProp({ required: true, items: String })
19 | public keywords: string[];
20 | }
21 |
22 | export const model = new IndexWeights().getModelForClass(IndexWeights);
23 |
--------------------------------------------------------------------------------
/test/models/internet-user.ts:
--------------------------------------------------------------------------------
1 | import { mapProp, prop, Typegoose } from '../../src/typegoose';
2 |
3 | export class SideNote {
4 | @prop()
5 | public content: string;
6 |
7 | @prop()
8 | public link?: string;
9 | }
10 |
11 | enum ProjectValue {
12 | WORKING = 'working',
13 | UNDERDEVELOPMENT = 'underdevelopment',
14 | BROKEN = 'broken',
15 | }
16 |
17 | class InternetUser extends Typegoose {
18 | @mapProp({ of: String, mapDefault: {} })
19 | public socialNetworks?: Map;
20 |
21 | @mapProp({ of: SideNote })
22 | public sideNotes?: Map;
23 |
24 | @mapProp({ of: String, enum: ProjectValue })
25 | public projects: Map;
26 | }
27 |
28 | export const model = new InternetUser().getModelForClass(InternetUser);
29 |
--------------------------------------------------------------------------------
/test/models/inventory.ts:
--------------------------------------------------------------------------------
1 | // Tests for discriminators and refPaths
2 | import { arrayProp, prop, Ref, Typegoose } from '../../src/typegoose';
3 |
4 | export class Scooter extends Typegoose {
5 | @prop()
6 | public makeAndModel?: string;
7 | }
8 |
9 | export class Beverage extends Typegoose {
10 | @prop({ default: false })
11 | public isSugarFree?: boolean;
12 |
13 | @prop({ default: false })
14 | public isDecaf?: boolean;
15 | }
16 |
17 | export class Inventory extends Typegoose {
18 | @prop({ default: 100 })
19 | public count?: number;
20 |
21 | @prop({ default: 1.00 })
22 | public value?: number;
23 |
24 | @prop({ required: true, enum: ['Beverage', 'Scooter'] })
25 | public refItemPathName!: string;
26 |
27 | @prop()
28 | public name?: string;
29 |
30 | @prop({ required: true, refPath: 'refItemPathName' })
31 | public kind!: Ref;
32 |
33 | @arrayProp({ required: true, itemsRefPath: 'refItemPathName' })
34 | public irp!: Ref[];
35 | }
36 |
37 | export class TestIRPbyString extends Typegoose {
38 | @prop({ required: true })
39 | public normalProp!: string;
40 |
41 | @arrayProp({ required: true, itemsRef: 'Beverage' })
42 | public bev!: Ref[];
43 | }
44 |
45 | export const ScooterModel = new Scooter().getModelForClass(Scooter);
46 | export const BeverageModel = new Beverage().getModelForClass(Beverage);
47 | export const InventoryModel = new Inventory().getModelForClass(Inventory);
48 | export const TestIRPbyStringModel = new TestIRPbyString().getModelForClass(TestIRPbyString);
49 |
--------------------------------------------------------------------------------
/test/models/job.ts:
--------------------------------------------------------------------------------
1 | import { instanceMethod, prop } from '../../src/typegoose';
2 |
3 | export class JobType {
4 | @prop({ required: true })
5 | public field: string;
6 |
7 | @prop({ required: true })
8 | public salery: number;
9 | }
10 |
11 | export class Job {
12 | @prop()
13 | public title?: string;
14 |
15 | @prop()
16 | public position?: string;
17 |
18 | @prop({ required: true, default: Date.now })
19 | public startedAt?: Date;
20 |
21 | @prop({ _id: false })
22 | public jobType?: JobType;
23 |
24 | @instanceMethod
25 | public titleInUppercase?() {
26 | return this.title.toUpperCase();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/test/models/nested-object.ts:
--------------------------------------------------------------------------------
1 | import { prop, Typegoose } from '../../src/typegoose';
2 |
3 | export class AddressNested {
4 | public street: string;
5 |
6 | constructor(street: string) {
7 | this.street = street;
8 | }
9 | }
10 |
11 | export class PersonNested extends Typegoose {
12 | @prop()
13 | public name: string;
14 |
15 | @prop()
16 | public address: AddressNested;
17 |
18 | @prop()
19 | public moreAddresses: AddressNested[] = [];
20 | }
21 |
22 | export const PersonNestedModel = new PersonNested().getModelForClass(PersonNested);
23 |
--------------------------------------------------------------------------------
/test/models/person.ts:
--------------------------------------------------------------------------------
1 | import { instanceMethod, pre, prop, staticMethod } from '../../src/typegoose';
2 | import { PersistentModel } from './PersistentModel';
3 |
4 | // add a pre-save hook to PersistentModel
5 | @pre('save', function (next) {
6 | if (!this.createdAt) {
7 | this.createdAt = new Date();
8 | }
9 | next();
10 | })
11 | export class Person extends PersistentModel {
12 | // add new property
13 | @prop({ required: true, validate: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/ })
14 | public email: string;
15 |
16 | // override instanceMethod
17 | @instanceMethod
18 | public getClassName() {
19 | return 'Person';
20 | }
21 |
22 | // override staticMethod
23 | @staticMethod
24 | public static getStaticName() {
25 | return 'Person';
26 | }
27 | }
28 |
29 | export const model = new Person().getModelForClass(Person);
30 |
--------------------------------------------------------------------------------
/test/models/rating.ts:
--------------------------------------------------------------------------------
1 | import { index } from '../../src';
2 | import { arrayProp, Ref } from '../../src/prop';
3 | import { prop, Typegoose } from '../../src/typegoose';
4 | import { Car } from './car';
5 | import { User } from './user';
6 |
7 | @index({ car: 1, user: 1 }, { unique: true })
8 | @index({ location: '2dsphere' })
9 | export class Rating extends Typegoose {
10 | @prop({ ref: Car })
11 | public car: Ref;
12 |
13 | @prop({ ref: User })
14 | public user: Ref;
15 |
16 | @prop()
17 | public stars: number;
18 |
19 | @arrayProp({ items: Array })
20 | public location: [[number]];
21 | }
22 |
23 | export const model = new Rating().getModelForClass(Rating);
24 |
--------------------------------------------------------------------------------
/test/models/select.ts:
--------------------------------------------------------------------------------
1 | import { prop, Typegoose } from '../../src/typegoose';
2 |
3 | export enum SelectStrings {
4 | test1 = 'testing 1 should not default include',
5 | test2 = 'testing 2 should default include',
6 | test3 = 'testing 3 should not default include'
7 | }
8 |
9 | // Note: "select: true" is just to test if it works, and dosnt give an error
10 | export class Select extends Typegoose {
11 | @prop({ required: true, default: SelectStrings.test1, select: false })
12 | public test1!: string;
13 |
14 | @prop({ required: true, default: SelectStrings.test2, select: true })
15 | public test2!: string;
16 |
17 | @prop({ required: true, default: SelectStrings.test3, select: false })
18 | public test3!: string;
19 | }
20 |
21 | export const model = new Select().getModelForClass(Select);
22 |
--------------------------------------------------------------------------------
/test/models/stringValidators.ts:
--------------------------------------------------------------------------------
1 | import { prop, Typegoose } from '../../src/typegoose';
2 |
3 | export class StringValidators extends Typegoose {
4 | @prop({ maxlength: 3 })
5 | public maxLength: string;
6 |
7 | @prop({ minlength: 10 })
8 | public minLength: string;
9 |
10 | @prop({ trim: true })
11 | public trimmed: string;
12 |
13 | @prop({ uppercase: true })
14 | public uppercased: string;
15 |
16 | @prop({ lowercase: true })
17 | public lowercased: string;
18 |
19 | @prop({ enum: ['one', 'two', 'three'] })
20 | public enumed: string;
21 | }
22 |
23 | export const model = new StringValidators().getModelForClass(StringValidators);
24 |
--------------------------------------------------------------------------------
/test/models/user.ts:
--------------------------------------------------------------------------------
1 | import * as findOrCreate from 'mongoose-findorcreate';
2 | import {
3 | arrayProp,
4 | instanceMethod,
5 | InstanceType,
6 | ModelType,
7 | plugin,
8 | prop,
9 | Ref,
10 | staticMethod,
11 | Typegoose,
12 | } from '../../src/typegoose';
13 | import { Genders } from '../enums/genders';
14 | import { Role } from '../enums/role';
15 | import { Car } from './car';
16 | import { Job } from './job';
17 |
18 | export interface FindOrCreateResult {
19 | created: boolean;
20 | doc: InstanceType;
21 | }
22 |
23 | @plugin(findOrCreate)
24 | export class User extends Typegoose {
25 | @prop({ required: true })
26 | public firstName: string;
27 |
28 | @prop({ required: true })
29 | public lastName: string;
30 |
31 | @prop()
32 | public get fullName() {
33 | return `${this.firstName} ${this.lastName}`;
34 | }
35 | public set fullName(full) {
36 | const split = full.split(' ');
37 | this.firstName = split[0];
38 | this.lastName = split[1];
39 | }
40 |
41 | @prop({ default: 'Nothing' })
42 | public nick?: string;
43 |
44 | @prop({ index: true, unique: true })
45 | public uniqueId?: string;
46 |
47 | @prop({ unique: true, sparse: true })
48 | public username?: string;
49 |
50 | @prop({ expires: '24h' })
51 | public expireAt?: Date;
52 |
53 | @prop({ min: 10, max: 21 })
54 | public age?: number;
55 |
56 | @prop({ enum: Genders, required: true })
57 | public gender: Genders;
58 |
59 | @prop({ enum: Role })
60 | public role: Role;
61 |
62 | @arrayProp({ items: String, enum: Role, default: [Role.Guest] })
63 | public roles: Role[];
64 |
65 | @prop()
66 | public job?: Job;
67 |
68 | @prop({ ref: Car })
69 | public car?: Ref;
70 |
71 | @arrayProp({ items: String, required: true })
72 | public languages: string[];
73 |
74 | @arrayProp({ items: Job })
75 | public previousJobs?: Job[];
76 |
77 | @arrayProp({ itemsRef: Car })
78 | public previousCars?: Ref[];
79 |
80 | @staticMethod
81 | public static findByAge(this: ModelType & typeof User, age: number) {
82 | return this.findOne({ age });
83 | }
84 |
85 | @instanceMethod
86 | public incrementAge(this: InstanceType) {
87 | const age = this.age || 1;
88 | this.age = age + 1;
89 | return this.save();
90 | }
91 |
92 | @instanceMethod
93 | public addLanguage(this: InstanceType) {
94 | this.languages.push('Hungarian');
95 |
96 | return this.save();
97 | }
98 |
99 | @instanceMethod
100 | public addJob(this: InstanceType, job: Partial = {}) {
101 | this.previousJobs.push(job);
102 |
103 | return this.save();
104 | }
105 |
106 | public static findOrCreate: (condition: any) => Promise>;
107 | }
108 |
109 | export const model = new User().getModelForClass(User);
110 |
--------------------------------------------------------------------------------
/test/models/userRefs.ts:
--------------------------------------------------------------------------------
1 | import { arrayProp, prop, Ref, Typegoose } from '../../src/typegoose';
2 |
3 | export class UserRef extends Typegoose {
4 | @prop({ ref: UserRef, default: null })
5 | public master?: Ref;
6 |
7 | @arrayProp({ itemsRef: UserRef, default: [] })
8 | public subAccounts!: Ref[];
9 |
10 | @prop({ required: true })
11 | public name!: string;
12 | }
13 |
14 | export const UserRefModel = new UserRef().getModelForClass(UserRef);
15 |
--------------------------------------------------------------------------------
/test/models/virtualprop.ts:
--------------------------------------------------------------------------------
1 | import { prop, Ref, Typegoose } from '../../src/typegoose';
2 |
3 | export class Virtual extends Typegoose {
4 | @prop({ required: true })
5 | public dummyVirtual?: string;
6 |
7 | @prop({ ref: 'VirtualSub', foreignField: 'virtual', localField: '_id', justOne: false, overwrite: true })
8 | public get virtualSubs() { return undefined; }
9 | }
10 |
11 | export class VirtualSub extends Typegoose {
12 | @prop({ required: true, ref: Virtual })
13 | public virtual: Ref;
14 |
15 | @prop({ required: true })
16 | public dummy: string;
17 | }
18 |
--------------------------------------------------------------------------------
/test/tests/biguser.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as mongoose from 'mongoose';
3 |
4 | import { Genders } from '../enums/genders';
5 | import { Role } from '../enums/role';
6 | import { model as Car } from '../models/car';
7 | import { model as User } from '../models/user';
8 |
9 | /**
10 | * Function to pass into describe
11 | * ->Important: you need to always bind this
12 | * @example
13 | * ```
14 | * import { suite as BigUserTest } from './biguser.test'
15 | * ...
16 | * describe('BigUser', BigUserTest.bind(this));
17 | * ...
18 | * ```
19 | */
20 | export function suite() {
21 | it('should create a User with connections', async () => {
22 | const [tesla, trabant, zastava] = await Car.create([{
23 | model: 'Tesla',
24 | version: 'ModelS',
25 | price: mongoose.Types.Decimal128.fromString('50123.25')
26 | }, {
27 | model: 'Trabant',
28 | price: mongoose.Types.Decimal128.fromString('28189.25')
29 | }, {
30 | model: 'Zastava',
31 | price: mongoose.Types.Decimal128.fromString('1234.25')
32 | }]);
33 |
34 | const user = await User.create({
35 | _id: mongoose.Types.ObjectId(),
36 | firstName: 'John',
37 | lastName: 'Doe',
38 | age: 20,
39 | uniqueId: 'john-doe-20',
40 | gender: Genders.MALE,
41 | role: Role.User,
42 | job: {
43 | title: 'Developer',
44 | position: 'Lead',
45 | jobType: {
46 | salery: 5000,
47 | field: 'IT',
48 | },
49 | },
50 | car: tesla.id,
51 | languages: ['english', 'typescript'],
52 | previousJobs: [{
53 | title: 'Janitor',
54 | }, {
55 | title: 'Manager',
56 | }],
57 | previousCars: [trabant.id, zastava.id],
58 | });
59 |
60 | {
61 | const foundUser = await User
62 | .findById(user.id)
63 | .populate('car previousCars')
64 | .exec();
65 |
66 | expect(foundUser).to.have.property('nick', 'Nothing');
67 | expect(foundUser).to.have.property('firstName', 'John');
68 | expect(foundUser).to.have.property('lastName', 'Doe');
69 | expect(foundUser).to.have.property('uniqueId', 'john-doe-20');
70 | expect(foundUser).to.have.property('age', 20);
71 | expect(foundUser).to.have.property('gender', Genders.MALE);
72 | expect(foundUser).to.have.property('role', Role.User);
73 | expect(foundUser).to.have.property('roles').to.have.length(1).to.include(Role.Guest);
74 | expect(foundUser).to.have.property('job');
75 | expect(foundUser).to.have.property('car');
76 | expect(foundUser).to.have.property('languages').to.have.length(2).to.include('english').to.include('typescript');
77 | expect(foundUser.job).to.have.property('title', 'Developer');
78 | expect(foundUser.job).to.have.property('position', 'Lead');
79 | expect(foundUser.job).to.have.property('startedAt').to.be.instanceof(Date);
80 | expect(foundUser.job.titleInUppercase()).to.equal('Developer'.toUpperCase());
81 | expect(foundUser.job.jobType).to.not.have.property('_id');
82 | expect(foundUser.job.jobType).to.have.property('salery', 5000);
83 | expect(foundUser.job.jobType).to.have.property('field', 'IT');
84 | expect(foundUser.job.jobType).to.have.property('salery').to.be.a('number');
85 | expect(foundUser.car).to.have.property('model', 'Tesla');
86 | expect(foundUser.car).to.have.property('version', 'models');
87 | expect(foundUser).to.have.property('previousJobs').to.have.length(2);
88 |
89 | expect(foundUser).to.have.property('fullName', 'John Doe');
90 |
91 | const [janitor, manager] = foundUser.previousJobs;
92 | expect(janitor).to.have.property('title', 'Janitor');
93 | expect(manager).to.have.property('title', 'Manager');
94 |
95 | expect(foundUser).to.have.property('previousCars').to.have.length(2);
96 |
97 | const [foundTrabant, foundZastava] = foundUser.previousCars;
98 | expect(foundTrabant).to.have.property('model', 'Trabant');
99 | expect(foundTrabant).to.have.property('isSedan', true);
100 | expect(foundZastava).to.have.property('model', 'Zastava');
101 | expect(foundZastava).to.have.property('isSedan', undefined);
102 |
103 | foundUser.fullName = 'Sherlock Holmes';
104 | expect(foundUser).to.have.property('firstName', 'Sherlock');
105 | expect(foundUser).to.have.property('lastName', 'Holmes');
106 |
107 | await foundUser.incrementAge();
108 | expect(foundUser).to.have.property('age', 21);
109 | }
110 |
111 | {
112 | const foundUser = await User.findByAge(21);
113 | expect(foundUser).to.have.property('firstName', 'Sherlock');
114 | expect(foundUser).to.have.property('lastName', 'Holmes');
115 | }
116 | });
117 |
118 | it('should create a user with [Plugin].findOrCreate', async () => {
119 | const createdUser = await User.findOrCreate({
120 | firstName: 'Jane',
121 | lastName: 'Doe',
122 | gender: Genders.FEMALE,
123 | });
124 |
125 | expect(createdUser).to.not.be.an('undefined');
126 | expect(createdUser).to.have.property('created');
127 | expect(createdUser.created).to.be.equals(true);
128 | expect(createdUser).to.have.property('doc');
129 | expect(createdUser.doc).to.have.property('firstName', 'Jane');
130 |
131 | const foundUser = await User.findOrCreate({
132 | firstName: 'Jane',
133 | lastName: 'Doe',
134 | });
135 |
136 | expect(foundUser).to.not.be.an('undefined');
137 | expect(foundUser).to.have.property('created');
138 | expect(foundUser.created).to.be.equals(false);
139 | expect(foundUser).to.have.property('doc');
140 | expect(foundUser.doc).to.have.property('firstName', 'Jane');
141 |
142 | try {
143 | await User.create({
144 | _id: mongoose.Types.ObjectId(),
145 | firstName: 'John',
146 | lastName: 'Doe',
147 | age: 20,
148 | gender: Genders.MALE,
149 | uniqueId: 'john-doe-20',
150 | });
151 | } catch (err) {
152 | expect(err).to.have.property('code', 11000);
153 | }
154 | });
155 | }
156 |
--------------------------------------------------------------------------------
/test/tests/db_index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import { model as Car } from '../models/car';
4 | import { IndexWeights, model as IndexWeightsModel } from '../models/indexweigths';
5 | import { model as Rating } from '../models/rating';
6 | import { model as Select, SelectStrings } from '../models/select';
7 | import { model as User } from '../models/user';
8 |
9 | /**
10 | * Function to pass into describe
11 | * ->Important: you need to always bind this
12 | * @example
13 | * ```
14 | * import { suite as IndexTests } from './index.test'
15 | * ...
16 | * describe('Index', IndexTests.bind(this));
17 | * ...
18 | * ```
19 | */
20 | export function suite() {
21 | describe('Property Option {select}', () => {
22 | before(async () => {
23 | const selecttest = new Select();
24 | await selecttest.save();
25 | });
26 |
27 | it('should only return default selected properties', async () => {
28 | /** variable name long: foundSelectDefault */
29 | const fSDefault = (await Select.findOne({}).exec()).toObject();
30 |
31 | expect(fSDefault).to.not.have.property('test1');
32 | expect(fSDefault).to.have.property('test2', SelectStrings.test2);
33 | expect(fSDefault).to.not.have.property('test3');
34 | });
35 |
36 | it('should only return specificly selected properties', async () => {
37 | /** variable name long: foundSelectExtra */
38 | const fSExtra = (await Select.findOne({}).select(['+test1', '+test3', '-test2']).exec()).toObject();
39 |
40 | expect(fSExtra).to.have.property('test1', SelectStrings.test1);
41 | expect(fSExtra).to.not.have.property('test2');
42 | expect(fSExtra).to.have.property('test3', SelectStrings.test3);
43 | });
44 | });
45 |
46 | it('should create and find indexes with weights', async () => {
47 | const docMongoDB = await IndexWeightsModel.create({
48 | about: 'NodeJS module for MongoDB',
49 | content: 'MongoDB-native is the default driver for MongoDB in NodeJS',
50 | keywords: ['mongodb', 'js', 'nodejs']
51 | } as IndexWeights);
52 | const docMongoose = await IndexWeightsModel.create({
53 | about: 'NodeJS module for MongoDB',
54 | content: 'Mongoose is a Module for NodeJS that interfaces with MongoDB',
55 | keywords: ['mongoose', 'js', 'nodejs']
56 | } as IndexWeights);
57 | const docTypegoose = await IndexWeightsModel.create({
58 | about: 'TypeScript Module for Mongoose',
59 | content: 'Typegoose is a Module for NodeJS that makes Mongoose more compatible with Typescript',
60 | keywords: ['typegoose', 'ts', 'nodejs', 'mongoose']
61 | } as IndexWeights);
62 |
63 | {
64 | const found = await IndexWeightsModel.find({ $text: { $search: 'mongodb' } }).exec();
65 | expect(found).to.be.length(2);
66 | // expect it to be sorted by textScore
67 | expect(found[0].id).to.be.equal(docMongoDB.id);
68 | expect(found[1].id).to.be.equal(docMongoose.id);
69 | }
70 | {
71 | const found = await IndexWeightsModel.find({ $text: { $search: 'mongoose -js' } }).exec();
72 | expect(found).to.be.length(1);
73 | expect(found[0].id).to.be.equal(docTypegoose.id);
74 | }
75 | });
76 |
77 | it('should add compound index', async () => {
78 | const user = await User.findOne();
79 | const car = await Car.findOne();
80 |
81 | await Rating.create({ user, car, stars: 4 });
82 |
83 | // should fail, because user and car should be unique
84 | try {
85 | await Rating.create({ user, car, stars: 5 });
86 | } catch (err) {
87 | expect(err).to.have.property('code', 11000);
88 | }
89 | });
90 | }
91 |
--------------------------------------------------------------------------------
/test/tests/getClassForDocument.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as mongoose from 'mongoose';
3 |
4 | import { fail } from 'assert';
5 | import { getClassForDocument } from '../../src/utils';
6 | import { Genders } from '../enums/genders';
7 | import { Car as CarType, model as Car } from '../models/car';
8 | import { model as InternetUser } from '../models/internet-user';
9 | import { AddressNested, PersonNested, PersonNestedModel } from '../models/nested-object';
10 | import { model as Person } from '../models/person';
11 | import { model as User, User as UserType } from '../models/user';
12 |
13 | /**
14 | * Function to pass into describe
15 | * ->Important: you need to always bind this
16 | * @example
17 | * ```
18 | * import { suite as GCFDTest } from './getClassForDocument.test'
19 | * ...
20 | * describe('getClassForDocument()', GCFDTest.bind(this));
21 | * ...
22 | * ```
23 | */
24 | export function suite() {
25 | it('should return correct class type for document', async () => {
26 | const car = await Car.create({
27 | model: 'Tesla',
28 | price: mongoose.Types.Decimal128.fromString('50123.25')
29 | });
30 | const carReflectedType = getClassForDocument(car);
31 | expect(carReflectedType).to.equals(CarType);
32 |
33 | const user = await User.create({
34 | firstName: 'John2',
35 | lastName: 'Doe2',
36 | gender: Genders.MALE,
37 | languages: ['english2', 'typescript2'],
38 | uniqueId: 'not-needed'
39 | });
40 | const userReflectedType = getClassForDocument(user);
41 | expect(userReflectedType).to.equals(UserType);
42 |
43 | // assert negative to be sure (false positive)
44 | expect(carReflectedType).to.not.equals(UserType);
45 | expect(userReflectedType).to.not.equals(CarType);
46 | });
47 |
48 | it('should use inherited schema', async () => {
49 | let user = await Person.create({ email: 'my@email.com' });
50 |
51 | const car = await Car.create({
52 | model: 'Tesla',
53 | price: mongoose.Types.Decimal128.fromString('50123.25')
54 | });
55 | await user.addCar(car);
56 |
57 | user = await Person.findById(user.id).populate('cars');
58 |
59 | // verify properties
60 | expect(user).to.have.property('createdAt');
61 | expect(user).to.have.property('email', 'my@email.com');
62 |
63 | expect(user.cars.length).to.be.above(0);
64 | user.cars.map((currentCar: CarType) => {
65 | expect(currentCar.model).to.be.an('string');
66 | });
67 |
68 | // verify methods
69 | expect(user.getClassName()).to.equals('Person');
70 | expect(Person.getStaticName()).to.equals('Person');
71 | });
72 |
73 | it('Should store nested address', async () => {
74 | const personInput = new PersonNested();
75 | personInput.name = 'Person, Some';
76 | personInput.address = new AddressNested('A Street 1');
77 | personInput.moreAddresses = [
78 | new AddressNested('A Street 2'),
79 | new AddressNested('A Street 3'),
80 | ];
81 |
82 | const person = await PersonNestedModel.create(personInput);
83 |
84 | expect(person).is.not.be.an('undefined');
85 | expect(person.name).equals('Person, Some');
86 | expect(person.address).is.not.be.an('undefined');
87 | expect(person.address.street).equals('A Street 1');
88 | expect(person.moreAddresses).is.not.be.an('undefined');
89 | expect(person.moreAddresses.length).equals(2);
90 | expect(person.moreAddresses[0].street).equals('A Street 2');
91 | expect(person.moreAddresses[1].street).equals('A Street 3');
92 | });
93 |
94 | it('should properly set Decimal128, ObjectID types to field', () => {
95 | expect((Car.schema as any).paths.price.instance).to.eq('Decimal128');
96 | expect((Car.schema as any).paths.someId.instance).to.eq('ObjectID');
97 | });
98 |
99 | // faild validation will need to be checked
100 | it('Should validate Decimal128', async () => {
101 | try {
102 | await Car.create({
103 | model: 'Tesla',
104 | price: 'NO DECIMAL',
105 | });
106 | // fail('Validation must fail.');
107 |
108 | } catch (e) {
109 |
110 | expect(e).to.be.a.instanceof((mongoose.Error as any).ValidationError);
111 | }
112 | const car = await Car.create({
113 | model: 'Tesla',
114 | price: mongoose.Types.Decimal128.fromString('123.45')
115 | });
116 | const foundCar = await Car.findById(car._id).exec();
117 | expect(foundCar.price).to.be.a.instanceof(mongoose.Types.Decimal128);
118 | expect(foundCar.price.toString()).to.eq('123.45');
119 | });
120 |
121 | it('Should validate email', async () => {
122 | try {
123 | await Person.create({
124 | email: 'email',
125 | });
126 | fail('Validation must fail.');
127 | } catch (e) {
128 | expect(e).to.be.a.instanceof(mongoose.Error.ValidationError);
129 | expect(e.message).to.be.equal( // test it specificly, to know that it is not another error
130 | 'Person validation failed: email: Validator failed for path `email` with value `email`'
131 | );
132 | }
133 | });
134 |
135 | it(`Should Validate Map`, async () => {
136 | try {
137 | await InternetUser.create({
138 | projects: {
139 | p1: 'project'
140 | }
141 | });
142 | fail('Validation Should Fail');
143 | } catch (e) {
144 | expect(e).to.be.a.instanceof(mongoose.Error.ValidationError);
145 | expect(e.message).to.be.equal( // test it specificly, to know that it is not another error
146 | 'InternetUser validation failed: projects.p1: `project` is not a valid enum value for path `projects.p1`.'
147 | );
148 | }
149 | });
150 | }
151 |
--------------------------------------------------------------------------------
/test/tests/hooks.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import { Hook, model as HookModel } from '../models/hook1';
4 | import { model as Dummy } from '../models/hook2';
5 |
6 | /**
7 | * Function to pass into describe
8 | * ->Important: you need to always bind this
9 | * @example
10 | * ```
11 | * import { suite as HookTests } from './hooks.test'
12 | * ...
13 | * describe('Hooks', HookTests.bind(this));
14 | * ...
15 | * ```
16 | */
17 | export function suite() {
18 | it('RegEXP tests', async () => {
19 | const doc = new HookModel({ material: 'iron' } as Hook);
20 | await doc.save();
21 | await doc.updateOne(doc); // to run the update hook with regexp, find dosnt work (it dosnt get applied)
22 |
23 | const found = await HookModel.findById(doc.id).exec();
24 | expect(found).to.not.be.an('undefined');
25 | expect(found).to.have.property('material', 'REGEXP_POST');
26 | expect(found).to.have.property('shape', 'REGEXP_PRE');
27 | });
28 |
29 | it('should update the property using isModified during pre save hook', async () => {
30 | const hook = await HookModel.create({
31 | material: 'steel',
32 | });
33 | expect(hook).to.have.property('shape', 'oldShape');
34 |
35 | hook.set('shape', 'changed');
36 | const savedHook = await hook.save();
37 | expect(savedHook).to.have.property('shape', 'newShape');
38 | });
39 |
40 | it('should test findOne post hook', async () => {
41 | await Dummy.create({ text: 'initial' });
42 |
43 | // text is changed in pre save hook
44 | const dummyFromDb = await Dummy.findOne({ text: 'saved' });
45 | expect(dummyFromDb).to.have.property('text', 'changed in post findOne hook');
46 | });
47 |
48 | it('should find the unexpected dummies because of pre and post hooks', async () => {
49 | await Dummy.create([{ text: 'whatever' }, { text: 'whatever' }]);
50 |
51 | const foundDummies = await Dummy.find({ text: 'saved' });
52 |
53 | // pre-save-hook changed text to saved
54 | expect(foundDummies.length).to.be.above(2);
55 | expect(foundDummies[0]).to.have.property('text', 'changed in post find hook');
56 | expect(foundDummies[1]).to.have.property('text', 'saved');
57 | });
58 |
59 | it('should test the updateMany hook', async () => {
60 | await Dummy.insertMany([{ text: 'foobar42' }, { text: 'foobar42' }]);
61 |
62 | await Dummy.updateMany({ text: 'foobar42' }, { text: 'lorem ipsum' });
63 |
64 | const foundUpdatedDummies = await Dummy.find({ text: 'updateManied' });
65 |
66 | // pre-updateMany-hook changed text to 'updateManied'
67 | expect(foundUpdatedDummies.length).to.equal(2);
68 | });
69 | }
70 |
--------------------------------------------------------------------------------
/test/tests/shouldAdd.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as mongoose from 'mongoose';
3 |
4 | import { Ref } from '../../src/typegoose';
5 | import { Genders } from '../enums/genders';
6 | import { Alias, model as AliasModel } from '../models/alias';
7 | import { model as InternetUser } from '../models/internet-user';
8 | import { BeverageModel as Beverage, InventoryModel as Inventory, ScooterModel as Scooter } from '../models/inventory';
9 | import { model as User } from '../models/user';
10 | import { Virtual, VirtualSub } from '../models/virtualprop';
11 |
12 | /**
13 | * Function to pass into describe
14 | * ->Important: you need to always bind this
15 | * @example
16 | * ```
17 | * import { suite as ShouldAddTest } from './shouldAdd.test'
18 | * ...
19 | * describe('Should add', ShouldAddTest.bind(this));
20 | * ...
21 | * ```
22 | */
23 | export function suite() {
24 | it('should add a language and job using instance methods', async () => {
25 | const user = await User.create({
26 | firstName: 'harry',
27 | lastName: 'potter',
28 | gender: Genders.MALE,
29 | languages: ['english'],
30 | uniqueId: 'unique-id',
31 | });
32 | await user.addJob({ position: 'Dark Wizzard', title: 'Archmage' });
33 | await user.addJob();
34 | const savedUser = await user.addLanguage();
35 |
36 | expect(savedUser.languages).to.include('Hungarian');
37 | expect(savedUser.previousJobs.length).to.be.above(0);
38 | savedUser.previousJobs.map((prevJob) => {
39 | expect(prevJob.startedAt).to.be.a('date');
40 | });
41 | });
42 |
43 | it('should add and populate the virtual properties', async () => {
44 | const virtualModel = new Virtual().getModelForClass(Virtual);
45 | const virtualSubModel = new VirtualSub().getModelForClass(VirtualSub);
46 |
47 | const virtual1 = await new virtualModel({ dummyVirtual: 'dummyVirtual1' } as Virtual).save();
48 | const virtualsub1 = await new virtualSubModel({
49 | dummy: 'virtualSub1',
50 | virtual: virtual1._id
51 | } as Partial).save();
52 | const virtualsub2 = await new virtualSubModel({
53 | dummy: 'virtualSub2',
54 | virtual: mongoose.Types.ObjectId() as Ref
55 | } as Partial).save();
56 | const virtualsub3 = await new virtualSubModel({
57 | dummy: 'virtualSub3',
58 | virtual: virtual1._id
59 | } as Partial).save();
60 |
61 | const newfound = await virtualModel.findById(virtual1._id).populate('virtualSubs').exec();
62 |
63 | expect(newfound.dummyVirtual).to.be.equal('dummyVirtual1');
64 | expect(newfound.virtualSubs).to.not.be.an('undefined');
65 | expect(newfound.virtualSubs[0].dummy).to.be.equal('virtualSub1');
66 | expect(newfound.virtualSubs[0]._id.toString()).to.be.equal(virtualsub1._id.toString());
67 | expect(newfound.virtualSubs[1].dummy).to.be.equal('virtualSub3');
68 | expect(newfound.virtualSubs[1]._id.toString()).to.be.equal(virtualsub3._id.toString());
69 | expect(newfound.virtualSubs).to.not.include(virtualsub2);
70 | });
71 |
72 | it(`should add dynamic fields using map`, async () => {
73 | const user = await InternetUser.create({
74 | socialNetworks: {
75 | 'twitter': 'twitter account',
76 | 'facebook': 'facebook account',
77 | },
78 | sideNotes: {
79 | 'day1': {
80 | content: 'day1',
81 | link: 'url'
82 | },
83 | 'day2': {
84 | content: 'day2',
85 | link: 'url//2'
86 | },
87 | },
88 | projects: {},
89 | });
90 | expect(user).to.not.be.an('undefined');
91 | expect(user).to.have.property('socialNetworks').to.be.instanceOf(Map);
92 | expect(user.socialNetworks.get('twitter')).to.be.eq('twitter account');
93 | expect(user.socialNetworks.get('facebook')).to.be.eq('facebook account');
94 | expect(user).to.have.property('sideNotes').to.be.instanceOf(Map);
95 | expect(user.sideNotes.get('day1')).to.have.property('content', 'day1');
96 | expect(user.sideNotes.get('day1')).to.have.property('link', 'url');
97 | expect(user.sideNotes.has('day2')).to.be.equal(true);
98 | });
99 |
100 | it('Should support dynamic references via refPath', async () => {
101 | const sprite = new Beverage({
102 | isDecaf: true,
103 | isSugarFree: false
104 | });
105 | await sprite.save();
106 |
107 | await new Beverage({
108 | isDecaf: false,
109 | isSugarFree: true
110 | }).save();
111 |
112 | const vespa = new Scooter({
113 | makeAndModel: 'Vespa'
114 | });
115 | await vespa.save();
116 |
117 | await new Inventory({
118 | refItemPathName: 'Beverage',
119 | kind: sprite,
120 | count: 10,
121 | value: 1.99
122 | }).save();
123 |
124 | await new Inventory({
125 | refItemPathName: 'Scooter',
126 | kind: vespa,
127 | count: 1,
128 | value: 1099.98
129 | }).save();
130 |
131 | // I should now have two "inventory" items, with different embedded reference documents.
132 | const items = await Inventory.find({}).populate('kind');
133 | expect((items[0].kind as typeof Beverage).isDecaf).to.be.equals(true);
134 |
135 | // wrong type to make typescript happy
136 | expect((items[1].kind as typeof Beverage).isDecaf).to.be.an('undefined');
137 | });
138 |
139 | it('it should alias correctly', () => {
140 | const created = new AliasModel({ alias: 'hello from aliasProp', normalProp: 'hello from normalProp' } as Alias);
141 |
142 | expect(created).to.not.be.an('undefined');
143 | expect(created).to.have.property('normalProp', 'hello from normalProp');
144 | expect(created).to.have.property('alias', 'hello from aliasProp');
145 | expect(created).to.have.property('aliasProp');
146 |
147 | // include virtuals
148 | {
149 | const toObject = created.toObject({ virtuals: true });
150 | expect(toObject).to.not.be.an('undefined');
151 | expect(toObject).to.have.property('normalProp', 'hello from normalProp');
152 | expect(toObject).to.have.property('alias', 'hello from aliasProp');
153 | expect(toObject).to.have.property('aliasProp', 'hello from aliasProp');
154 | }
155 | // do not include virtuals
156 | {
157 | const toObject = created.toObject();
158 | expect(toObject).to.not.be.an('undefined');
159 | expect(toObject).to.have.property('normalProp', 'hello from normalProp');
160 | expect(toObject).to.have.property('alias', 'hello from aliasProp');
161 | expect(toObject).to.not.have.property('aliasProp');
162 | }
163 | });
164 | }
165 |
--------------------------------------------------------------------------------
/test/tests/stringValidator.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as mongoose from 'mongoose';
3 |
4 | import { model as StringValidators } from '../models/stringValidators';
5 |
6 | /**
7 | * Function to pass into describe
8 | * ->Important: you need to always bind this
9 | * @example
10 | * ```
11 | * import { suite as StringValidatorTests } from './stringValidator.test'
12 | * ...
13 | * describe('String Validators', StringValidatorTests.bind(this));
14 | * ...
15 | * ```
16 | */
17 | export function suite() {
18 | it('should respect maxlength', (done) => {
19 | expect(StringValidators.create({
20 | maxLength: 'this is too long',
21 | })).to.eventually.rejectedWith(mongoose.Error.ValidationError).and.notify(done);
22 | });
23 |
24 | it('should respect minlength', (done) => {
25 | expect(StringValidators.create({
26 | minLength: 'too short',
27 | })).to.eventually.rejectedWith(mongoose.Error.ValidationError).and.notify(done);
28 | });
29 |
30 | it('should trim', async () => {
31 | const trimmed = await StringValidators.create({
32 | trimmed: 'trim my end ',
33 | });
34 | expect(trimmed.trimmed).equals('trim my end');
35 | });
36 |
37 | it('should uppercase', async () => {
38 | const uppercased = await StringValidators.create({
39 | uppercased: 'make me uppercase',
40 | });
41 | expect(uppercased.uppercased).equals('MAKE ME UPPERCASE');
42 | });
43 |
44 | it('should lowercase', async () => {
45 | const lowercased = await StringValidators.create({
46 | lowercased: 'MAKE ME LOWERCASE',
47 | });
48 | expect(lowercased.lowercased).equals('make me lowercase');
49 | });
50 |
51 | it('should respect enum', (done) => {
52 | expect(StringValidators.create({
53 | enumed: 'not in the enum',
54 | })).to.eventually.rejectedWith(mongoose.Error).and.notify(done);
55 | });
56 | }
57 |
--------------------------------------------------------------------------------
/test/tests/typeguards.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import { isDocument, isDocumentArray } from '../../src/typeguards';
4 | import { UserRefModel } from '../models/userRefs';
5 |
6 | /**
7 | * Function to pass into describe
8 | * ->Important: you need to always bind this
9 | * @example
10 | * ```
11 | * import { suite as TypeguardsTest } from './typeguards.test'
12 | * ...
13 | * describe('Type guards', TypeguardsTest.bind(this));
14 | * ...
15 | * ```
16 | */
17 | export function suite() {
18 | it('should guarantee array of document types', async () => {
19 | const UserMaster = await UserRefModel.create({
20 | name: 'master',
21 | });
22 | const UserSub = await UserRefModel.create({
23 | name: 'sub',
24 | });
25 |
26 | UserMaster.subAccounts.push(UserSub._id);
27 |
28 | await UserMaster.populate('subAccounts').execPopulate();
29 |
30 | if (isDocumentArray(UserMaster.subAccounts)) {
31 | expect(UserMaster.subAccounts).to.have.lengthOf(1);
32 | for (const doc of UserMaster.subAccounts) {
33 | expect(doc.name).to.be.equal('sub');
34 | expect(doc.name).to.not.be.equal('other');
35 | }
36 | } else {
37 | throw new Error('"UserMaster.subAccounts" is not populated!');
38 | }
39 | });
40 |
41 | it('should guarantee single document type', async () => {
42 | const UserMaster = await UserRefModel.create({
43 | name: 'master',
44 | });
45 | const UserSub = await UserRefModel.create({
46 | name: 'sub',
47 | });
48 |
49 | UserSub.master = UserMaster._id;
50 |
51 | await UserSub.populate('master').execPopulate();
52 |
53 | if (isDocument(UserSub.master)) {
54 | expect(UserSub.master.name).to.be.equal('master');
55 | expect(UserSub.master.name).to.not.be.equal('other');
56 | } else {
57 | throw new Error('"UserSub.master" is not populated!');
58 | }
59 | });
60 |
61 | it('should detect if array of refs is not populated', async () => {
62 | const UserMaster = await UserRefModel.create({
63 | name: 'master',
64 | });
65 | const UserSub = await UserRefModel.create({
66 | name: 'sub',
67 | });
68 | UserMaster.subAccounts.push(UserSub._id);
69 |
70 | if (!isDocumentArray(UserMaster.subAccounts)) {
71 | expect(UserMaster.subAccounts).to.have.lengthOf(1);
72 | for (const doc of UserMaster.subAccounts) {
73 | expect(doc).to.not.have.property('name');
74 | }
75 | } else {
76 | throw new Error(
77 | '"UserMaster.subAccounts" is populated where it should not!'
78 | );
79 | }
80 | });
81 |
82 | it('should detect if ref is not populated', async () => {
83 | const UserMaster = await UserRefModel.create({
84 | name: 'master',
85 | });
86 | const UserSub = await UserRefModel.create({
87 | name: 'sub',
88 | });
89 |
90 | UserSub.master = UserMaster._id;
91 |
92 | if (!isDocument(UserSub.master)) {
93 | expect(UserSub.master).to.not.have.property('name');
94 | } else {
95 | throw new Error('"UserSub.master" is populated where it should not!');
96 | }
97 | });
98 |
99 | it('should handle recursive populations - multiple populates', async () => {
100 | const User1 = await UserRefModel.create({
101 | name: '1',
102 | });
103 | const User2 = await UserRefModel.create({
104 | name: '2',
105 | master: User1._id,
106 | });
107 | const User3 = await UserRefModel.create({
108 | name: '3',
109 | master: User2._id,
110 | });
111 |
112 | await User3.populate('master').execPopulate();
113 | if (isDocument(User3.master)) {
114 | // User3.master === User2
115 | await User3.master.populate('master').execPopulate();
116 | if (isDocument(User3.master.master)) {
117 | // User3.master.master === User1
118 | expect(User3.master.master.name).to.be.equal(User1.name);
119 | } else {
120 | throw new Error('User3.master.master should be populated!');
121 | }
122 | } else {
123 | throw new Error('User3.master should be populated!');
124 | }
125 |
126 | await User3.populate({
127 | path: 'master',
128 | populate: {
129 | path: 'master',
130 | },
131 | });
132 | });
133 |
134 | it('should handle recursive populations - single populate', async () => {
135 | const User1 = await UserRefModel.create({
136 | name: '1',
137 | });
138 | const User2 = await UserRefModel.create({
139 | name: '2',
140 | master: User1._id,
141 | });
142 | const User3 = await UserRefModel.create({
143 | name: '3',
144 | master: User2._id,
145 | });
146 |
147 | await User3.populate({
148 | path: 'master',
149 | populate: {
150 | path: 'master',
151 | },
152 | }).execPopulate();
153 | if (isDocument(User3.master) && isDocument(User3.master.master)) {
154 | // User3.master === User2 && User3.master.master === User1
155 | expect(User3.master.name).to.be.equal(User2.name);
156 | expect(User3.master.master.name).to.be.equal(User1.name);
157 | } else {
158 | throw new Error('"User3" should be deep populated!');
159 | }
160 | });
161 | }
162 |
--------------------------------------------------------------------------------
/test/utils/config.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 |
3 | interface IConfig {
4 | Memory: boolean;
5 | DataBase: string;
6 | Port: number;
7 | Auth: IAuth;
8 | IP: string;
9 | }
10 |
11 | interface IAuth {
12 | User: string;
13 | Passwd: string;
14 | DB: string;
15 | }
16 |
17 | enum EConfig {
18 | MONGODB_IP = 'MongoDB IP is not specified!',
19 | MONGODB_DB = 'MongoDB DataBase is not specified!',
20 | MONGODB_PORT = 'MongoDB Port is not specified!',
21 | MONGODB_AUTH = 'You should activate & use MongoDB Authentication!'
22 | }
23 |
24 | const env: NodeJS.ProcessEnv = process.env; // just to write less
25 |
26 | let path: string = env.CONFIG ? env.CONFIG : './test/config.json';
27 | path = fs.existsSync(path) ? path : './test/config_default.json';
28 |
29 | const configRAW: Readonly =
30 | JSON.parse(fs.readFileSync(path).toString());
31 |
32 | // ENV || CONFIG-FILE || DEFAULT
33 | const configFINAL: Readonly = {
34 | Memory: (env.C_USE_IN_MEMORY === 'true' ? true : undefined) ||
35 | (typeof configRAW.Memory === 'boolean' ? configRAW.Memory : true),
36 | DataBase: env.C_DATABASE || configRAW.DataBase || 'typegooseTest',
37 | Port: parseInt(env.C_PORT as string, 10) || configRAW.Port || 27017,
38 | Auth: {
39 | User: env.C_AUTH_USER || configRAW.Auth.User || '',
40 | Passwd: env.C_AUTH_PASSWD || configRAW.Auth.Passwd || '',
41 | DB: env.C_AUTH_DB || configRAW.Auth.DB || ''
42 | },
43 | IP: env.C_IP || configRAW.IP || 'mongodb'
44 | };
45 |
46 | /** Small callback for the tests below */
47 | function cb(text: string): void {
48 | // tslint:disable-next-line:no-console
49 | console.error(text);
50 | process.exit(-1);
51 | }
52 |
53 | if (!configFINAL.Memory) {
54 | if (!configFINAL.IP) { cb(EConfig.MONGODB_IP); }
55 | if (!configFINAL.DataBase) { cb(EConfig.MONGODB_DB); }
56 | if (!configFINAL.Port) { cb(EConfig.MONGODB_PORT); }
57 | }
58 |
59 | export { configFINAL as config };
60 |
--------------------------------------------------------------------------------
/test/utils/mongooseConnect.ts:
--------------------------------------------------------------------------------
1 | import { MongoMemoryServer } from 'mongodb-memory-server-global';
2 | import * as mongoose from 'mongoose';
3 | import { config } from './config';
4 |
5 | /** its needed in global space, because we dont want to create a new instance everytime */
6 | let instance: MongoMemoryServer = null;
7 |
8 | if (config.Memory) {
9 | // only create an instance, if it is enabled in the config, wich defaults to "true"
10 | instance = new MongoMemoryServer();
11 | }
12 |
13 | /** is it the First time connecting in this test run? */
14 | let isFirst = true;
15 | /**
16 | * Make a Connection to MongoDB
17 | */
18 | export async function connect(): Promise {
19 | if (config.Memory) {
20 | await mongoose.connect(await instance.getConnectionString(), {
21 | useNewUrlParser: true,
22 | useFindAndModify: true,
23 | useCreateIndex: true,
24 | autoIndex: true
25 | });
26 | } else {
27 | const options = {
28 | useNewUrlParser: true,
29 | useFindAndModify: true,
30 | useCreateIndex: true,
31 | dbName: config.DataBase,
32 | autoIndex: true
33 | };
34 | if (config.Auth.User.length > 0) {
35 | Object.assign(options, {
36 | user: config.Auth.User,
37 | pass: config.Auth.Passwd,
38 | authSource: config.Auth.DB
39 | });
40 | }
41 | await mongoose.connect(`mongodb://${config.IP}:${config.Port}/`, options);
42 | }
43 |
44 | if (isFirst) {
45 | return await firstConnect();
46 | }
47 | return;
48 | }
49 |
50 | /**
51 | * Disconnect from MongoDB
52 | * @returns when it is disconnected
53 | */
54 | export async function disconnect(): Promise {
55 | await mongoose.disconnect();
56 | if (config.Memory) {
57 | await instance.stop();
58 | }
59 | return;
60 | }
61 |
62 | /**
63 | * Only execute this function when the tests were not started
64 | */
65 | async function firstConnect() {
66 | isFirst = false;
67 | await mongoose.connection.db.dropDatabase(); // to always have a clean database
68 |
69 | await Promise.all( // recreate the indexes that were dropped
70 | Object.keys(mongoose.models).map(async modelName => {
71 | await mongoose.models[modelName].ensureIndexes();
72 | })
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "test/**/*.ts"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": [
5 | "esnext"
6 | ],
7 | "removeComments": true,
8 | "module": "commonjs",
9 | "moduleResolution": "node",
10 | "newLine": "LF",
11 | "declaration": true,
12 | "experimentalDecorators": true,
13 | "emitDecoratorMetadata": true,
14 | "allowJs": false,
15 | "outDir": "lib",
16 | "inlineSourceMap": true,
17 | "incremental": true,
18 | "tsBuildInfoFile": "./.tsbuildinfo"
19 | },
20 | "include": [
21 | "src/**/*.ts"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended",
5 | "tslint-config-prettier",
6 | "prettier-tslint"
7 | ],
8 | "rulesDirectory": [
9 | "node_modules/tslint-eslint-rules/dist/rules"
10 | ],
11 | "rules": {
12 | "quotemark": {
13 | "severity": "warn",
14 | "options": [
15 | "single"
16 | ]
17 | },
18 | "indent": {
19 | "severity": "error",
20 | "options": [
21 | "spaces",
22 | 2
23 | ]
24 | },
25 | "ter-indent": [
26 | true,
27 | 2
28 | ],
29 | "semicolon": {
30 | "severity": "error",
31 | "options": [
32 | "always"
33 | ]
34 | },
35 | "linebreak-style": [
36 | true,
37 | "LF"
38 | ],
39 | "max-line-length": [
40 | true,
41 | 120
42 | ],
43 | "comment-format": {
44 | "options": [
45 | "check-space"
46 | ],
47 | "severity": "warn"
48 | },
49 | "whitespace": {
50 | "options": [
51 | "check-branch",
52 | "check-operator",
53 | "check-separator",
54 | "check-preblock",
55 | "check-branch",
56 | "check-module"
57 | ]
58 | },
59 | "variable-name": [
60 | true,
61 | "ban-keywords",
62 | "check-format",
63 | "allow-leading-underscore",
64 | "require-const-for-all-caps",
65 | "allow-pascal-case"
66 | ],
67 | "no-implicit-dependencies": [
68 | true,
69 | "dev"
70 | ],
71 | "member-access": [
72 | true,
73 | "check-accessor",
74 | "check-parameter-property"
75 | ],
76 | "no-unused-variable": {
77 | "severity": "warn"
78 | },
79 | "no-unused-expression": {
80 | "severity": "warn"
81 | },
82 | "encoding": true,
83 | "no-invalid-template-strings": true,
84 | "eofline": true,
85 | "restrict-plus-operands": true,
86 | "use-isnan": true,
87 | "no-duplicate-imports": true,
88 | "prefer-method-signature": true,
89 | "no-require-imports": true,
90 | "max-classes-per-file": false,
91 | "interface-name": false,
92 | "object-literal-sort-keys": false,
93 | "array-type": false,
94 | "member-ordering": false
95 | }
96 | }
97 |
--------------------------------------------------------------------------------