├── .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 | [![Build Status](https://travis-ci.org/szokodiakos/typegoose.svg?branch=master)](https://travis-ci.org/szokodiakos/typegoose) 18 | [![Coverage Status](https://coveralls.io/repos/github/szokodiakos/typegoose/badge.svg?branch=master#feb282019)](https://coveralls.io/github/szokodiakos/typegoose?branch=master) 19 | [![npm](https://img.shields.io/npm/dt/typegoose.svg)]() 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 | --------------------------------------------------------------------------------