├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .nycrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── _layouts │ └── default.html ├── api │ ├── Nohm.html │ ├── NohmClass.html │ ├── NohmErrors.LinkError.html │ ├── NohmErrors.ValidationError.html │ ├── NohmErrors.html │ ├── NohmModel.html │ ├── NohmStaticModel.html │ ├── Validators.html │ ├── fonts │ │ ├── Montserrat │ │ │ ├── Montserrat-Bold.eot │ │ │ ├── Montserrat-Bold.ttf │ │ │ ├── Montserrat-Bold.woff │ │ │ ├── Montserrat-Bold.woff2 │ │ │ ├── Montserrat-Regular.eot │ │ │ ├── Montserrat-Regular.ttf │ │ │ ├── Montserrat-Regular.woff │ │ │ └── Montserrat-Regular.woff2 │ │ └── Source-Sans-Pro │ │ │ ├── sourcesanspro-light-webfont.eot │ │ │ ├── sourcesanspro-light-webfont.svg │ │ │ ├── sourcesanspro-light-webfont.ttf │ │ │ ├── sourcesanspro-light-webfont.woff │ │ │ ├── sourcesanspro-light-webfont.woff2 │ │ │ ├── sourcesanspro-regular-webfont.eot │ │ │ ├── sourcesanspro-regular-webfont.svg │ │ │ ├── sourcesanspro-regular-webfont.ttf │ │ │ ├── sourcesanspro-regular-webfont.woff │ │ │ └── sourcesanspro-regular-webfont.woff2 │ ├── index.html │ ├── scripts │ │ ├── collapse.js │ │ ├── linenumber.js │ │ ├── nav.js │ │ ├── polyfill.js │ │ ├── prettify │ │ │ ├── Apache-License-2.0.txt │ │ │ ├── lang-css.js │ │ │ └── prettify.js │ │ └── search.js │ ├── styles │ │ ├── jsdoc.css │ │ └── prettify.css │ ├── tsOut_errors_LinkError.js.html │ ├── tsOut_errors_ValidationError.js.html │ ├── tsOut_index.js.html │ ├── tsOut_middleware.js.html │ ├── tsOut_model.js.html │ ├── tsOut_validators.js.html │ └── ts_universalValidators.js.html ├── index.md ├── index_v1.md └── style.css ├── examples └── rest-user-server │ ├── Dockerfile │ ├── README.md │ ├── UserModel.js │ ├── client.js │ ├── docker-compose.yml │ ├── index.html │ ├── package-lock.json │ ├── package.json │ └── rest-server.js ├── extra ├── models.js └── stress.js ├── jsdoc.json ├── package-lock.json ├── package.json ├── test ├── custom_methods.test.ts ├── custom_validations.js ├── custom_validations2.js ├── exports.test.ts ├── features.test.ts ├── find.test.ts ├── helper.ts ├── meta.test.ts ├── middleware.test.ts ├── property.test.ts ├── pubsub.test.ts ├── pubsub │ ├── Model.ts │ ├── child.ts │ └── child_wrapper.js ├── redisHelper.test.ts ├── regressions.test.ts ├── relationTests.test.ts ├── snapshots │ ├── exports.test.ts.md │ ├── exports.test.ts.snap │ ├── exports.ts.md │ ├── exports.ts.snap │ ├── features.test.ts.md │ ├── features.test.ts.snap │ ├── middleware.test.ts.md │ └── middleware.test.ts.snap ├── testArgs.ts ├── tsconfig.json ├── tslint.json ├── typescript.test.ts ├── uniques.test.ts └── validations.test.ts ├── ts ├── errors │ ├── LinkError.ts │ └── ValidationError.ts ├── eventComposers.ts ├── helpers.ts ├── idGenerators.ts ├── index.ts ├── middleware.ts ├── model.header.ts ├── model.ts ├── typed-redis-helper.ts ├── universalValidators.js └── validators.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*] 14 | charset = utf-8 15 | # Indentation override for all JS under lib directory 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 8, 9 | "ecmaFeatures": { 10 | "arrowFunctions": true, 11 | "generators": true, 12 | "classes": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-console": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .settings.xml 2 | node_modules 3 | .project 4 | nohm.komodoproject 5 | benchmark.js.js 6 | admin/*.sh 7 | *.log 8 | .sass-cache 9 | dump.rdb 10 | experiments.js 11 | .c9revisions 12 | .settings 13 | .c9/ 14 | tsOut/ 15 | .vscode/ 16 | coverage/ 17 | .nyc_output/ 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "ts/**/*.ts", 4 | "ts/universalValidators.js" 5 | ], 6 | "extension": [ 7 | ".js", 8 | ".ts" 9 | ], 10 | "exclude": [ 11 | "test/" 12 | ], 13 | "reporter": [ 14 | "text", 15 | "html", 16 | "lcov" 17 | ], 18 | "sourceMap": true 19 | } 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tsOut/ 2 | ts/universalValidators.js 3 | package.json 4 | CHANGELOG.md 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "arrowParens": "always" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Moritz Peters 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nohm 2 | 3 | [![Known Vulnerabilities (Snyk)](https://snyk.io/test/github/maritz/nohm/badge.svg)](https://snyk.io/test/github/maritz/nohm) 4 | [![Coverage Status](https://coveralls.io/repos/github/maritz/nohm/badge.png?branch=master)](https://coveralls.io/github/maritz/nohm?branch=master) 5 | 6 | ## Description 7 | 8 | Nohm is an object relational mapper (ORM) written for node.js and redis written in Typescript. 9 | 10 | ### Features 11 | 12 | - **Standard ORM features (validate, store, search, sort, link/unlink, delete)** 13 | - **Share validations with browser.** 14 | Allows using the same code for client validations that is used for backend. Includes filtering which validations are shared. 15 | - **Subscribe to orm events (save, delete, link, unlink)** 16 | With this you can do things like socket connections to get live updates from stored models. 17 | Since it uses redis PUBSUB you can scale your node app and clients can connect to separate node app instances but will still get the same live updates. 18 | - **Typescript typings** 19 | nohm is written in Typescript and thus provides first-class typings for most things, including the option to type your model properties. This means if you use Typescript you don't have to remember every single property name of each model anymore, your IDE can tell you. 20 | - **Dynamic relations** 21 | This is a double-edged sword. Usually ORMs describe relations statically and you have to do database changes before you can add new relations. 22 | In nohm all relations are defined and used at run-time, since there are no schemas stored in the database. 23 | 24 | ## Requirements 25 | 26 | - redis >= 2.4 27 | 28 | ## Documentation 29 | 30 | [v2 documentation](https://maritz.github.io/nohm/index.html) 31 | 32 | [API docs](https://maritz.github.io/nohm/api/index.html) 33 | 34 | [v1 documentation](http://maritz.github.com/nohm/) 35 | 36 | [v1 to v2 migration guide](https://github.com/maritz/nohm/blob/master/CHANGELOG.md#v200-currently-in-alpha) 37 | 38 | ## Example 39 | 40 | The [examples/rest-user-server](https://github.com/maritz/nohm/tree/master/examples/rest-user-server) is running as a demo on [https://nohm-example.maritz.space](https://nohm-example.maritz.space). It showcases most features on a basic level, including the shared validation and PubSub. 41 | 42 |
43 | 44 | Example ES6 code (click to expand) 45 | 46 | ```javascript 47 | import { Nohm, NohmModel, ValidationError } from 'nohm'; 48 | // or if your environment does not support module import 49 | // const NohmModule = require('nohm'); // access NohmModule.Nohm, NohmModule.NohmModel and NohmModule.ValidationError 50 | 51 | // This is the parent object where you set redis connection, create your models and some other configuration stuff 52 | const nohm = Nohm; 53 | 54 | nohm.setPrefix('example'); // This prefixes all redis keys. By default the prefix is "nohm", you probably want to change it to your applications name or something similar 55 | 56 | // This is a class that you can extend to create nohm models. Not needed when using nohm.model() 57 | const Model = NohmModel; 58 | 59 | const existingCountries = ['Narnia', 'Gondor', 'Tatooine']; 60 | 61 | // Using ES6 classes here, but you could also use the old nohm.model definition 62 | class UserModel extends Model { 63 | getCountryFlag() { 64 | return `http://example.com/flag_${this.property('country')}.png`; 65 | } 66 | } 67 | // Define the required static properties 68 | UserModel.modelName = 'User'; 69 | UserModel.definitions = { 70 | email: { 71 | type: 'string', 72 | unique: true, 73 | validations: ['email'], 74 | }, 75 | country: { 76 | type: 'string', 77 | defaultValue: 'Narnia', 78 | index: true, 79 | validations: [ 80 | // the function name will be part of the validation error messages, so for this it would be "custom_checkCountryExists" 81 | async function checkCountryExists(value) { 82 | // needs to return a promise that resolves to a bool - async functions take care of the promise part 83 | return existingCountries.includes(value); 84 | }, 85 | { 86 | name: 'length', 87 | options: { min: 3 }, 88 | }, 89 | ], 90 | }, 91 | visits: { 92 | type: function incrVisitsBy(value, key, old) { 93 | // arguments are always string here since they come from redis. 94 | // in behaviors (type functions) you are responsible for making sure they return in the type you want them to be. 95 | return parseInt(old, 10) + parseInt(value, 10); 96 | }, 97 | defaultValue: 0, 98 | index: true, 99 | }, 100 | }; 101 | 102 | // register our model in nohm and returns the resulting Class, do not use the UserModel directly! 103 | const UserModelClass = nohm.register(UserModel); 104 | 105 | const redis = require('redis').createClient(); 106 | // wait for redis to connect, otherwise we might try to write to a non-existent redis server 107 | redis.on('connect', async () => { 108 | nohm.setClient(redis); 109 | 110 | // factory returns a promise, resolving to a fresh instance (or a loaded one if id is provided, see below) 111 | const user = await nohm.factory('User'); 112 | 113 | // set some properties 114 | user.property({ 115 | email: 'mark13@example.com', 116 | country: 'Gondor', 117 | visits: 1, 118 | }); 119 | 120 | try { 121 | await user.save(); 122 | } catch (err) { 123 | if (err instanceof ValidationError) { 124 | // validation failed 125 | for (const key in err.errors) { 126 | const failures = err.errors[key].join(`', '`); 127 | console.log( 128 | `Validation of property '${key}' failed in these validators: '${failures}'.`, 129 | ); 130 | 131 | // in a real app you'd probably do something with the validation errors (like make an object for the client) 132 | // and then return or rethrow some other error 133 | } 134 | } 135 | // rethrow because we didn't recover from the error. 136 | throw err; 137 | } 138 | console.log(`Saved user with id ${user.id}`); 139 | 140 | const id = user.id; 141 | 142 | // somewhere else we could then load the user again 143 | const loadedUser = await UserModelClass.load(id); // this will throw an error if the user cannot be found 144 | 145 | // alternatively you can use nohm.factory('User', id) 146 | 147 | console.log(`User loaded. His properties are %j`, loadedUser.allProperties()); 148 | const newVisits = loadedUser.property('visits', 20); 149 | console.log(`User visits set to ${newVisits}.`); // Spoiler: it's 21 150 | 151 | // or find users by country 152 | const gondorians = await UserModelClass.findAndLoad({ 153 | country: 'Gondor', 154 | }); 155 | console.log( 156 | `Here are all users from Gondor: %j`, 157 | gondorians.map((u) => u.property('email')), 158 | ); 159 | 160 | await loadedUser.remove(); 161 | console.log(`User deleted from database.`); 162 | }); 163 | ``` 164 | 165 |
166 | 167 |
168 | 169 | Example Typescript code (click to expand) 170 | 171 | ```typescript 172 | import { Nohm, NohmModel, TTypedDefinitions } from 'nohm'; 173 | 174 | // We're gonna assume the basics are clear and the connection is set up etc. - look at the ES6 example otherwise. 175 | // This example highlights some of the typing capabilities in nohm. 176 | 177 | interface IUserProperties { 178 | email: string; 179 | visits: number; 180 | } 181 | 182 | class UserModel extends NohmModel { 183 | public static modelName = 'User'; 184 | 185 | protected static definitions: TTypedDefinitions = { 186 | // because of the TTypedDefinitions we can only define properties keys here that match our interface keys 187 | // the structure of the definitions is also typed 188 | email: { 189 | type: 'string', // the type value is currently not checked. If you put a wrong type here, no compile error will appear. 190 | unique: true, 191 | validations: ['email'], 192 | }, 193 | visits: { 194 | defaultValue: 0, 195 | index: true, 196 | type: function incrVisitsBy(value, _key, old): number { 197 | return old + value; // TS Error: arguments are all strings, not assignable to number 198 | }, 199 | }, 200 | }; 201 | 202 | public getVisitsAsString(): string { 203 | return this.property('visits'); // TS Error: visits is number and thus not assignable to string 204 | } 205 | 206 | public static async loadTyped(id: string): Promise { 207 | // see main() below for explanation 208 | return userModelStatic.load(id); 209 | } 210 | } 211 | 212 | const userModelStatic = nohm.register(UserModel); 213 | 214 | async function main() { 215 | // currently you still have to pass the generic if you want typing for class methods 216 | const user = await userModelStatic.load('some id'); 217 | // you can use the above defined loadTyped method to work around that. 218 | 219 | const props = user.allProperties(); 220 | props.email; // string 221 | props.id; // any 222 | props.visits; // number 223 | props.foo; // TS Error: Property foo does not exist 224 | user.getVisitsAsString(); // string 225 | } 226 | 227 | main(); 228 | ``` 229 | 230 |
231 | 232 | ### More detailed examples 233 | 234 | - [nohm/examples/rest-user-server](https://github.com/maritz/nohm/tree/master/examples/rest-user-server) 235 | - [Beauvoir](https://github.com/yuchi/Beauvoir) Simple project management app - by yuchi (uses node v0.6 - very old) 236 | 237 | Do you have code that should/could be listed here? Message me! 238 | 239 | ## Add it to your project 240 | 241 | npm install --save nohm 242 | 243 | ## Debug 244 | 245 | Nohm uses the [debug](https://github.com/visionmedia/debug) module under the namespace "nohm". To see detailed debug logging set the environment variable DEBUG accordingly: 246 | 247 | DEBUG="nohm:*" node yourApp.js 248 | 249 | Available submodule debug namespaces are `nohm:index`, `nohm:model`, `nohm:middleware`, `nohm:pubSub` and `nohm:idGenerator`. 250 | 251 | ## Developing nohm 252 | 253 | If you want to make changes to nohm, you can fork or clone it. Then install the dependencies: 254 | 255 | npm install 256 | 257 | and run the development scripts (compile & watch & tests): 258 | 259 | npm run dev 260 | 261 | When submitting PRs, please make sure that you run the linter and that everything still builds fine. 262 | The easiest way to do that is to run the `prepublishOnly` script: 263 | 264 | npm run prepublishOnly 265 | 266 | ## Running tests 267 | 268 | Build the javascript files: 269 | 270 | npm run build 271 | 272 | Then run the tests: 273 | 274 | npm run test 275 | # or 276 | npm run test:watch 277 | 278 | This requires a running redis server. (you can configure host/port with the command line arguments --redis-host 1.1.1.1 --redis-port 1234) 279 | 280 | **WARNING**: The tests also create a lot of temporary keys in your database that look something like this: 281 | 282 | nohmtestsuniques:something:something 283 | 284 | After the tests have run all keys that match the pattern nohmtests\* are deleted! 285 | 286 | You can change the prefix ("nohmtests") part doing something like 287 | 288 | node test/tests.js --nohm-prefix YourNewPrefix 289 | 290 | Now the keys will look like this: 291 | 292 | YourNewPrefixuniques:something:something 293 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nohm Documentation - {{ page.title }} 5 | 6 | 7 | 8 |

Nohm Documentation - {{ page.title }}

9 | {{ content }} 10 | 11 | -------------------------------------------------------------------------------- /docs/api/Nohm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nohm - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 32 | 33 |
34 | 35 |

Nohm

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 47 |

48 | Nohm 49 |

50 | 51 | 52 |
53 | 54 |
55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 | 63 |
Source:
64 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
99 | 100 | 101 | 102 | 103 | 104 |

Some generic definitions for Nohm

105 | 106 | 107 | 108 | 109 |
110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |

Methods

128 | 129 | 130 | 131 | 132 | 133 | 134 |

(static) MiddlewareCallback(req, res, nextopt)

135 | 136 | 137 | 138 | 139 | 140 | 141 |
142 | 143 | 144 |
Source:
145 |
148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 |
180 | 181 | 182 | 183 | 184 | 185 |
186 |

This function is what is returned by NohmClass#middleware.

187 |
188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 |
Parameters:
200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 237 | 238 | 239 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 268 | 269 | 270 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 299 | 300 | 301 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 |
NameTypeAttributesDescription
req 230 | 231 | 232 | Object 233 | 234 | 235 | 236 | 240 | 241 | 242 | 243 | 244 | 245 |

http IncomingMessage

res 261 | 262 | 263 | Object 264 | 265 | 266 | 267 | 271 | 272 | 273 | 274 | 275 | 276 |

http ServerResponse

next 292 | 293 | 294 | function 295 | 296 | 297 | 298 | 302 | 303 | <optional>
304 | 305 | 306 | 307 | 308 | 309 |

Optional next function for express/koa

320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 |
346 | 347 |
348 | 349 | 350 | 351 | 352 | 353 | 354 |
355 | 356 |
357 | 358 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | -------------------------------------------------------------------------------- /docs/api/NohmErrors.LinkError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LinkError - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 32 | 33 |
34 | 35 |

LinkError

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 47 |

48 | NohmErrors. 49 | 50 | LinkError 51 |

52 | 53 | 54 |
55 | 56 |
57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 |

new LinkError()

65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 |
Source:
75 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 | 111 | 112 | 113 | 114 | 115 |
116 |

Error thrown whenever linking failed during NohmModel#save.

117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |
148 | 149 | 150 | 151 |

Extends

152 | 153 | 154 | 155 | 156 |
    157 |
  • Error
  • 158 |
159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 |

Members

175 | 176 | 177 | 178 |

errors :Array.<{success: boolean, child: NohmModel, parent: NohmModel, error: (null|Error|LinkError|ValidationError)}>

179 | 180 | 181 | 182 | 183 | 184 |
185 | 186 | 187 |
Source:
188 |
191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
223 | 224 | 225 | 226 | 227 | 228 |
229 |

Details about which part of linking failed.

230 |
231 | 232 | 233 | 234 |
Type:
235 |
    236 |
  • 237 | 238 | Array.<{success: boolean, child: NohmModel, parent: NohmModel, error: (null|Error|LinkError|ValidationError)}> 239 | 240 | 241 |
  • 242 |
243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 |
258 | 259 |
260 | 261 | 262 | 263 | 264 | 265 | 266 |
267 | 268 |
269 | 270 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /docs/api/NohmErrors.ValidationError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ValidationError - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 32 | 33 |
34 | 35 |

ValidationError

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 47 |

48 | NohmErrors. 49 | 50 | ValidationError 51 |

52 | 53 | 54 |
55 | 56 |
57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 |

new ValidationError()

65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 |
Source:
75 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 | 111 | 112 | 113 | 114 | 115 |
116 |

Error thrown whenever validation failed during NohmModel#validate or NohmModel#save.

117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |
148 | 149 | 150 | 151 |

Extends

152 | 153 | 154 | 155 | 156 |
    157 |
  • Error
  • 158 |
159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 |

Members

175 | 176 | 177 | 178 |

errors :Object.<string, Array.<string>>

179 | 180 | 181 | 182 | 183 | 184 |
185 | 186 | 187 |
Source:
188 |
191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
223 | 224 | 225 | 226 | 227 | 228 |
229 |

Details about which properties failed to validate in which way.

230 |

The type is an object with property names as keys and then an array with validation 231 | names of the validations that failed

232 |
233 | 234 | 235 | 236 |
Type:
237 |
    238 |
  • 239 | 240 | Object.<string, Array.<string>> 241 | 242 | 243 |
  • 244 |
245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 |
260 | 261 |
262 | 263 | 264 | 265 | 266 | 267 | 268 |
269 | 270 |
271 | 272 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /docs/api/NohmErrors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | NohmErrors - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 32 | 33 |
34 | 35 |

NohmErrors

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 47 |

48 | NohmErrors 49 |

50 | 51 | 52 |
53 | 54 |
55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 | 63 |
Source:
64 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
99 | 100 | 101 | 102 | 103 | 104 |

Nohm specific Errors

105 | 106 | 107 | 108 | 109 |
110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |

Classes

118 | 119 |
120 |
LinkError
121 |
122 | 123 |
ValidationError
124 |
125 |
126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
142 | 143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 |
151 | 152 |
153 | 154 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /docs/api/fonts/Montserrat/Montserrat-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Montserrat/Montserrat-Bold.eot -------------------------------------------------------------------------------- /docs/api/fonts/Montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /docs/api/fonts/Montserrat/Montserrat-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Montserrat/Montserrat-Bold.woff -------------------------------------------------------------------------------- /docs/api/fonts/Montserrat/Montserrat-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Montserrat/Montserrat-Bold.woff2 -------------------------------------------------------------------------------- /docs/api/fonts/Montserrat/Montserrat-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Montserrat/Montserrat-Regular.eot -------------------------------------------------------------------------------- /docs/api/fonts/Montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /docs/api/fonts/Montserrat/Montserrat-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Montserrat/Montserrat-Regular.woff -------------------------------------------------------------------------------- /docs/api/fonts/Montserrat/Montserrat-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Montserrat/Montserrat-Regular.woff2 -------------------------------------------------------------------------------- /docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot -------------------------------------------------------------------------------- /docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf -------------------------------------------------------------------------------- /docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff -------------------------------------------------------------------------------- /docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 -------------------------------------------------------------------------------- /docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot -------------------------------------------------------------------------------- /docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf -------------------------------------------------------------------------------- /docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff -------------------------------------------------------------------------------- /docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 -------------------------------------------------------------------------------- /docs/api/scripts/collapse.js: -------------------------------------------------------------------------------- 1 | function hideAllButCurrent(){ 2 | //by default all submenut items are hidden 3 | //but we need to rehide them for search 4 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(parent) { 5 | parent.style.display = "none"; 6 | }); 7 | 8 | //only current page (if it exists) should be opened 9 | var file = window.location.pathname.split("/").pop().replace(/\.html/, ''); 10 | document.querySelectorAll("nav > ul > li > a").forEach(function(parent) { 11 | var href = parent.attributes.href.value.replace(/\.html/, ''); 12 | if (file === href) { 13 | parent.parentNode.querySelectorAll("ul li").forEach(function(elem) { 14 | elem.style.display = "block"; 15 | }); 16 | } 17 | }); 18 | } 19 | 20 | hideAllButCurrent(); -------------------------------------------------------------------------------- /docs/api/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/api/scripts/nav.js: -------------------------------------------------------------------------------- 1 | function scrollToNavItem() { 2 | var path = window.location.href.split('/').pop().replace(/\.html/, ''); 3 | document.querySelectorAll('nav a').forEach(function(link) { 4 | var href = link.attributes.href.value.replace(/\.html/, ''); 5 | if (path === href) { 6 | link.scrollIntoView({block: 'center'}); 7 | return; 8 | } 9 | }) 10 | } 11 | 12 | scrollToNavItem(); 13 | -------------------------------------------------------------------------------- /docs/api/scripts/polyfill.js: -------------------------------------------------------------------------------- 1 | //IE Fix, src: https://www.reddit.com/r/programminghorror/comments/6abmcr/nodelist_lacks_foreach_in_internet_explorer/ 2 | if (typeof(NodeList.prototype.forEach)!==typeof(alert)){ 3 | NodeList.prototype.forEach=Array.prototype.forEach; 4 | } -------------------------------------------------------------------------------- /docs/api/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/api/scripts/search.js: -------------------------------------------------------------------------------- 1 | 2 | var searchAttr = 'data-search-mode'; 3 | function contains(a,m){ 4 | return (a.textContent || a.innerText || "").toUpperCase().indexOf(m) !== -1; 5 | }; 6 | 7 | //on search 8 | document.getElementById("nav-search").addEventListener("keyup", function(event) { 9 | var search = this.value.toUpperCase(); 10 | 11 | if (!search) { 12 | //no search, show all results 13 | document.documentElement.removeAttribute(searchAttr); 14 | 15 | document.querySelectorAll("nav > ul > li:not(.level-hide)").forEach(function(elem) { 16 | elem.style.display = "block"; 17 | }); 18 | 19 | if (typeof hideAllButCurrent === "function"){ 20 | //let's do what ever collapse wants to do 21 | hideAllButCurrent(); 22 | } else { 23 | //menu by default should be opened 24 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 25 | elem.style.display = "block"; 26 | }); 27 | } 28 | } else { 29 | //we are searching 30 | document.documentElement.setAttribute(searchAttr, ''); 31 | 32 | //show all parents 33 | document.querySelectorAll("nav > ul > li").forEach(function(elem) { 34 | elem.style.display = "block"; 35 | }); 36 | //hide all results 37 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 38 | elem.style.display = "none"; 39 | }); 40 | //show results matching filter 41 | document.querySelectorAll("nav > ul > li > ul a").forEach(function(elem) { 42 | if (!contains(elem.parentNode, search)) { 43 | return; 44 | } 45 | elem.parentNode.style.display = "block"; 46 | }); 47 | //hide parents without children 48 | document.querySelectorAll("nav > ul > li").forEach(function(parent) { 49 | var countSearchA = 0; 50 | parent.querySelectorAll("a").forEach(function(elem) { 51 | if (contains(elem, search)) { 52 | countSearchA++; 53 | } 54 | }); 55 | 56 | var countUl = 0; 57 | var countUlVisible = 0; 58 | parent.querySelectorAll("ul").forEach(function(ulP) { 59 | // count all elements that match the search 60 | if (contains(ulP, search)) { 61 | countUl++; 62 | } 63 | 64 | // count all visible elements 65 | var children = ulP.children 66 | for (i=0; i 2 | 3 | 4 | 5 | 6 | tsOut/errors/LinkError.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 32 | 33 |
34 | 35 |

tsOut/errors/LinkError.js

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |
"use strict";
46 | Object.defineProperty(exports, "__esModule", { value: true });
47 | // tslint:disable:max-line-length
48 | /**
49 |  * Details about which part of linking failed.
50 |  *
51 |  * @type { Array.<{ success: boolean, child: NohmModel, parent: NohmModel, error: null | Error | LinkError | ValidationError}> }
52 |  * @name errors
53 |  * @memberof NohmErrors.LinkError#
54 |  */
55 | // tslint:enable:max-line-length
56 | /**
57 |  * Error thrown whenever linking failed during {@link NohmModel#save}.
58 |  *
59 |  * @class LinkError
60 |  * @memberof NohmErrors
61 |  * @extends {Error}
62 |  */
63 | class LinkError extends Error {
64 |     constructor(errors, errorMessage = 'Linking failed. See .errors on this Error object for an Array of failures.') {
65 |         super(errorMessage);
66 |         this.errors = errors;
67 |     }
68 | }
69 | exports.LinkError = LinkError;
70 | //# sourceMappingURL=LinkError.js.map
71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | 81 |
82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /docs/api/tsOut_errors_ValidationError.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tsOut/errors/ValidationError.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 32 | 33 |
34 | 35 |

tsOut/errors/ValidationError.js

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |
"use strict";
 46 | Object.defineProperty(exports, "__esModule", { value: true });
 47 | // tslint:disable:max-line-length
 48 | /**
 49 |  * Details about which properties failed to validate in which way.
 50 |  *
 51 |  * The type is an object with property names as keys and then an array with validation
 52 |  * names of the validations that failed
 53 |  *
 54 |  * @type { Object.<string, Array<string>> }
 55 |  * @name errors
 56 |  * @memberof NohmErrors.ValidationError#
 57 |  */
 58 | // tslint:enable:max-line-length
 59 | /**
 60 |  * Error thrown whenever validation failed during {@link NohmModel#validate} or {@link NohmModel#save}.
 61 |  *
 62 |  * @class ValidationError
 63 |  * @memberof NohmErrors
 64 |  * @extends {Error}
 65 |  */
 66 | class ValidationError extends Error {
 67 |     constructor(errors, modelName, errorMessage = 'Validation failed. See .errors on this Error or the Nohm model instance for details.') {
 68 |         super(errorMessage);
 69 |         const emptyErrors = {};
 70 |         this.modelName = modelName;
 71 |         this.errors = Object.keys(errors).reduce((obj, key) => {
 72 |             const error = errors[key];
 73 |             if (error && error.length > 0) {
 74 |                 obj[key] = error;
 75 |             }
 76 |             return obj;
 77 |         }, emptyErrors);
 78 |     }
 79 | }
 80 | exports.ValidationError = ValidationError;
 81 | //# sourceMappingURL=ValidationError.js.map
82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 |
91 | 92 |
93 | 94 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /docs/api/tsOut_validators.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tsOut/validators.js - Documentation 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 32 | 33 |
34 | 35 |

tsOut/validators.js

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |
"use strict";
46 | Object.defineProperty(exports, "__esModule", { value: true });
47 | const path = require("path");
48 | exports.universalValidatorPath = path.join(__dirname, '..', 'ts', 'universalValidators.js');
49 | // tslint:disable-next-line:no-var-requires
50 | const newRawValidators = require(exports.universalValidatorPath);
51 | /**
52 |  * @namespace Validators
53 |  */
54 | exports.validators = newRawValidators.validators;
55 | exports.regexps = newRawValidators.regexps;
56 | //# sourceMappingURL=validators.js.map
57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 | 67 |
68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, object, iframe, blockquote, pre, 2 | abbr, address, cite, code, 3 | del, dfn, em, img, ins, kbd, q, samp, 4 | small, strong, sub, sup, var, 5 | b, i, 6 | fieldset, form, label, legend, 7 | table, caption, tbody, tfoot, thead, tr, th, td, 8 | article, aside, canvas, details, figcaption, figure, 9 | footer, header, hgroup, menu, nav, section, summary, 10 | time, mark, audio, video { 11 | margin:0; 12 | padding:0; 13 | border:0; 14 | outline:0; 15 | font-size:100%; 16 | vertical-align:baseline; 17 | background:transparent; 18 | } 19 | 20 | 21 | 22 | body { 23 | background-color: #eee; 24 | margin: 20px 80px; 25 | color: black; 26 | } 27 | 28 | span.additionalInfo { 29 | color: grey; 30 | } 31 | 32 | h1, h2, h3, h4, h5, h6 { 33 | margin-top: 1em; 34 | } 35 | 36 | h2 { 37 | border-bottom: 1px solid #000; 38 | font-size: 26px; 39 | } 40 | h3 { 41 | border-bottom: 1px solid #999; 42 | font-size: 25px; 43 | } 44 | h4 { 45 | font-size: 23px; 46 | margin-bottom: 8px; 47 | } 48 | h5 { 49 | font-size: 20px; 50 | margin-bottom: 5px; 51 | } 52 | h6 { 53 | font-size: 17px; 54 | margin-bottom: 2px; 55 | } 56 | 57 | p { 58 | margin: 8px 10px; 59 | } 60 | 61 | small { 62 | font-size: 60%; 63 | } 64 | 65 | strong { 66 | font-weight: 700; 67 | font-size: 95%; 68 | } 69 | 70 | div.highlight { 71 | border: 1px solid #ccc; 72 | border-radius: 3px; 73 | overflow: auto; 74 | padding: 5px; 75 | } 76 | 77 | div.highlight .hll { background-color: #ffffcc } 78 | div.highlight { background: #ffffff; } 79 | div.highlight .c { color: #008000 } /* Comment */ 80 | div.highlight .err { border: 1px solid #FF0000 } /* Error */ 81 | div.highlight .k { color: #0000ff } /* Keyword */ 82 | div.highlight .cm { color: #008000 } /* Comment.Multiline */ 83 | div.highlight .cp { color: #0000ff } /* Comment.Preproc */ 84 | div.highlight .c1 { color: #008000 } /* Comment.Single */ 85 | div.highlight .cs { color: #008000 } /* Comment.Special */ 86 | div.highlight .ge { font-style: italic } /* Generic.Emph */ 87 | div.highlight .gh { font-weight: bold } /* Generic.Heading */ 88 | div.highlight .gp { font-weight: bold } /* Generic.Prompt */ 89 | div.highlight .gs { font-weight: bold } /* Generic.Strong */ 90 | div.highlight .gu { font-weight: bold } /* Generic.Subheading */ 91 | div.highlight .kc { color: #0000ff } /* Keyword.Constant */ 92 | div.highlight .kd { color: #0000ff } /* Keyword.Declaration */ 93 | div.highlight .kn { color: #0000ff } /* Keyword.Namespace */ 94 | div.highlight .kp { color: #0000ff } /* Keyword.Pseudo */ 95 | div.highlight .kr { color: #0000ff } /* Keyword.Reserved */ 96 | div.highlight .kt { color: #2b91af } /* Keyword.Type */ 97 | div.highlight .s { color: #a31515 } /* Literal.String */ 98 | div.highlight .nc { color: #2b91af } /* Name.Class */ 99 | div.highlight .ow { color: #0000ff } /* Operator.Word */ 100 | div.highlight .sb { color: #a31515 } /* Literal.String.Backtick */ 101 | div.highlight .sc { color: #a31515 } /* Literal.String.Char */ 102 | div.highlight .sd { color: #a31515 } /* Literal.String.Doc */ 103 | div.highlight .s2 { color: #a31515 } /* Literal.String.Double */ 104 | div.highlight .se { color: #a31515 } /* Literal.String.Escape */ 105 | div.highlight .sh { color: #a31515 } /* Literal.String.Heredoc */ 106 | div.highlight .si { color: #a31515 } /* Literal.String.Interpol */ 107 | div.highlight .sx { color: #a31515 } /* Literal.String.Other */ 108 | div.highlight .sr { color: #a31515 } /* Literal.String.Regex */ 109 | div.highlight .s1 { color: #a31515 } /* Literal.String.Single */ 110 | div.highlight .ss { color: #a31515 } /* Literal.String.Symbol */ -------------------------------------------------------------------------------- /examples/rest-user-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json client.js index.html rest-server.js UserModel.js ./ 6 | 7 | RUN npm ci 8 | 9 | EXPOSE 3000 10 | 11 | CMD ["npm", "start"] 12 | -------------------------------------------------------------------------------- /examples/rest-user-server/README.md: -------------------------------------------------------------------------------- 1 | # Simple example of a basic REST api using nohm 2 | 3 | _WARNING_: There are many things in this example that should never be done in a real app. 4 | For example sending out the passwords, don't do that. It's just for demo purposes here. 5 | 6 | ## Requirements 7 | 8 | To run this example you need a local redis database with the default port open. 9 | 10 | The app will create keys in it with the prefix '`rest-user-server-example:`'! 11 | 12 | ## See it live 13 | 14 | An online version can be found at [https://nohm-example.maritz.space/](https://nohm-example.maritz.space/). 15 | 16 | ## Run it 17 | 18 | Install dependencies: 19 | 20 | npm install 21 | 22 | Then run it with 23 | 24 | node rest-server.js 25 | 26 | Go to [http://localhost:3000](http://localhost:3000) 27 | 28 | ## Run it in docker 29 | 30 | Alternatively you can use the docker-compose config to run a contained redis and node app. 31 | 32 | docker-compose up --build 33 | -------------------------------------------------------------------------------- /examples/rest-user-server/UserModel.js: -------------------------------------------------------------------------------- 1 | const nohm = require('nohm').Nohm; 2 | const bcrypt = require('bcrypt'); 3 | 4 | const SALT_ROUNDS = 10; 5 | 6 | /** 7 | * Given a password this creates a hash using bcrypt. 8 | */ 9 | const hashPassword = (password) => { 10 | return bcrypt.hashSync(password, SALT_ROUNDS); 11 | }; 12 | 13 | const PASSWORD_MINLENGTH = 6; // we use this multiple times and store it here to only have one place where it needs to be configured 14 | 15 | /** 16 | * Model definition of a simple user 17 | */ 18 | module.exports = nohm.model('User', { 19 | properties: { 20 | name: { 21 | type: 'string', 22 | unique: true, 23 | validations: [ 24 | 'notEmpty', 25 | { 26 | name: 'length', 27 | options: { 28 | min: 4, 29 | }, 30 | }, 31 | ], 32 | }, 33 | email: { 34 | type: 'string', 35 | validations: [ 36 | { 37 | name: 'email', 38 | options: { 39 | optional: true, // this means only values that pass the email regexp are accepted. BUT it is also optional, thus a falsy value is accepted as well. 40 | }, 41 | }, 42 | ], 43 | }, 44 | createdAt: { 45 | defaultValue: () => Date.now(), 46 | load_pure: true, // make sure the defaultValue is not set on load 47 | type: (_a, _b, oldValue) => parseInt(oldValue, 10), // never change the value after creation 48 | index: true, 49 | }, 50 | updatedAt: { 51 | defaultValue: () => Date.now(), 52 | load_pure: true, // make sure the defaultValue is not set on load 53 | type: 'timestamp', 54 | index: true, 55 | }, 56 | someRegex: { 57 | type: 'string', 58 | validations: [ 59 | { 60 | name: 'regexp', 61 | options: { 62 | regex: /^asd$/, 63 | optional: true, 64 | }, 65 | }, 66 | ], 67 | }, 68 | password: { 69 | load_pure: true, // this ensures that there is no typecasting when loading from the db. 70 | // because when typecasting, we create a new hash of the password. 71 | type: function (value) { 72 | const valueDefined = value && typeof value.length !== 'undefined'; 73 | if (valueDefined && value.length >= PASSWORD_MINLENGTH) { 74 | return hashPassword(value); 75 | } else { 76 | return value; 77 | } 78 | }, 79 | validations: [ 80 | 'notEmpty', 81 | { 82 | name: 'length', 83 | options: { 84 | min: PASSWORD_MINLENGTH, 85 | }, 86 | }, 87 | ], 88 | }, 89 | }, 90 | methods: { 91 | // custom methods we define here to make handling this model easier. 92 | 93 | /** 94 | * Check a given username/password combination for validity. 95 | */ 96 | async login(name, password) { 97 | if (!name || name === '' || !password || password === '') { 98 | return false; 99 | } 100 | const ids = await this.find({ name: name }); 101 | if (ids.length === 0) { 102 | return false; 103 | } else { 104 | await this.load(ids[0]); 105 | return bcrypt.compare(password, this.property('password')); 106 | } 107 | }, 108 | 109 | /** 110 | * This function makes dealing with user input a little easier, since we may not want the user to be able to do things on some fields. 111 | * You can specify a data object that might come from the user and an array containing the fields that should be used from used from the data. 112 | * Optionally you can specify a function that gets called on every field/data pair to do a dynamic check if the data should be included. 113 | * The principle of this might make it into core nohm at some point. 114 | */ 115 | fill(data, fields, fieldCheck) { 116 | const props = {}; 117 | const doFieldCheck = typeof fieldCheck === 'function'; 118 | 119 | fields = Array.isArray(fields) ? fields : Object.keys(data); 120 | 121 | fields.forEach((propKey) => { 122 | if ( 123 | !Object.prototype.hasOwnProperty.call(this.getDefinitions(), propKey) 124 | ) { 125 | return; 126 | } 127 | 128 | if (doFieldCheck) { 129 | const fieldCheckResult = fieldCheck(propKey, data[propKey]); 130 | if (fieldCheckResult === false) { 131 | return; 132 | } else if (fieldCheckResult) { 133 | props[propKey] = fieldCheckResult; 134 | return; 135 | } 136 | } 137 | 138 | props[propKey] = data[propKey]; 139 | }); 140 | console.log('properties now ', props); 141 | this.property(props); 142 | return props; 143 | }, 144 | 145 | /** 146 | * This is a wrapper around fill and save. 147 | * It also makes sure that if there are validation errors. 148 | */ 149 | async store(data) { 150 | console.log('filling', data); 151 | this.fill(data); 152 | this.property('updatedAt', Date.now()); 153 | await this.save(); 154 | }, 155 | 156 | /** 157 | * Wrapper around fill and valid. 158 | * This makes it easier to check user input. 159 | */ 160 | async checkProperties(data, fields) { 161 | this.fill(data, fields); 162 | return this.valid(false, false); 163 | }, 164 | 165 | /** 166 | * safe allProperties 167 | */ 168 | safeAllProperties(stringify) { 169 | const props = this.allProperties(); 170 | delete props.password; 171 | return stringify ? JSON.stringify(props) : props; 172 | }, 173 | }, 174 | }); 175 | -------------------------------------------------------------------------------- /examples/rest-user-server/client.js: -------------------------------------------------------------------------------- 1 | /*global $, nohmValidations, io, confirm, alert */ 2 | $(function () { 3 | const $userList = $('#users tbody'); 4 | let sortField = 'updatedAt'; 5 | let sortDirection = 'ASC'; 6 | let refreshNoticeInterval; 7 | let refreshTime = 0; 8 | const updateUserList = function () { 9 | clearInterval(refreshNoticeInterval); 10 | $('#refreshNotice').remove(); 11 | $.get( 12 | `/User/?sortField=${sortField}&direction=${sortDirection}`, 13 | function (response) { 14 | $userList.empty(); 15 | $.each(response, function (index, item) { 16 | const createdAt = new Date(parseInt(item.createdAt, 10)); 17 | const updatedAt = new Date(parseInt(item.updatedAt, 10)); 18 | $userList.append( 19 | $(` 20 | 21 | ${item.id} 22 | ${item.name} 23 | ${item.email} 24 | ${item.password} 25 | ${createdAt.toLocaleString()} 26 | ${updatedAt.toLocaleString()} 27 | Edit 28 | Remove 29 | 30 | `).data('raw', item), 31 | ); 32 | }); 33 | if (response.length > 5) { 34 | setTimeout(() => { 35 | updateUserList(); 36 | }, 10000); 37 | refreshTime = Date.now() + 10000; 38 | $('#users').after( 39 | 'More than 5 users, will refresh user list in 10 seconds. You should be able to see a remove event log at the bottom the moment it gets removed.', 40 | ); 41 | refreshNoticeInterval = setInterval(() => { 42 | $('#refreshNotice span').text( 43 | ((refreshTime - Date.now()) / 1000).toFixed(0), 44 | ); 45 | }, 500); 46 | } 47 | }, 48 | ); 49 | }; 50 | $userList.on('click', 'tbody a[name="edit-user"]', function (target) { 51 | const data = $(target.currentTarget).parents('tr').data('raw'); 52 | Object.keys(data).forEach((key) => { 53 | if (key !== 'password') { 54 | $(`form input[name="${key}"]`).val(data[key]); 55 | } 56 | }); 57 | }); 58 | $userList.on('click', 'tbody a[name="remove-user"]', function (target) { 59 | const data = $(target.currentTarget).parents('tr').data('raw'); 60 | const confirmed = confirm('Delete user?'); 61 | if (confirmed) { 62 | $.ajax(`/User/${data.id}`, { 63 | data, 64 | type: 'DELETE', 65 | }) 66 | .done(updateUserList) 67 | .fail(failHandler) 68 | .always(finalHandler); 69 | } 70 | }); 71 | $('#users thead').on('click', 'th[data-sortFieldName]', function (target) { 72 | const fieldName = $(target.currentTarget).attr('data-sortFieldName'); 73 | if (sortField === fieldName) { 74 | sortDirection = sortDirection === 'ASC' ? 'DESC' : 'ASC'; 75 | } else { 76 | sortField = fieldName; 77 | sortDirection = 'ASC'; 78 | } 79 | const sortCharacter = sortDirection === 'ASC' ? '▼' : '▲'; 80 | $('#sortMarker') 81 | .detach() 82 | .text(sortCharacter) 83 | .appendTo($(target.currentTarget)); 84 | updateUserList(); 85 | }); 86 | updateUserList(); 87 | 88 | const failHandler = ($form) => { 89 | return (jqXHR) => { 90 | const status = jqXHR.status; 91 | const errorSource = status < 500 ? 'Server validation' : 'Unknown server'; 92 | $form 93 | .find('ul[name=errors]') 94 | .append(`
  • ${errorSource} error: ${jqXHR.responseText}`); 95 | }; 96 | }; 97 | const finalHandler = ($form) => { 98 | return () => { 99 | $form.find('fieldset').attr('disabled', false); 100 | }; 101 | }; 102 | 103 | const $editForm = $('form#edit'); 104 | $editForm.submit(function (e) { 105 | e.preventDefault(); 106 | 107 | const data = {}; 108 | const id = $editForm.find('input[name="id"]').val(); 109 | const idSet = id.length !== 0; 110 | $editForm.find('input').each(function () { 111 | const $this = $(this); 112 | if ($this.attr('type') === 'submit') { 113 | return; 114 | } 115 | const name = $this.attr('name'); 116 | const value = $this.val(); 117 | if (name === 'password' && idSet && value.length === 0) { 118 | // when id is set, we don't need a new password every time 119 | return; 120 | } 121 | data[name] = value; 122 | }); 123 | nohmValidations.validate('User', data).then((validation) => { 124 | $editForm.find('ul[name=errors]').empty(); 125 | if (validation.result) { 126 | $editForm.find('fieldset').attr('disabled', true); 127 | if (idSet) { 128 | $.ajax(`/User/${id}`, { 129 | data, 130 | type: 'PUT', 131 | }) 132 | .done(updateUserList) 133 | .fail(failHandler($editForm)) 134 | .always(finalHandler($editForm)); 135 | } else { 136 | $.post('/User/', data) 137 | .done(updateUserList) 138 | .fail(failHandler($editForm)) 139 | .always(finalHandler($editForm)); 140 | } 141 | } else { 142 | $.each(validation.errors, function (index, error) { 143 | $editForm 144 | .find('ul[name=errors]') 145 | .append( 146 | '
  • Client validation error: ' + 147 | index + 148 | ': ' + 149 | JSON.stringify(error), 150 | ); 151 | }); 152 | } 153 | }); 154 | }); 155 | 156 | const $loginForm = $('form#login'); 157 | $loginForm.submit(function (e) { 158 | e.preventDefault(); 159 | 160 | const data = { 161 | name: $loginForm.find('input[name="name"]').val(), 162 | password: $loginForm.find('input[name="password"]').val(), 163 | }; 164 | nohmValidations.validate('User', data).then((validation) => { 165 | $loginForm.find('ul[name=errors]').empty(); 166 | if (validation.result) { 167 | $loginForm.find('fieldset').attr('disabled', true); 168 | $.post('/User/login', data) 169 | .done(() => { 170 | alert('Login worked!'); 171 | }) 172 | .fail(failHandler($loginForm)) 173 | .always(finalHandler($loginForm)); 174 | } else { 175 | $.each(validation.errors, function (index, error) { 176 | $loginForm 177 | .find('[name="errors"]') 178 | .append( 179 | '
  • Client validation error: ' + 180 | index + 181 | ': ' + 182 | JSON.stringify(error), 183 | ); 184 | }); 185 | } 186 | }); 187 | }); 188 | 189 | const $eventLog = $('#eventlog'); 190 | const socket = io(); 191 | socket.on('nohmEvent', function (msg) { 192 | $eventLog.append($('
  • ').text(JSON.stringify(msg))); 193 | updateUserList(); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /examples/rest-user-server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | nohm-example-redis: 5 | image: redis:latest 6 | restart: on-failure:5 7 | networks: 8 | - inside 9 | 10 | nohm-example: 11 | build: 12 | context: . 13 | dockerfile: ./Dockerfile 14 | restart: on-failure:5 15 | environment: 16 | REDIS_HOST: nohm-example-redis 17 | labels: 18 | - "traefik.enable=${NOHM_EXAMPLE_NETWORK_ENABLE:-true}" 19 | - "traefik.docker.network=${NOHM_EXAMPLE_NETWORK:-nohm}" 20 | - "traefik.http.routers.nohmexample.entrypoints=${NOHM_EXAMPLE_ENTRYPOINT:-web}" 21 | - "traefik.http.routers.nohmexample.rule=Host(`${NOHM_EXAMPLE_DOMAIN:-nohm-example.maritz.space}`)" 22 | - "traefik.http.services.nohmexample.loadbalancer.server.port=${NOHM_EXAMPLE_PORT:-3000}" 23 | networks: 24 | - outside 25 | - inside 26 | 27 | networks: 28 | outside: 29 | name: ${NOHM_EXAMPLE_NETWORK:-nohm} 30 | inside: 31 | -------------------------------------------------------------------------------- /examples/rest-user-server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nohm REST User server example 5 | 6 | 47 | 48 | 49 | 50 |

    nohm example REST server

    51 |

    52 | This is an example app that demonstrates some nohm features. 53 |
    54 | Warning: This app is not a real app and not secure. Do 55 | NOT enter real data. 56 |

    57 |

    58 | You can create, update and remove users as much as you want. All connected browsers will instantly see all updates. 59 |
    You can test the login method by entering the name and original password in the second form. 60 |
    An interval is running on the server that checks the number of users every 5 seconds. If there are more than 5, the 61 | least recently updated are deleted so that only 5 remain. 62 |
    You can also get get the user list sorted (server-side) by clicking on some of the table head columns. 63 |

    64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
    ID (uuid)NameEmailPassword (hashed)Created atUpdated at 72 | 73 | EditRemove
    81 | 82 |
    83 |
    84 | Errors: 85 |
      86 | Create or edit user 87 |
      ID: 88 | (fill to change existing) 89 |
      Name: 90 | 91 |
      Email: 92 | (optional) 93 |
      Password: 94 | (optional when id set) 95 |
      96 | 97 |
      98 |
      99 | 100 |
      101 |
      102 | Errors: 103 |
        104 | Test login 105 |
        Name: 106 | 107 |
        Password: 108 | 109 |
        110 | 111 |
        112 |
        113 | 114 |

        nohm event log

        115 |

        116 | These events are redis pubsub, so even if you have multiple instances of the node.js app using nohm, they all get the same 117 | events. 118 |
        You can also open another tab or window of this page, do some things there and see the same events end up here. 119 |

        120 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /examples/rest-user-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nohm-example-usage", 3 | "version": "2.2.0", 4 | "description": "small nohm example app", 5 | "author": "Moritz Peters", 6 | "private": true, 7 | "main": "rest-server.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/maritz/nohm.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/maritz/nohm/issues" 14 | }, 15 | "scripts": { 16 | "start": "node rest-server.js" 17 | }, 18 | "dependencies": { 19 | "bcrypt": "^5.0.1", 20 | "body-parser": "^1.19.2", 21 | "express": "^4.17.3", 22 | "nohm": "^3.0.0", 23 | "socket.io": "^4.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/rest-user-server/rest-server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const Nohm = require('nohm').Nohm; 3 | const UserModel = require('./UserModel.js'); 4 | const redis = require('redis'); 5 | const fs = require('fs'); 6 | const bodyParser = require('body-parser'); 7 | const http = require('http'); 8 | const socketIo = require('socket.io'); 9 | 10 | const redisOptions = { 11 | host: process.env.REDIS_HOST || 'localhost', 12 | port: process.env.REDIS_PORT, 13 | }; 14 | 15 | const redisClient = redis.createClient(redisOptions); 16 | const pubSubClient = redis.createClient(redisOptions); 17 | 18 | redisClient.once('connect', async () => { 19 | Nohm.setPrefix('rest-user-server-example'); 20 | Nohm.setClient(redisClient); 21 | 22 | const app = express(); 23 | const server = http.Server(app); 24 | 25 | const io = socketIo(server); 26 | 27 | await Nohm.setPubSubClient(pubSubClient); 28 | Nohm.setPublish(true); 29 | 30 | const subscriber = async (eventName) => { 31 | await new UserModel().subscribe(eventName, (payload) => { 32 | console.log('Emitting event "%s"', eventName); 33 | io.emit('nohmEvent', { 34 | eventName, 35 | payload, 36 | }); 37 | }); 38 | }; 39 | ['update', 'create', 'save', 'remove', 'link', 'unlink'].forEach(subscriber); 40 | 41 | app.use(bodyParser.urlencoded({ extended: false })); 42 | 43 | app.use( 44 | Nohm.middleware( 45 | [ 46 | { 47 | model: UserModel, 48 | }, 49 | ], 50 | /* 51 | // doesn't work yet in v2.0.0 52 | { 53 | extraFiles: __dirname + '/custom_validations.js', 54 | },*/ 55 | ), 56 | ); 57 | 58 | setInterval(() => { 59 | const maxUsers = process.env.MAX_USERS || 5; 60 | const user = new UserModel(); 61 | redisClient.SCARD(`${user.prefix('idsets')}`, async (err, number) => { 62 | if (err) { 63 | console.error('SCARD failed:', err); 64 | return; 65 | } 66 | if (number > maxUsers) { 67 | const sortedIds = await UserModel.sort({ 68 | field: 'updatedAt', 69 | direction: 'ASC', 70 | }); 71 | sortedIds.splice(-1 * maxUsers); 72 | console.log('Auto removing', sortedIds); 73 | await Promise.all(sortedIds.map((id) => UserModel.remove(id))); 74 | } 75 | }); 76 | }, 5000); 77 | 78 | app.get('/User/', async function (req, res, next) { 79 | try { 80 | const defaultSortField = 'updatedAt'; 81 | const allowedSortFields = ['name', 'email', 'createdAt', 'updatedAt']; 82 | let sortField = req.query.sortField; 83 | if (!sortField || !allowedSortFields.includes(sortField)) { 84 | sortField = defaultSortField; 85 | } 86 | const direction = req.query.direction === 'DESC' ? 'DESC' : 'ASC'; 87 | // the sorting could easily be done after loading here, but for demo purposes we use nohm functionality 88 | const sortedIds = await UserModel.sort({ 89 | field: sortField, 90 | direction, 91 | }); 92 | const users = await UserModel.loadMany(sortedIds); 93 | const response = users.map((user) => { 94 | return { 95 | id: user.id, 96 | name: user.property('name'), 97 | email: user.property('email'), 98 | createdAt: user.property('createdAt'), 99 | updatedAt: user.property('updatedAt'), 100 | password: user.property('password'), // WARNING: obviously in a real application NEVER give this out. This is just to test/demo the fill() method 101 | }; 102 | }); 103 | res.json(response); 104 | } catch (err) { 105 | next(err); 106 | } 107 | }); 108 | 109 | app.post('/User/', async function (req, res, next) { 110 | try { 111 | const data = { 112 | name: req.body.name, 113 | password: req.body.password, 114 | email: req.body.email, 115 | }; 116 | const user = await Nohm.factory('User'); // alternatively new UserModel(); like in put 117 | await user.store(data); 118 | res.json({ result: 'success', data: user.safeAllProperties() }); 119 | } catch (err) { 120 | next(err); 121 | } 122 | }); 123 | 124 | app.put('/User/:id', async function (req, res, next) { 125 | try { 126 | const data = { 127 | name: req.body.name, 128 | password: req.body.password, 129 | email: req.body.email, 130 | }; 131 | const user = new UserModel(); // alternatively Nohm.factory('User'); like in post 132 | await user.load(req.params.id); 133 | console.log('loaded user', user.property('updatedAt')); 134 | await user.store(data); 135 | res.json({ result: 'success', data: user.safeAllProperties() }); 136 | } catch (err) { 137 | next(err); 138 | } 139 | }); 140 | 141 | app.post('/User/login', async function (req, res) { 142 | const user = new UserModel(); // alternatively Nohm.factory('User'); like in post 143 | const loginSuccess = await user.login(req.body.name, req.body.password); 144 | if (loginSuccess) { 145 | res.json({ result: 'success' }); 146 | } else { 147 | res.status(401); 148 | res.send('Wrong login'); 149 | } 150 | }); 151 | 152 | app.delete('/User/:id', async function (req, res, next) { 153 | try { 154 | await UserModel.remove(req.params.id); 155 | res.status(204); 156 | res.end(); 157 | } catch (err) { 158 | next(err); 159 | } 160 | }); 161 | 162 | app.get('/', function (req, res) { 163 | res.send(fs.readFileSync(__dirname + '/index.html', 'utf-8')); 164 | }); 165 | 166 | app.get('/client.js', function (req, res) { 167 | res.sendFile(__dirname + '/client.js'); 168 | }); 169 | 170 | // eslint-disable-next-line no-unused-vars 171 | app.use(function (err, _req, res, _next) { 172 | // error handler 173 | res.status(500); 174 | let errData = err.message; 175 | if (err instanceof Error) { 176 | if (err.message === 'not found') { 177 | res.status(404); 178 | } 179 | } 180 | if (err instanceof Nohm.ValidationError) { 181 | res.status(400); 182 | errData = err.errors; 183 | } 184 | if (res.statusCode >= 500) { 185 | console.error('Server error:', err); 186 | } 187 | res.send({ result: 'error', data: errData }); 188 | }); 189 | 190 | server.listen(3000, () => { 191 | console.log('listening on 3000'); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /extra/models.js: -------------------------------------------------------------------------------- 1 | var nohm = require('../').Nohm; 2 | 3 | exports.user = nohm.model('UserMockup', { 4 | properties: { 5 | name: { 6 | type: 'string', 7 | defaultValue: 'test', 8 | validations: [ 9 | 'notEmpty' 10 | ] 11 | }, 12 | key: { 13 | type: 'integer', 14 | index: true 15 | } 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /extra/stress.js: -------------------------------------------------------------------------------- 1 | const nohm = require(__dirname + '/../').Nohm; 2 | let iterations = 10000; 3 | 4 | let redisOptions = {}; 5 | 6 | process.argv.forEach(function(val, index) { 7 | if (val === '--nohm-prefix') { 8 | redisOptions.prefix = process.argv[index + 1]; 9 | } 10 | if (val === '--redis-host') { 11 | redisOptions.redis_host = process.argv[index + 1]; 12 | } 13 | if (val === '--redis-port') { 14 | redisOptions.redis_port = process.argv[index + 1]; 15 | } 16 | if (val === '--redis-auth') { 17 | redisOptions.redis_auth = process.argv[index + 1]; 18 | } 19 | if (val === '--iterations') { 20 | iterations = parseInt(process.argv[index + 1], 10); 21 | if (isNaN(iterations)) { 22 | throw new Error( 23 | `Invalid iterations argument: ${ 24 | process.argv[index + 1] 25 | }. Must be a number.`, 26 | ); 27 | } 28 | } 29 | }); 30 | 31 | let redisClient; 32 | 33 | const main = () => { 34 | console.info('Connected to redis.'); 35 | stress().catch((error) => { 36 | console.error('An error occurred during benchmarking:', error); 37 | process.exit(1); 38 | }); 39 | }; 40 | 41 | if (process.env.NOHM_TEST_IOREDIS == 'true') { 42 | console.info('Using ioredis for stress test'); 43 | const Redis = require('ioredis'); 44 | 45 | redisClient = new Redis({ 46 | port: redisOptions.redis_port, 47 | host: redisOptions.redis_host, 48 | password: redisOptions.redis_auth, 49 | }); 50 | redisClient.once('ready', main); 51 | } else { 52 | console.info('Using node_redis for stress test'); 53 | redisClient = require('redis').createClient( 54 | redisOptions.redis_port, 55 | redisOptions.redis_host, 56 | { 57 | auth_pass: redisOptions.redis_auth, 58 | }, 59 | ); 60 | redisClient.once('connect', main); 61 | } 62 | 63 | const stress = async () => { 64 | console.log( 65 | `Starting stress test - saving and then updating ${iterations} models in parallel.`, 66 | ); 67 | 68 | nohm.setPrefix(redisOptions.prefix || 'nohm-stress-test'); 69 | nohm.setClient(redisClient); 70 | 71 | var models = require(__dirname + '/models'); 72 | var UserModel = models.user; 73 | 74 | try { 75 | await nohm.purgeDb(); 76 | } catch (err) { 77 | console.error('Failed to purge DB before starting.', err); 78 | process.exit(1); 79 | } 80 | 81 | var counter = 0; 82 | var start = Date.now(); 83 | var startUpdates = 0; 84 | var users = []; 85 | 86 | var callback = function() { 87 | counter++; 88 | if (counter >= iterations) { 89 | const updateTime = Date.now() - startUpdates; 90 | const timePerUpdate = (iterations / updateTime) * 1000; 91 | console.log( 92 | `${updateTime}ms for ${counter} parallel User updates, ${timePerUpdate.toFixed( 93 | 2, 94 | )} updates/second`, 95 | ); 96 | console.log(`Total time: ${Date.now() - start}ms`); 97 | console.log('Memory usage after', process.memoryUsage()); 98 | redisClient.scard( 99 | `${nohm.prefix.idsets}${new UserModel().modelName}`, 100 | async (err, numUsers) => { 101 | if (err) { 102 | console.error( 103 | 'Error while trying to check number of saved users.', 104 | err, 105 | ); 106 | process.exit(1); 107 | } 108 | if (numUsers !== iterations) { 109 | console.error( 110 | `Number of users is wrong. ${numUsers} !== ${iterations}`, 111 | `${nohm.prefix.idsets}${UserModel.modelName}`, 112 | ); 113 | process.exit(1); 114 | } 115 | try { 116 | await nohm.purgeDb(); 117 | } catch (err) { 118 | console.error('Failed to purge DB during cleanup.', err); 119 | process.exit(1); 120 | } 121 | console.log('Done.'); 122 | redisClient.quit(); 123 | process.exit(); 124 | }, 125 | ); 126 | } 127 | }; 128 | 129 | function errorCallback(err) { 130 | console.log('update error: ' + err); 131 | process.exit(); 132 | } 133 | 134 | function update() { 135 | startUpdates = Date.now(); 136 | console.log('Saves done, starting updates'); 137 | counter = 0; 138 | for (var i = 0, len = users.length; i < len; i++) { 139 | users[i].property({ name: 'Bob' + i, key: i + i }); 140 | users[i] 141 | .save() 142 | .then(callback) 143 | .catch(errorCallback); 144 | } 145 | } 146 | 147 | var saveCallback = function(err) { 148 | if (err) { 149 | console.log('error: ' + err); 150 | process.exit(); 151 | } 152 | counter++; 153 | if (counter >= iterations) { 154 | saveCallback = null; 155 | const saveTime = Date.now() - start; 156 | const timePerSave = (iterations / saveTime) * 1000; 157 | console.log( 158 | `${saveTime}ms for ${counter} parallel User saves, ${timePerSave.toFixed( 159 | 2, 160 | )} saves/second`, 161 | ); 162 | update(); 163 | } 164 | }; 165 | 166 | for (var i = 0; i < iterations; i++) { 167 | var user = new UserModel(); 168 | user.property({ name: 'Bob', key: i }); 169 | user 170 | .save() 171 | .then(saveCallback) 172 | .catch(errorCallback); 173 | users.push(user); 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": false 4 | }, 5 | "source": { 6 | "include": ["./tsOut", "./ts/universalValidators.js", "./README.md"], 7 | "includePattern": ".js$", 8 | "excludePattern": "(node_modules/|docs)" 9 | }, 10 | "plugins": ["plugins/markdown"], 11 | "opts": { 12 | "template": "node_modules/docdash/", 13 | "encoding": "utf8", 14 | "destination": "docs/api/", 15 | "recurse": true, 16 | "verbose": true 17 | }, 18 | "templates": { 19 | "cleverLinks": false, 20 | "monospaceLinks": false, 21 | "default": { 22 | "includeDate": false 23 | } 24 | }, 25 | "docdash": { 26 | "static": false, 27 | "sort": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nohm", 3 | "version": "3.0.0", 4 | "description": "redis ORM (Object relational mapper)", 5 | "license": "MIT", 6 | "engines": { 7 | "node": ">=8" 8 | }, 9 | "main": "./tsOut/index.js", 10 | "directories": { 11 | "lib": "./ts" 12 | }, 13 | "keywords": [ 14 | "redis", 15 | "orm", 16 | "database", 17 | "pubsub", 18 | "typescript", 19 | "odm" 20 | ], 21 | "types": "./tsOut/index.d.ts", 22 | "files": [ 23 | "docs/index.md", 24 | "ts/", 25 | "tsOut/", 26 | "CHANGELOG.md", 27 | "README.md", 28 | "LICENSE" 29 | ], 30 | "scripts": { 31 | "test": "nyc ava --timeout=20s", 32 | "test:watch": "ava --watch --timeout=5s", 33 | "coverage:failIfLow": "nyc check-coverage --lines 90 --functions 90 --branches 80", 34 | "coverage:coveralls": "nyc report --reporter=text-lcov | coveralls", 35 | "lint": "tslint --project ./ && eslint test/ ts/universalValidators.js", 36 | "lint:auto-fix": "tslint--project ./ --fix", 37 | "prebuild": "rimraf tsOut/", 38 | "build": "npm run lint && tsc --project ./", 39 | "build:watch": "tsc --project ./ --watch --pretty", 40 | "predev": "npm run build", 41 | "dev": "concurrently --names build,test -k \"npm run build:watch\" \"npm run test:watch\" -c cyan,blue --handle-input", 42 | "prepublishOnly": "npm run build && npm run test && pkg-ok", 43 | "generateDocs": "rimraf docs/api/ && jsdoc -c jsdoc.json", 44 | "prerelease": "npm run generateDocs", 45 | "release": "git add docs/api/ && standard-version -a" 46 | }, 47 | "dependencies": { 48 | "debug": "^4.3.3", 49 | "ioredis": "^4.28.5", 50 | "lodash": "^4.17.11", 51 | "redis": "^3.0.2", 52 | "traverse": "^0.6.6", 53 | "uuid": "^8.3.2" 54 | }, 55 | "devDependencies": { 56 | "@abraham/pkg-ok": "^3.0.0-next.2", 57 | "@types/debug": "^4.1.7", 58 | "@types/express": "^4.17.13", 59 | "@types/ioredis": "^4.28.8", 60 | "@types/lodash": "^4.14.179", 61 | "@types/node": "<=14.15.0", 62 | "@types/redis": "^2.8.28", 63 | "@types/traverse": "^0.6.32", 64 | "@types/uuid": "^8.3.4", 65 | "ava": "^4.0.1", 66 | "concurrently": "^7.0.0", 67 | "coveralls": "^3.1.1", 68 | "docdash": "^1.2.0", 69 | "eslint": "^8.10.0", 70 | "jsdoc": "^3.6.10", 71 | "nodemon": "^2.0.15", 72 | "nyc": "^15.1.0", 73 | "rimraf": "^3.0.2", 74 | "standard-version": "^9.3.2", 75 | "testdouble": "^3.16.4", 76 | "ts-node": "^10.5.0", 77 | "tslint": "^6.1.3", 78 | "typescript": "3.4.5" 79 | }, 80 | "author": "Moritz Peters", 81 | "repository": { 82 | "type": "git", 83 | "url": "https://github.com/maritz/nohm.git" 84 | }, 85 | "bugs": { 86 | "url": "https://github.com/maritz/nohm/issues" 87 | }, 88 | "contributors": [ 89 | { 90 | "name": "Pier Paolo Ramon", 91 | "url": "https://github.com/yuchi" 92 | } 93 | ], 94 | "ava": { 95 | "extensions": [ 96 | "ts" 97 | ], 98 | "files": [ 99 | "test/*.test.ts", 100 | "ts/typescript.test.ts" 101 | ], 102 | "require": [ 103 | "ts-node/register/transpile-only" 104 | ] 105 | }, 106 | "standard-version": { 107 | "types": [ 108 | { 109 | "type": "feat", 110 | "section": "Features" 111 | }, 112 | { 113 | "type": "fix", 114 | "section": "Bug Fixes" 115 | }, 116 | { 117 | "type": "chore", 118 | "section": "Other" 119 | }, 120 | { 121 | "type": "docs", 122 | "hidden": true 123 | }, 124 | { 125 | "type": "style", 126 | "hidden": true 127 | }, 128 | { 129 | "type": "refactor", 130 | "hidden": true 131 | }, 132 | { 133 | "type": "perf", 134 | "section": "Other" 135 | }, 136 | { 137 | "type": "test", 138 | "hidden": true 139 | } 140 | ] 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/custom_methods.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as td from 'testdouble'; 3 | 4 | import { nohm } from '../ts'; 5 | import * as args from './testArgs'; 6 | import { sleep } from './helper'; 7 | 8 | test.before(async () => { 9 | await args.setClient(nohm, args.redis); 10 | }); 11 | 12 | const normalModel = nohm.model('normalModel', { 13 | properties: { 14 | name: { 15 | type: 'string', 16 | }, 17 | }, 18 | }); 19 | 20 | const MethodOverwrite = nohm.model('methodOverwrite', { 21 | properties: { 22 | name: { 23 | defaultValue: 'test', 24 | type: 'string', 25 | unique: true, 26 | validations: ['notEmpty'], 27 | }, 28 | }, 29 | methods: { 30 | // tslint:disable-next-line:no-shadowed-variable 31 | test: function test() { 32 | return this.property('name'); 33 | }, 34 | }, 35 | }); 36 | 37 | test('methods', async (t) => { 38 | const methodOverwrite = new MethodOverwrite(); 39 | 40 | t.is( 41 | // @ts-ignore _super_prop is dynamically added 42 | typeof methodOverwrite.test, 43 | 'function', 44 | 'Adding a method to a model did not create that method on a new instance.', 45 | ); 46 | t.is( 47 | // @ts-ignore _super_prop is dynamically added 48 | methodOverwrite.test(), 49 | methodOverwrite.property('name'), 50 | "The test method did not work properly. (probably doesn't have the correct `this`.", 51 | ); 52 | }); 53 | 54 | test('methodsSuper', async (t) => { 55 | const warnDouble = td.replace(global.console, 'warn'); 56 | 57 | const methodOverwriteSuperMethod = nohm.model('methodOverwriteSuperMethod', { 58 | properties: { 59 | name: { 60 | defaultValue: 'test', 61 | type: 'string', 62 | unique: true, 63 | validations: ['notEmpty'], 64 | }, 65 | }, 66 | methods: { 67 | prop: function prop(name) { 68 | if (name === 'super') { 69 | // @ts-ignore _super_prop is dynamically added, not elegant but the only way this works right now 70 | return this._super_prop('name'); 71 | } else { 72 | // @ts-ignore _super_prop is dynamically added 73 | return this._super_prop.apply(this, arguments, 0); 74 | } 75 | }, 76 | }, 77 | }); 78 | 79 | const methodOverwrite = new methodOverwriteSuperMethod(); 80 | 81 | // wait 100ms because the warning from the constructor is delayed as well. 82 | await sleep(100); 83 | td.verify( 84 | warnDouble, 85 | warnDouble( 86 | '\x1b[31m%s\x1b[0m', 87 | `WARNING: Overwriting existing property/method 'prop' in 'methodOverwriteSuperMethod' because of method definition.`, 88 | ), 89 | ); 90 | 91 | t.is( 92 | typeof methodOverwrite.prop, 93 | 'function', 94 | 'Overwriting a method in a model definition did not create that method on a new instance.', 95 | ); 96 | t.is( 97 | // @ts-ignore _super_prop is dynamically added 98 | typeof methodOverwrite._super_prop, 99 | 'function', 100 | 'Overwriting a method in a model definition did not create the _super_ method on a new instance.', 101 | ); 102 | t.is( 103 | methodOverwrite.prop('super'), 104 | methodOverwrite.property('name'), 105 | 'The super test method did not work properly.', 106 | ); 107 | td.verify( 108 | warnDouble, 109 | warnDouble( 110 | '\x1b[31m%s\x1b[0m', 111 | 'DEPRECATED: Usage of NohmModel.prop() is deprecated, use NohmModel.property() instead.', 112 | ), 113 | ); 114 | methodOverwrite.prop('name', 'methodTest'); 115 | td.verify( 116 | warnDouble, 117 | warnDouble( 118 | '\x1b[31m%s\x1b[0m', 119 | 'DEPRECATED: Usage of NohmModel.prop() is deprecated, use NohmModel.property() instead.', 120 | ), 121 | ); 122 | 123 | t.is( 124 | methodOverwrite.property('name'), 125 | 'methodTest', 126 | 'The super test method did not properly handle arguments', 127 | ); 128 | td.reset(); 129 | }); 130 | 131 | test('no super method if none needed', async (t) => { 132 | const instance = new normalModel(); 133 | 134 | t.true( 135 | !instance.hasOwnProperty('_super_test'), 136 | 'Defining a method that does not overwrite a nohm method created a _super_.', 137 | ); 138 | }); 139 | -------------------------------------------------------------------------------- /test/custom_validations.js: -------------------------------------------------------------------------------- 1 | exports.customValidationFile = function(value) { 2 | return Promise.resolve(value === 'customValidationFile'); 3 | }; 4 | 5 | exports.instanceValidation = function(value, opt) { 6 | return Promise.resolve(this.p(opt.property) === value); 7 | }; 8 | -------------------------------------------------------------------------------- /test/custom_validations2.js: -------------------------------------------------------------------------------- 1 | exports.customValidationFileTimesTwo = function(value) { 2 | return Promise.resolve(value === 'customValidationFileTimesTwo'); 3 | }; 4 | -------------------------------------------------------------------------------- /test/exports.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import * as NohmAll from '../ts'; 4 | 5 | // tslint:disable-next-line:no-duplicate-imports 6 | import NohmDefault, { 7 | Nohm, 8 | NohmClass, 9 | NohmModel, 10 | LinkError, 11 | ValidationError, 12 | nohm, 13 | } from '../ts'; 14 | 15 | test('exports the correct objects', (t) => { 16 | t.snapshot(NohmAll); 17 | t.snapshot(NohmDefault); 18 | t.snapshot(Nohm); 19 | t.is('function', typeof NohmClass); 20 | t.is('function', typeof NohmModel); 21 | t.is('function', typeof LinkError); 22 | t.is('function', typeof ValidationError); 23 | t.snapshot(nohm); 24 | }); 25 | -------------------------------------------------------------------------------- /test/helper.ts: -------------------------------------------------------------------------------- 1 | import { RedisClient } from 'redis'; 2 | 3 | export const cleanUp = ( 4 | redis: RedisClient, 5 | prefix: string, 6 | cb: (err?: Error | string) => unknown, 7 | ) => { 8 | redis.keys(prefix + '*', (err, keys) => { 9 | if (err) { 10 | cb(err); 11 | } 12 | if (!keys || keys.length === 0) { 13 | return cb(); 14 | } 15 | const len = keys.length; 16 | let k = 0; 17 | const deleteCallback = () => { 18 | k = k + 1; 19 | if (k === len) { 20 | cb(); 21 | } 22 | }; 23 | for (let i = 0; i < len; i++) { 24 | redis.del(keys[i], deleteCallback); 25 | } 26 | }); 27 | }; 28 | 29 | export const cleanUpPromise = (redis: RedisClient, prefix: string) => { 30 | return new Promise((resolve) => { 31 | cleanUp(redis, prefix, resolve); 32 | }); 33 | }; 34 | 35 | export const sleep = (time = 100) => { 36 | return new Promise((resolve) => { 37 | setTimeout(resolve, time); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /test/meta.test.ts: -------------------------------------------------------------------------------- 1 | import anyTest, { TestInterface } from 'ava'; 2 | import { createHash } from 'crypto'; 3 | import * as traverse from 'traverse'; 4 | 5 | import { nohm } from '../ts'; 6 | 7 | import * as args from './testArgs'; 8 | import { cleanUpPromise } from './helper'; 9 | import NohmModel from '../ts/model'; 10 | import { get, hget } from '../ts/typed-redis-helper'; 11 | 12 | const test = anyTest as TestInterface<{ 13 | users: Array>; 14 | userIds: Array; 15 | }>; 16 | 17 | const redis = args.redis; 18 | 19 | const prefix = args.prefix + 'meta'; 20 | 21 | nohm.model('UserMetaMockup', { 22 | properties: { 23 | name: { 24 | index: true, 25 | defaultValue: 'testName', 26 | type: 'string', 27 | validations: ['notEmpty'], 28 | }, 29 | email: { 30 | defaultValue: 'testMail@test.de', 31 | type: 'string', 32 | unique: true, 33 | validations: [ 34 | 'email', 35 | (values) => { 36 | return Promise.resolve(values !== 'thisisnoemail'); 37 | }, 38 | ], 39 | }, 40 | gender: { 41 | type: 'string', 42 | }, 43 | json: { 44 | defaultValue: '{}', 45 | type: 'json', 46 | }, 47 | number: { 48 | index: true, 49 | defaultValue: 1, 50 | type: 'integer', 51 | }, 52 | bool: { 53 | defaultValue: false, 54 | type: 'bool', 55 | }, 56 | }, 57 | idGenerator: 'increment', 58 | }); 59 | 60 | nohm.model('CommentMetaMockup', { 61 | properties: { 62 | name: { 63 | index: true, 64 | defaultValue: 'testName', 65 | type: 'string', 66 | validations: ['notEmpty'], 67 | }, 68 | }, 69 | idGenerator() { 70 | return Promise.resolve(+new Date()); 71 | }, 72 | }); 73 | 74 | const createUsers = async >( 75 | props: Array, 76 | ): Promise<[Array, Array]> => { 77 | const promises = props.map(async (prop) => { 78 | const user: TModel = await nohm.factory('UserMetaMockup'); 79 | user.property(prop); 80 | await user.save(); 81 | return user; 82 | }); 83 | 84 | const users = await Promise.all(promises); 85 | const ids = users.map((user) => { 86 | return user.id as string; 87 | }); 88 | return [users, ids]; 89 | }; 90 | 91 | test.before(async (t) => { 92 | nohm.setPrefix(prefix); 93 | await args.setClient(nohm, redis); 94 | await cleanUpPromise(redis, prefix); 95 | const [users, userIds] = await createUsers([ 96 | { 97 | name: 'metatestsone', 98 | email: 'metatestsone@hurgel.de', 99 | gender: 'male', 100 | number: 3, 101 | }, 102 | { 103 | name: 'metateststwo', 104 | email: 'numericindextest2@hurgel.de', 105 | gender: 'male', 106 | number: 4, 107 | }, 108 | ]); 109 | const comment = await nohm.factory('CommentMetaMockup'); 110 | users[0].link(comment); 111 | await users[0].save(); 112 | t.context.users = users; 113 | t.context.userIds = userIds; 114 | }); 115 | 116 | test.after(async () => { 117 | await cleanUpPromise(redis, prefix); 118 | }); 119 | 120 | const stringifyFunctions = (obj) => { 121 | return traverse(obj).map((x) => { 122 | if (typeof x === 'function') { 123 | return String(x); 124 | } else { 125 | return x; 126 | } 127 | }); 128 | }; 129 | 130 | test('version', async (t) => { 131 | const user = await nohm.factory('UserMetaMockup'); 132 | 133 | const hash = createHash('sha1'); 134 | 135 | hash.update(JSON.stringify(user.meta.properties)); 136 | hash.update(JSON.stringify(user.modelName)); 137 | // any because we are accessing private user.options 138 | hash.update((user as any).options.idGenerator.toString()); 139 | 140 | const version = await get(redis, prefix + ':meta:version:UserMetaMockup'); 141 | t.is(hash.digest('hex'), version, 'Version of the metadata did not match.'); 142 | }); 143 | 144 | test('version in instance', async (t) => { 145 | const user = await nohm.factory('UserMetaMockup'); 146 | 147 | const version = await hget( 148 | redis, 149 | prefix + ':hash:UserMetaMockup:1', 150 | '__meta_version', 151 | ); 152 | t.is( 153 | version, 154 | user.meta.version, 155 | 'Version of the instance did not match meta data.', 156 | ); 157 | }); 158 | 159 | test('meta callback and setting meta.inDb', (t) => { 160 | return new Promise((resolve) => { 161 | const testModel = nohm.model('TestVersionMetaMockup', { 162 | properties: { 163 | name: { 164 | defaultValue: 'testProperty', 165 | type: 'string', 166 | }, 167 | }, 168 | metaCallback(err, version) { 169 | t.is(err, null, 'Meta version callback had an error.'); 170 | t.true(testInstance.meta.inDb, 'Meta version inDb was not false.'); 171 | t.truthy(version, 'No version in meta.inDb callback'); 172 | resolve(); 173 | }, 174 | }); 175 | 176 | const testInstance = new testModel(); 177 | t.false( 178 | testInstance.meta.inDb, 179 | 'Meta version inDb was not false directly after instantiation.', 180 | ); 181 | }); 182 | }); 183 | 184 | test('idGenerator', async (t) => { 185 | const user = await nohm.factory('UserMetaMockup'); 186 | const comment = await nohm.factory('CommentMetaMockup'); 187 | 188 | const userIdGenerator = await get( 189 | redis, 190 | prefix + ':meta:idGenerator:UserMetaMockup', 191 | ); 192 | t.is( 193 | userIdGenerator, 194 | // any because we are accessing private user.options 195 | (user as any).options.idGenerator.toString(), 196 | 'idGenerator of the user did not match.', 197 | ); 198 | 199 | const commentIdGenerator = await get( 200 | redis, 201 | prefix + ':meta:idGenerator:CommentMetaMockup', 202 | ); 203 | t.is( 204 | commentIdGenerator, 205 | // any because we are accessing private user.options 206 | (comment as any).options.idGenerator.toString(), 207 | 'idGenerator of the comment did not match.', 208 | ); 209 | }); 210 | 211 | test('properties', async (t) => { 212 | const user = await nohm.factory('UserMetaMockup'); 213 | const comment = await nohm.factory('CommentMetaMockup'); 214 | 215 | const userMetaProps = await get( 216 | redis, 217 | prefix + ':meta:properties:UserMetaMockup', 218 | ); 219 | t.is( 220 | userMetaProps, 221 | JSON.stringify(stringifyFunctions(user.meta.properties)), 222 | 'Properties of the user did not match.', 223 | ); 224 | 225 | const commentMetaProps = await get( 226 | redis, 227 | prefix + ':meta:properties:CommentMetaMockup', 228 | ); 229 | t.is( 230 | commentMetaProps, 231 | JSON.stringify(stringifyFunctions(comment.meta.properties)), 232 | 'Properties of the comment did not match.', 233 | ); 234 | }); 235 | -------------------------------------------------------------------------------- /test/middleware.test.ts: -------------------------------------------------------------------------------- 1 | import test, { ExecutionContext } from 'ava'; 2 | 3 | import { nohm } from '../ts'; 4 | 5 | import * as args from './testArgs'; 6 | 7 | import * as vm from 'vm'; 8 | import { ServerResponse } from 'http'; 9 | import { IMiddlewareOptions } from '../ts/middleware'; 10 | 11 | const redis = args.redis; 12 | 13 | test.before(async () => { 14 | nohm.setPrefix(args.prefix); 15 | await args.setClient(nohm, redis); 16 | }); 17 | 18 | nohm.setExtraValidations(__dirname + '/custom_validations.js'); 19 | 20 | nohm.model('UserMiddlewareMockup', { 21 | properties: { 22 | name: { 23 | defaultValue: 'testName', 24 | type: 'string', 25 | validations: [ 26 | 'notEmpty', 27 | { 28 | name: 'length', 29 | options: { 30 | min: 2, 31 | }, 32 | }, 33 | ], 34 | }, 35 | customValidationFile: { 36 | defaultValue: 'customValidationFile', 37 | type: 'string', 38 | validations: ['customValidationFile'], 39 | }, 40 | customValidationFileTimesTwo: { 41 | defaultValue: 'customValidationFileTimesTwo', 42 | type: 'string', 43 | validations: ['customValidationFileTimesTwo'], 44 | }, 45 | excludedProperty: { 46 | defaultValue: 'asd', 47 | type: 'string', 48 | validations: ['notEmpty'], 49 | }, 50 | excludedValidation: { 51 | defaultValue: 'asd', 52 | type: 'string', 53 | validations: [ 54 | 'notEmpty', 55 | { 56 | name: 'length', 57 | options: { 58 | min: 2, 59 | }, 60 | }, 61 | ], 62 | }, 63 | }, 64 | }); 65 | nohm.model('ExcludedMiddlewareMockup', { 66 | properties: { 67 | name: { 68 | defaultValue: '', 69 | type: 'string', 70 | validations: ['notEmpty'], 71 | }, 72 | }, 73 | }); 74 | 75 | const setup = ( 76 | t: ExecutionContext, 77 | expected: number, 78 | options: IMiddlewareOptions = {}, 79 | ): Promise<{ sandbox: any; str: string }> => { 80 | return new Promise((resolve) => { 81 | t.plan(3 + expected); 82 | let length = 0; 83 | let headersSet = false; 84 | const namespace = 85 | options && options.namespace ? options.namespace : 'nohmValidations'; 86 | const dummyRes = { 87 | setHeader(name: string, value: string | number) { 88 | if (name === 'Content-Length') { 89 | t.true(value > 0, 'Header Content-Length was 0'); 90 | if (typeof value === 'string') { 91 | length = parseInt(value, 10); 92 | } else { 93 | length = value; 94 | } 95 | } 96 | headersSet = true; 97 | }, 98 | end(str: string) { 99 | const sandbox = { 100 | window: {}, 101 | console, 102 | }; 103 | t.true(headersSet, 'Headers were not set before res.end() was called'); 104 | t.is( 105 | length, 106 | str.length, 107 | 'Content-Length was not equal to the actual body length', 108 | ); 109 | try { 110 | // fixes the problem that in the browser we'd have globals automatically in window, here we don't. 111 | str = str.replace( 112 | /(typeof \(exports\) === 'undefined')/, 113 | '(window[nohmValidationsNamespaceName] = ' + namespace + ') && $1', 114 | ); 115 | 116 | vm.runInNewContext(str, sandbox, 'validations.vm'); 117 | } catch (e) { 118 | console.log(str); 119 | console.log('Parsing the javascript failed: ' + e.message); 120 | console.log(e.stack); 121 | t.fail('Parsing javascript failed.'); 122 | } 123 | resolve({ sandbox, str }); 124 | }, 125 | }; 126 | 127 | const url = options && options.url ? options.url : '/nohmValidations.js'; 128 | 129 | // `{ url } as any` because url should be the only part of the IncomingMessage argument that is used 130 | nohm.middleware(options)({ url } as any, dummyRes as ServerResponse, () => { 131 | t.fail( 132 | 'nohm.middleware() called next even though a valid middleware url was passed.', 133 | ); 134 | }); 135 | }); 136 | }; 137 | 138 | test('no options passed', async (t) => { 139 | const { sandbox } = await setup(t, 2); 140 | const val = sandbox.nohmValidations.models.UserMiddlewareMockup; 141 | t.is( 142 | val.name.indexOf('notEmpty'), 143 | 0, 144 | 'UserMiddlewareMockup did not have the proper validations', 145 | ); 146 | t.deepEqual( 147 | val.name[1], 148 | { 149 | name: 'length', 150 | options: { 151 | min: 2, 152 | }, 153 | }, 154 | 'UserMiddlewareMockup did not have the proper validations', 155 | ); 156 | }); 157 | 158 | test('validation', async (t) => { 159 | const { sandbox } = await setup(t, 3); 160 | const val = sandbox.nohmValidations.validate; 161 | const validation = await val('UserMiddlewareMockup', { 162 | name: 'asd', 163 | excludedProperty: 'asd', 164 | excludedValidation: 'asd', 165 | }); 166 | t.true(validation.result, 'Validate did not work as expected.'); 167 | 168 | const validation2 = await val('UserMiddlewareMockup', { 169 | name: 'a', 170 | excludedProperty: '', 171 | excludedValidation: 'a', 172 | }); 173 | t.false(validation2.result, 'Validate did not work as expected.'); 174 | t.deepEqual( 175 | validation2.errors, 176 | { 177 | name: ['length'], 178 | excludedProperty: ['notEmpty'], 179 | excludedValidation: ['length'], 180 | }, 181 | 'Validate did not work as expected.', 182 | ); 183 | }); 184 | 185 | test('options', async (t) => { 186 | const { sandbox } = await setup(t, 1, { 187 | url: './nohm.js', 188 | namespace: 'hurgel', 189 | }); 190 | t.snapshot(sandbox.hurgel, 'Namespace option not successful'); 191 | }); 192 | 193 | test('extra files', async (t) => { 194 | const { sandbox } = await setup(t, 1, { 195 | extraFiles: __dirname + '/custom_validations2.js', 196 | }); 197 | const validation = await sandbox.nohmValidations.validate( 198 | 'UserMiddlewareMockup', 199 | { 200 | customValidationFile: 'NOPE', 201 | customValidationFileTimesTwo: 'NOPE', 202 | }, 203 | ); 204 | t.deepEqual( 205 | validation.errors, 206 | { 207 | customValidationFile: ['customValidationFile'], 208 | customValidationFileTimesTwo: ['customValidationFileTimesTwo'], 209 | }, 210 | 'Validate did not work as expected.', 211 | ); 212 | }); 213 | 214 | test('exceptions', async (t) => { 215 | const { sandbox } = await setup(t, 2, { 216 | exclusions: { 217 | UserMiddlewareMockup: { 218 | excludedValidation: [1], 219 | excludedProperty: true, 220 | }, 221 | ExcludedMiddlewareMockup: true, 222 | }, 223 | }); 224 | const validate = sandbox.nohmValidations.validate; 225 | const validation = await validate('UserMiddlewareMockup', { 226 | excludedValidation: 'a', 227 | excludedProperty: '', 228 | }); 229 | t.true( 230 | validation.result, 231 | 'Validate did not work as expected with exclusions.', 232 | ); 233 | 234 | try { 235 | await validate('ExcludedMiddlewareMockup', { 236 | name: '', 237 | }); 238 | t.fail('Validate should have thrown an error about an invalid modelName'); 239 | } catch (e) { 240 | t.is( 241 | e.message, 242 | 'Invalid modelName passed to nohm or model was not properly exported.', 243 | 'Validate did not work as expected with exclusions.', 244 | ); 245 | } 246 | }); 247 | 248 | test('validate empty', async (t) => { 249 | const { sandbox } = await setup(t, 1); 250 | const val = sandbox.nohmValidations.validate; 251 | const validation = await val('UserMiddlewareMockup', { 252 | excludedProperty: 'asd', 253 | excludedValidation: 'asd', 254 | }); 255 | t.true(validation.result, 'Validate did not work as expected.'); 256 | }); 257 | 258 | test('validate undefined', async (t) => { 259 | const { sandbox } = await setup(t, 2); 260 | const val = sandbox.nohmValidations.validate; 261 | try { 262 | const result = await val('UserMiddlewareMockup', { 263 | name: undefined, 264 | }); 265 | t.false(result.result, 'Validating with name undefined succeeded'); 266 | t.deepEqual( 267 | result.errors, 268 | { name: ['length', 'notEmpty'] }, 269 | 'Validating with name undefined had wrong errors object.', 270 | ); 271 | } catch (e) { 272 | t.fail('Validate threw an error on undefined data.'); 273 | } 274 | }); 275 | -------------------------------------------------------------------------------- /test/property.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { nohm } from '../ts'; 4 | import * as args from './testArgs'; 5 | 6 | test.before(async () => { 7 | await args.setClient(nohm, args.redis); 8 | }); 9 | 10 | const userMockup = nohm.model('UserMockup', { 11 | properties: { 12 | name: { 13 | defaultValue: 'test', 14 | type: 'string', 15 | unique: true, 16 | validations: ['notEmpty'], 17 | }, 18 | visits: { 19 | index: true, 20 | type: 'integer', 21 | }, 22 | email: { 23 | defaultValue: 'email@email.de', 24 | type: 'string', 25 | unique: true, 26 | validations: ['email'], 27 | }, 28 | emailOptional: { 29 | defaultValue: '', 30 | type: 'string', 31 | unique: true, 32 | validations: [ 33 | { 34 | name: 'email', 35 | options: { 36 | optional: true, 37 | }, 38 | }, 39 | ], 40 | }, 41 | country: { 42 | index: true, 43 | defaultValue: 'Tibet', 44 | type: 'string', 45 | validations: ['notEmpty'], 46 | }, 47 | json: { 48 | defaultValue: '{}', 49 | type: 'json', 50 | }, 51 | }, 52 | idGenerator: 'increment', 53 | }); 54 | 55 | test('propertyGetter', (t) => { 56 | const user = new userMockup(); 57 | 58 | t.is(typeof user.p, 'function', 'Property getter short p is not available.'); 59 | 60 | t.is( 61 | typeof user.prop, 62 | 'function', 63 | 'Property getter short prop is not available.', 64 | ); 65 | 66 | t.is(typeof user.property, 'function', 'Property getter is not available.'); 67 | 68 | t.is( 69 | user.property('email'), 70 | 'email@email.de', 71 | 'Property getter did not return the correct value for email.', 72 | ); 73 | 74 | t.is( 75 | user.property('name'), 76 | 'test', 77 | 'Property getter did not return the correct value for name.', 78 | ); 79 | 80 | t.throws( 81 | () => { 82 | user.property('hurgelwurz'); 83 | }, 84 | { message: /Invalid property key 'hurgelwurz'\./ }, 85 | 'Calling .property() with an undefined key did not throw an error.', 86 | ); 87 | 88 | t.deepEqual( 89 | user.property('json'), 90 | {}, 91 | 'Property getter did not return the correct value for json.', 92 | ); 93 | }); 94 | 95 | test('propertySetter', (t) => { 96 | const user = new userMockup(); 97 | const controlUser = new userMockup(); 98 | 99 | t.is( 100 | user.property('email', 123), 101 | '', 102 | 'Setting a property did not return the new value that was set (with casting).', 103 | ); 104 | 105 | user.property('email', 'asdasd'); 106 | t.is( 107 | user.property('email'), 108 | 'asdasd', 109 | 'Setting a property did not actually set the property to the correct value', 110 | ); 111 | 112 | user.property('email', 'test@test.de'); 113 | t.not( 114 | user.property('email'), 115 | controlUser.property('email'), 116 | 'Creating a new instance of an Object does not create fresh properties.', 117 | ); 118 | 119 | user.property({ 120 | name: 'objectTest', 121 | email: 'object@test.de', 122 | }); 123 | 124 | t.is( 125 | user.property('name'), 126 | 'objectTest', 127 | 'Setting multiple properties by providing one object did not work correctly for the name.', 128 | ); 129 | t.is( 130 | user.property('email'), 131 | 'object@test.de', 132 | 'Setting multiple properties by providing one object did not work correctly for the email.', 133 | ); 134 | 135 | user.property('json', { 136 | test: 1, 137 | }); 138 | 139 | t.is( 140 | user.property('json').test, 141 | 1, 142 | 'Setting a json property did not work correctly.', 143 | ); 144 | }); 145 | 146 | test('propertyDiff', (t) => { 147 | const user = new userMockup(); 148 | const beforeName = user.property('name'); 149 | const afterName = 'hurgelwurz'; 150 | const beforeEmail = user.property('email'); 151 | const afterEmail = 'email.propertyDiff@test'; 152 | const shouldName = [ 153 | { 154 | key: 'name', 155 | before: beforeName, 156 | after: afterName, 157 | }, 158 | ]; 159 | const shouldMail = [ 160 | { 161 | key: 'email', 162 | before: beforeEmail, 163 | after: afterEmail, 164 | }, 165 | ]; 166 | const shouldNameAndMail = shouldName.concat(shouldMail); 167 | 168 | t.deepEqual( 169 | user.propertyDiff(), 170 | [], 171 | 'Property diff returned changes even though there were none', 172 | ); 173 | 174 | user.property('name', afterName); 175 | t.deepEqual( 176 | user.propertyDiff(), 177 | shouldName, 178 | 'Property diff did not correctly recognize the changed property `name`.', 179 | ); 180 | 181 | user.property('email', afterEmail); 182 | t.deepEqual( 183 | user.propertyDiff('name'), 184 | shouldName, 185 | 'Property diff did not correctly filter for changes only in `name`.', 186 | ); 187 | 188 | t.deepEqual( 189 | user.propertyDiff(), 190 | shouldNameAndMail, 191 | 'Property diff did not correctly recognize the changed properties `name` and `email`.', 192 | ); 193 | 194 | user.property('name', beforeName); 195 | t.deepEqual( 196 | user.propertyDiff(), 197 | shouldMail, 198 | 'Property diff did not correctly recognize the reset property `name`.', 199 | ); 200 | }); 201 | 202 | test('propertyReset', (t) => { 203 | const user = new userMockup(); 204 | const beforeName = user.property('name'); 205 | const beforeEmail = user.property('email'); 206 | 207 | user.property('name', user.property('name') + 'hurgelwurz'); 208 | user.property('email', user.property('email') + 'asdasd'); 209 | user.propertyReset('name'); 210 | t.is( 211 | user.property('name'), 212 | beforeName, 213 | 'Property reset did not properly reset `name`.', 214 | ); 215 | 216 | t.not( 217 | user.property('email'), 218 | beforeEmail, 219 | "Property reset reset `email` when it shouldn't have.", 220 | ); 221 | 222 | user.property('name', user.property('name') + 'hurgelwurz'); 223 | user.propertyReset(); 224 | t.true( 225 | user.property('name') === beforeName && 226 | user.property('email') === beforeEmail, 227 | 'Property reset did not properly reset `name` and `email`.', 228 | ); 229 | }); 230 | 231 | test('allProperties', (t) => { 232 | const user = new userMockup(); 233 | 234 | user.property('name', 'hurgelwurz'); 235 | user.property('email', 'hurgelwurz@test.de'); 236 | const should = { 237 | name: user.property('name'), 238 | visits: user.property('visits'), 239 | email: user.property('email'), 240 | emailOptional: user.property('emailOptional'), 241 | country: user.property('country'), 242 | json: {}, 243 | id: user.id, 244 | }; 245 | t.deepEqual(should, user.allProperties(), 'Getting all properties failed.'); 246 | }); 247 | -------------------------------------------------------------------------------- /test/pubsub/Model.ts: -------------------------------------------------------------------------------- 1 | import nohm from '../../ts/'; 2 | 3 | export const register = (passedNohm) => { 4 | passedNohm.model('Tester', { 5 | properties: { 6 | dummy: { 7 | type: 'string', 8 | }, 9 | }, 10 | publish: true, 11 | }); 12 | 13 | passedNohm.model('no_publish', { 14 | properties: { 15 | dummy: { 16 | type: 'string', 17 | }, 18 | }, 19 | }); 20 | }; 21 | 22 | register(nohm); 23 | -------------------------------------------------------------------------------- /test/pubsub/child.ts: -------------------------------------------------------------------------------- 1 | import * as redis from 'redis'; 2 | 3 | import nohm from '../../ts/'; 4 | import * as args from '../testArgs'; 5 | 6 | args.redis.on('ready', () => { 7 | nohm.setClient(args.redis); 8 | }); 9 | 10 | // tslint:disable-next-line:no-var-requires 11 | require('./Model'); 12 | 13 | process.on('message', async (msg) => { 14 | let event: any; 15 | let modelName: string; 16 | let instance: any; 17 | let fn: any; 18 | 19 | switch (msg.question) { 20 | case 'does nohm have pubsub?': 21 | process.send({ 22 | question: msg.question, 23 | answer: nohm.getPubSubClient(), 24 | }); 25 | break; 26 | 27 | case 'initialize': 28 | try { 29 | if (!msg.args.prefix) { 30 | console.error('No prefix passed in initialize function.'); 31 | process.exit(1); 32 | } 33 | nohm.setPrefix(msg.args.prefix); 34 | await nohm.setPubSubClient( 35 | redis.createClient(args.redisPort, args.redisHost), 36 | ); 37 | process.send({ 38 | question: msg.question, 39 | answer: true, 40 | error: null, 41 | }); 42 | } catch (err) { 43 | process.send({ 44 | question: msg.question, 45 | answer: err || true, 46 | error: err, 47 | }); 48 | } 49 | break; 50 | 51 | case 'subscribe': 52 | event = msg.args.event; 53 | modelName = msg.args.modelName; 54 | instance = await nohm.factory(modelName); 55 | await instance.subscribe(event, (change) => { 56 | process.send({ 57 | question: 'subscribe', 58 | answer: change, 59 | event, 60 | }); 61 | }); 62 | process.send({ 63 | question: 'subscribe', 64 | answer: 'ACK', 65 | }); 66 | break; 67 | 68 | case 'subscribeOnce': 69 | event = msg.args.event; 70 | modelName = msg.args.modelName; 71 | instance = await nohm.factory(modelName); 72 | await instance.subscribeOnce(event, (change) => { 73 | process.send({ 74 | question: 'subscribeOnce', 75 | answer: change, 76 | }); 77 | }); 78 | process.send({ 79 | question: 'subscribeOnce', 80 | answer: 'ACK', 81 | }); 82 | break; 83 | 84 | case 'unsubscribe': 85 | event = msg.args.event; 86 | modelName = msg.args.modelName; 87 | fn = msg.args.fn; 88 | instance = await nohm.factory(modelName); 89 | await instance.unsubscribe(event, fn); 90 | process.send({ 91 | question: 'unsubscribe', 92 | answer: true, 93 | }); 94 | break; 95 | } 96 | }); 97 | -------------------------------------------------------------------------------- /test/pubsub/child_wrapper.js: -------------------------------------------------------------------------------- 1 | require('ts-node').register({ 2 | transpileOnly: true, 3 | }); 4 | 5 | require('./child.ts'); 6 | -------------------------------------------------------------------------------- /test/redisHelper.test.ts: -------------------------------------------------------------------------------- 1 | import test, { ExecutionContext } from 'ava'; 2 | import * as td from 'testdouble'; 3 | 4 | import * as redisHelper from '../ts/typed-redis-helper'; 5 | import { Multi } from 'redis'; 6 | 7 | // an object that has the redis methods that are used as testdoubles (could also use a redis client, this is simpler) 8 | const mockRedis = td.object(redisHelper); 9 | 10 | const testCommand = (name: string, args: Array, resolveExpect?: any) => { 11 | return async (t: ExecutionContext) => { 12 | const testMethod = redisHelper[name]; 13 | 14 | try { 15 | await testMethod({}); 16 | } catch (e) { 17 | t.true(e instanceof Error, 'Error thrown was not instance of Error.'); 18 | t.is( 19 | e.message, 20 | redisHelper.errorMessage, 21 | 'Error thrown had the wrong message.', 22 | ); 23 | } 24 | 25 | try { 26 | td.when(mockRedis[name](...args)).thenCallback(null, resolveExpect); 27 | t.deepEqual( 28 | await testMethod.apply(testMethod, [mockRedis, ...args]), 29 | resolveExpect, 30 | `${name} did not resolve correctly`, 31 | ); 32 | } catch (e) { 33 | t.fail(`${name} without error rejected: ${e.message}`); 34 | } 35 | 36 | const errorString = Symbol('Error string'); 37 | try { 38 | args.splice(0, 1); // since we're passing errorKey as the first arg, we remove the first supplied one 39 | td.when(mockRedis[name](...['errorKey', ...args])).thenCallback( 40 | errorString, 41 | null, 42 | ); 43 | await testMethod.apply(testMethod, [mockRedis, 'errorKey', ...args]); 44 | t.fail('Succeeded where it should not have.'); 45 | } catch (e) { 46 | t.is(e, errorString, 'Error thrown was not errorString'); 47 | } 48 | }; 49 | }; 50 | 51 | test('get', testCommand('get', ['foo'], 'bar')); 52 | test('exists', testCommand('exists', ['foo'], true)); 53 | test('del', testCommand('del', ['foo'])); 54 | test('set', testCommand('set', ['foo', 'bar'])); 55 | test('mset', testCommand('mset', ['foo', ['foo', 'bar']])); 56 | test('setnx', testCommand('setnx', ['foo', 'bar'], true)); 57 | test('smembers', testCommand('smembers', ['foo'], ['bar'])); 58 | test('sismember', testCommand('sismember', ['foo', 'bar'], 1)); 59 | test('sadd', testCommand('sadd', ['foo', 'bar'], 1)); 60 | test('sinter', testCommand('sinter', ['foo', 'bar'], ['bar', 'baz'])); 61 | test('hgetall', testCommand('hgetall', ['foo'], ['bar', 'baz'])); 62 | test('psubscribe', testCommand('psubscribe', ['foo', ['bar', 'baz']])); 63 | test('punsubscribe', testCommand('punsubscribe', ['foo', ['bar', 'baz']])); 64 | test('keys', testCommand('keys', ['foo'], ['bar', 'baz'])); 65 | test('zscore', testCommand('zscore', ['foo', 'bar'], 2)); 66 | test('hset', testCommand('hset', ['foo', 'bar', 'baz'], 2)); 67 | test('hget', testCommand('hget', ['foo', 'bar'], 'baz')); 68 | 69 | test.serial('exec', async (t) => { 70 | // exec has no firstArg. it's easier to duplicate the test here instead of changing testCommand 71 | const mockMultiRedis: Multi = td.object(['exec', 'EXEC']) as Multi; 72 | 73 | try { 74 | // @ts-ignore intentionally calling with wrong arguments 75 | await redisHelper.exec({}); 76 | t.fail('Succeeded where it should have failed.'); 77 | } catch (e) { 78 | t.true(e instanceof Error, 'Error thrown was not instance of Error.'); 79 | t.is( 80 | e.message, 81 | redisHelper.errorMessage, 82 | 'Error thrown had the wrong message.', 83 | ); 84 | } 85 | 86 | const resolveExpect = ['foo', 'baz']; 87 | td.when(mockMultiRedis.exec()).thenCallback(null, resolveExpect); 88 | t.deepEqual( 89 | await redisHelper.exec(mockMultiRedis), 90 | resolveExpect, 91 | `exec did not resolve correctly`, 92 | ); 93 | 94 | // test that error callback rejects 95 | const errorString = Symbol('Error string'); 96 | try { 97 | td.when(mockMultiRedis.exec()).thenCallback(errorString, null); 98 | await redisHelper.exec(mockMultiRedis); 99 | t.fail('Error callback did not reject.'); 100 | } catch (e) { 101 | t.is(e, errorString, 'Error thrown was not errorString'); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /test/regressions.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { nohm } from '../ts'; 4 | 5 | import * as args from './testArgs'; 6 | import { cleanUp, cleanUpPromise } from './helper'; 7 | import { exists, smembers } from '../ts/typed-redis-helper'; 8 | 9 | const redis = args.redis; 10 | 11 | const prefix = args.prefix + 'regressions'; 12 | 13 | test.before(async () => { 14 | nohm.setPrefix(prefix); 15 | await args.setClient(nohm, redis); 16 | await cleanUpPromise(redis, prefix); 17 | }); 18 | 19 | test.afterEach(() => { 20 | return new Promise((resolve, reject) => { 21 | cleanUp(redis, prefix, (err) => { 22 | if (err) { 23 | reject(err); 24 | } else { 25 | resolve(); 26 | } 27 | }); 28 | }); 29 | }); 30 | 31 | test.serial('#114 update does not reset index', async (t) => { 32 | // https://github.com/maritz/nohm/issues/114 33 | 34 | const modelName = 'Regression114Model'; 35 | 36 | nohm.model(modelName, { 37 | properties: { 38 | uniqueDeletion: { 39 | type: 'string', 40 | unique: true, 41 | }, 42 | isActive: { 43 | index: true, 44 | defaultValue: true, 45 | type: 'boolean', 46 | }, 47 | scoredIndex: { 48 | index: true, 49 | defaultValue: 1, 50 | type: 'number', 51 | }, 52 | }, 53 | idGenerator: 'increment', 54 | }); 55 | 56 | const instance = await nohm.factory(modelName); 57 | instance.property({ uniqueDeletion: 'one' }); 58 | 59 | const instance2 = await nohm.factory(modelName); 60 | instance2.property({ 61 | uniqueDeletion: 'two', 62 | isActive: false, 63 | scoredIndex: 123, 64 | }); 65 | 66 | const instance3 = await nohm.factory(modelName); 67 | instance3.property({ uniqueDeletion: 'three' }); 68 | await Promise.all([instance.save(), instance2.save(), instance3.save()]); 69 | 70 | const uniqueKey = `${prefix}:uniques:${instance2.modelName}:uniqueDeletion:two`; 71 | 72 | // make sure we check the correct unique key 73 | const uniqueExistsCheck = await exists(redis, uniqueKey); 74 | 75 | t.is(uniqueExistsCheck, 1, 'A unique key of a changed property remained.'); 76 | 77 | const instance2Activated = await nohm.factory(modelName); 78 | instance2Activated.id = instance2.id; 79 | instance2Activated.property({ 80 | uniqueDeletion: 'twoDelete', 81 | isActive: true, 82 | }); 83 | await instance2Activated.save(); 84 | 85 | const membersTrue = await smembers( 86 | redis, 87 | `${prefix}:index:${modelName}:isActive:true`, 88 | ); 89 | 90 | t.deepEqual( 91 | membersTrue, 92 | [instance.id, instance2.id, instance3.id], 93 | 'Not all instances were properly indexed as isActive:true', 94 | ); 95 | 96 | const membersFalse = await smembers( 97 | redis, 98 | `${prefix}:index:${modelName}:isActive:false`, 99 | ); 100 | 101 | t.deepEqual(membersFalse, [], 'An index for isActive:false remained.'); 102 | 103 | const uniqueExists = await exists(redis, uniqueKey); 104 | 105 | t.is(uniqueExists, 0, 'A unique key of a changed property remained.'); 106 | }); 107 | -------------------------------------------------------------------------------- /test/snapshots/exports.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/exports.test.ts` 2 | 3 | The actual snapshot is saved in `exports.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## exports the correct objects 8 | 9 | > Snapshot 1 10 | 11 | { 12 | IChangeEventPayload: undefined, 13 | IDefaultEventPayload: undefined, 14 | IDictionary: undefined, 15 | ILinkOptions: undefined, 16 | IModelOptions: undefined, 17 | IModelPropertyDefinition: undefined, 18 | IModelPropertyDefinitions: undefined, 19 | INohmPrefixes: undefined, 20 | IRelationChangeEventPayload: undefined, 21 | ISortOptions: undefined, 22 | IStaticMethods: undefined, 23 | LinkError: Function LinkError {}, 24 | Nohm: NohmClass { 25 | LinkError: Function LinkError {}, 26 | ValidationError: Function ValidationError {}, 27 | extraValidators: [], 28 | isPublishSubscribed: false, 29 | meta: true, 30 | modelCache: {}, 31 | prefix: { 32 | channel: 'nohm:channel:', 33 | hash: 'nohm:hash:', 34 | idsets: 'nohm:idsets:', 35 | incrementalIds: 'nohm:ids:', 36 | index: 'nohm:index:', 37 | meta: { 38 | idGenerator: 'nohm:meta:idGenerator:', 39 | properties: 'nohm:meta:properties:', 40 | version: 'nohm:meta:version:', 41 | }, 42 | relationKeys: 'nohm:relationKeys:', 43 | relations: 'nohm:relations:', 44 | scoredindex: 'nohm:scoredindex:', 45 | unique: 'nohm:uniques:', 46 | }, 47 | publish: false, 48 | }, 49 | NohmClass: Function NohmClass {}, 50 | NohmModel: Function NohmModelExtendable {}, 51 | TLinkCallback: undefined, 52 | TTypedDefinitions: undefined, 53 | ValidationError: Function ValidationError {}, 54 | boolProperty: 'bool', 55 | dateProperty: 'date', 56 | default: NohmClass { 57 | LinkError: Function LinkError {}, 58 | ValidationError: Function ValidationError {}, 59 | extraValidators: [], 60 | isPublishSubscribed: false, 61 | meta: true, 62 | modelCache: {}, 63 | prefix: { 64 | channel: 'nohm:channel:', 65 | hash: 'nohm:hash:', 66 | idsets: 'nohm:idsets:', 67 | incrementalIds: 'nohm:ids:', 68 | index: 'nohm:index:', 69 | meta: { 70 | idGenerator: 'nohm:meta:idGenerator:', 71 | properties: 'nohm:meta:properties:', 72 | version: 'nohm:meta:version:', 73 | }, 74 | relationKeys: 'nohm:relationKeys:', 75 | relations: 'nohm:relations:', 76 | scoredindex: 'nohm:scoredindex:', 77 | unique: 'nohm:uniques:', 78 | }, 79 | publish: false, 80 | }, 81 | floatProperty: 'float', 82 | integerProperty: 'integer', 83 | jsonProperty: 'json', 84 | nohm: NohmClass { 85 | LinkError: Function LinkError {}, 86 | ValidationError: Function ValidationError {}, 87 | extraValidators: [], 88 | isPublishSubscribed: false, 89 | meta: true, 90 | modelCache: {}, 91 | prefix: { 92 | channel: 'nohm:channel:', 93 | hash: 'nohm:hash:', 94 | idsets: 'nohm:idsets:', 95 | incrementalIds: 'nohm:ids:', 96 | index: 'nohm:index:', 97 | meta: { 98 | idGenerator: 'nohm:meta:idGenerator:', 99 | properties: 'nohm:meta:properties:', 100 | version: 'nohm:meta:version:', 101 | }, 102 | relationKeys: 'nohm:relationKeys:', 103 | relations: 'nohm:relations:', 104 | scoredindex: 'nohm:scoredindex:', 105 | unique: 'nohm:uniques:', 106 | }, 107 | publish: false, 108 | }, 109 | numberProperty: 'number', 110 | stringProperty: 'string', 111 | timeProperty: 'time', 112 | timestampProperty: 'timestamp', 113 | } 114 | 115 | > Snapshot 2 116 | 117 | NohmClass { 118 | LinkError: Function LinkError {}, 119 | ValidationError: Function ValidationError {}, 120 | extraValidators: [], 121 | isPublishSubscribed: false, 122 | meta: true, 123 | modelCache: {}, 124 | prefix: { 125 | channel: 'nohm:channel:', 126 | hash: 'nohm:hash:', 127 | idsets: 'nohm:idsets:', 128 | incrementalIds: 'nohm:ids:', 129 | index: 'nohm:index:', 130 | meta: { 131 | idGenerator: 'nohm:meta:idGenerator:', 132 | properties: 'nohm:meta:properties:', 133 | version: 'nohm:meta:version:', 134 | }, 135 | relationKeys: 'nohm:relationKeys:', 136 | relations: 'nohm:relations:', 137 | scoredindex: 'nohm:scoredindex:', 138 | unique: 'nohm:uniques:', 139 | }, 140 | publish: false, 141 | } 142 | 143 | > Snapshot 3 144 | 145 | NohmClass { 146 | LinkError: Function LinkError {}, 147 | ValidationError: Function ValidationError {}, 148 | extraValidators: [], 149 | isPublishSubscribed: false, 150 | meta: true, 151 | modelCache: {}, 152 | prefix: { 153 | channel: 'nohm:channel:', 154 | hash: 'nohm:hash:', 155 | idsets: 'nohm:idsets:', 156 | incrementalIds: 'nohm:ids:', 157 | index: 'nohm:index:', 158 | meta: { 159 | idGenerator: 'nohm:meta:idGenerator:', 160 | properties: 'nohm:meta:properties:', 161 | version: 'nohm:meta:version:', 162 | }, 163 | relationKeys: 'nohm:relationKeys:', 164 | relations: 'nohm:relations:', 165 | scoredindex: 'nohm:scoredindex:', 166 | unique: 'nohm:uniques:', 167 | }, 168 | publish: false, 169 | } 170 | 171 | > Snapshot 4 172 | 173 | NohmClass { 174 | LinkError: Function LinkError {}, 175 | ValidationError: Function ValidationError {}, 176 | extraValidators: [], 177 | isPublishSubscribed: false, 178 | meta: true, 179 | modelCache: {}, 180 | prefix: { 181 | channel: 'nohm:channel:', 182 | hash: 'nohm:hash:', 183 | idsets: 'nohm:idsets:', 184 | incrementalIds: 'nohm:ids:', 185 | index: 'nohm:index:', 186 | meta: { 187 | idGenerator: 'nohm:meta:idGenerator:', 188 | properties: 'nohm:meta:properties:', 189 | version: 'nohm:meta:version:', 190 | }, 191 | relationKeys: 'nohm:relationKeys:', 192 | relations: 'nohm:relations:', 193 | scoredindex: 'nohm:scoredindex:', 194 | unique: 'nohm:uniques:', 195 | }, 196 | publish: false, 197 | } 198 | -------------------------------------------------------------------------------- /test/snapshots/exports.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/test/snapshots/exports.test.ts.snap -------------------------------------------------------------------------------- /test/snapshots/exports.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/exports.ts` 2 | 3 | The actual snapshot is saved in `exports.ts.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## exports the correct objects 8 | 9 | > Snapshot 1 10 | 11 | { 12 | IChangeEventPayload: undefined, 13 | IDefaultEventPayload: undefined, 14 | IDictionary: undefined, 15 | ILinkOptions: undefined, 16 | IModelOptions: undefined, 17 | IModelPropertyDefinition: undefined, 18 | IModelPropertyDefinitions: undefined, 19 | INohmPrefixes: undefined, 20 | IRelationChangeEventPayload: undefined, 21 | ISortOptions: undefined, 22 | IStaticMethods: undefined, 23 | LinkError: Function LinkError {}, 24 | Nohm: NohmClass { 25 | LinkError: Function LinkError {}, 26 | ValidationError: Function ValidationError {}, 27 | extraValidators: [], 28 | isPublishSubscribed: false, 29 | meta: true, 30 | modelCache: {}, 31 | prefix: { 32 | channel: 'nohm:channel:', 33 | hash: 'nohm:hash:', 34 | idsets: 'nohm:idsets:', 35 | incrementalIds: 'nohm:ids:', 36 | index: 'nohm:index:', 37 | meta: { 38 | idGenerator: 'nohm:meta:idGenerator:', 39 | properties: 'nohm:meta:properties:', 40 | version: 'nohm:meta:version:', 41 | }, 42 | relationKeys: 'nohm:relationKeys:', 43 | relations: 'nohm:relations:', 44 | scoredindex: 'nohm:scoredindex:', 45 | unique: 'nohm:uniques:', 46 | }, 47 | publish: false, 48 | }, 49 | NohmClass: Function NohmClass {}, 50 | NohmModel: Function NohmModelExtendable {}, 51 | TLinkCallback: undefined, 52 | TTypedDefinitions: undefined, 53 | ValidationError: Function ValidationError {}, 54 | boolProperty: 'bool', 55 | dateProperty: 'date', 56 | default: NohmClass { 57 | LinkError: Function LinkError {}, 58 | ValidationError: Function ValidationError {}, 59 | extraValidators: [], 60 | isPublishSubscribed: false, 61 | meta: true, 62 | modelCache: {}, 63 | prefix: { 64 | channel: 'nohm:channel:', 65 | hash: 'nohm:hash:', 66 | idsets: 'nohm:idsets:', 67 | incrementalIds: 'nohm:ids:', 68 | index: 'nohm:index:', 69 | meta: { 70 | idGenerator: 'nohm:meta:idGenerator:', 71 | properties: 'nohm:meta:properties:', 72 | version: 'nohm:meta:version:', 73 | }, 74 | relationKeys: 'nohm:relationKeys:', 75 | relations: 'nohm:relations:', 76 | scoredindex: 'nohm:scoredindex:', 77 | unique: 'nohm:uniques:', 78 | }, 79 | publish: false, 80 | }, 81 | floatProperty: 'float', 82 | integerProperty: 'integer', 83 | jsonProperty: 'json', 84 | nohm: NohmClass { 85 | LinkError: Function LinkError {}, 86 | ValidationError: Function ValidationError {}, 87 | extraValidators: [], 88 | isPublishSubscribed: false, 89 | meta: true, 90 | modelCache: {}, 91 | prefix: { 92 | channel: 'nohm:channel:', 93 | hash: 'nohm:hash:', 94 | idsets: 'nohm:idsets:', 95 | incrementalIds: 'nohm:ids:', 96 | index: 'nohm:index:', 97 | meta: { 98 | idGenerator: 'nohm:meta:idGenerator:', 99 | properties: 'nohm:meta:properties:', 100 | version: 'nohm:meta:version:', 101 | }, 102 | relationKeys: 'nohm:relationKeys:', 103 | relations: 'nohm:relations:', 104 | scoredindex: 'nohm:scoredindex:', 105 | unique: 'nohm:uniques:', 106 | }, 107 | publish: false, 108 | }, 109 | numberProperty: 'number', 110 | stringProperty: 'string', 111 | timeProperty: 'time', 112 | timestampProperty: 'timestamp', 113 | } 114 | 115 | > Snapshot 2 116 | 117 | NohmClass { 118 | LinkError: Function LinkError {}, 119 | ValidationError: Function ValidationError {}, 120 | extraValidators: [], 121 | isPublishSubscribed: false, 122 | meta: true, 123 | modelCache: {}, 124 | prefix: { 125 | channel: 'nohm:channel:', 126 | hash: 'nohm:hash:', 127 | idsets: 'nohm:idsets:', 128 | incrementalIds: 'nohm:ids:', 129 | index: 'nohm:index:', 130 | meta: { 131 | idGenerator: 'nohm:meta:idGenerator:', 132 | properties: 'nohm:meta:properties:', 133 | version: 'nohm:meta:version:', 134 | }, 135 | relationKeys: 'nohm:relationKeys:', 136 | relations: 'nohm:relations:', 137 | scoredindex: 'nohm:scoredindex:', 138 | unique: 'nohm:uniques:', 139 | }, 140 | publish: false, 141 | } 142 | 143 | > Snapshot 3 144 | 145 | NohmClass { 146 | LinkError: Function LinkError {}, 147 | ValidationError: Function ValidationError {}, 148 | extraValidators: [], 149 | isPublishSubscribed: false, 150 | meta: true, 151 | modelCache: {}, 152 | prefix: { 153 | channel: 'nohm:channel:', 154 | hash: 'nohm:hash:', 155 | idsets: 'nohm:idsets:', 156 | incrementalIds: 'nohm:ids:', 157 | index: 'nohm:index:', 158 | meta: { 159 | idGenerator: 'nohm:meta:idGenerator:', 160 | properties: 'nohm:meta:properties:', 161 | version: 'nohm:meta:version:', 162 | }, 163 | relationKeys: 'nohm:relationKeys:', 164 | relations: 'nohm:relations:', 165 | scoredindex: 'nohm:scoredindex:', 166 | unique: 'nohm:uniques:', 167 | }, 168 | publish: false, 169 | } 170 | 171 | > Snapshot 4 172 | 173 | NohmClass { 174 | LinkError: Function LinkError {}, 175 | ValidationError: Function ValidationError {}, 176 | extraValidators: [], 177 | isPublishSubscribed: false, 178 | meta: true, 179 | modelCache: {}, 180 | prefix: { 181 | channel: 'nohm:channel:', 182 | hash: 'nohm:hash:', 183 | idsets: 'nohm:idsets:', 184 | incrementalIds: 'nohm:ids:', 185 | index: 'nohm:index:', 186 | meta: { 187 | idGenerator: 'nohm:meta:idGenerator:', 188 | properties: 'nohm:meta:properties:', 189 | version: 'nohm:meta:version:', 190 | }, 191 | relationKeys: 'nohm:relationKeys:', 192 | relations: 'nohm:relations:', 193 | scoredindex: 'nohm:scoredindex:', 194 | unique: 'nohm:uniques:', 195 | }, 196 | publish: false, 197 | } 198 | -------------------------------------------------------------------------------- /test/snapshots/exports.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/test/snapshots/exports.ts.snap -------------------------------------------------------------------------------- /test/snapshots/features.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/features.test.ts` 2 | 3 | The actual snapshot is saved in `features.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## setPrefix 8 | 9 | > Setting a custom prefix did not work as expected 10 | 11 | { 12 | channel: 'hurgel:channel:', 13 | hash: 'hurgel:hash:', 14 | idsets: 'hurgel:idsets:', 15 | incrementalIds: 'hurgel:ids:', 16 | index: 'hurgel:index:', 17 | meta: { 18 | idGenerator: 'hurgel:meta:idGenerator:', 19 | properties: 'hurgel:meta:properties:', 20 | version: 'hurgel:meta:version:', 21 | }, 22 | relationKeys: 'hurgel:relationKeys:', 23 | relations: 'hurgel:relations:', 24 | scoredindex: 'hurgel:scoredindex:', 25 | unique: 'hurgel:uniques:', 26 | } 27 | 28 | ## no key left behind 29 | 30 | > Not all keys were removed from the database 31 | 32 | [ 33 | 'nohmtestsfeature:ids:UserMockup', 34 | 'nohmtestsfeature:meta:idGenerator:UserMockup', 35 | 'nohmtestsfeature:meta:properties:UserMockup', 36 | 'nohmtestsfeature:meta:version:UserMockup', 37 | ] 38 | -------------------------------------------------------------------------------- /test/snapshots/features.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/test/snapshots/features.test.ts.snap -------------------------------------------------------------------------------- /test/snapshots/middleware.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/middleware.test.ts` 2 | 3 | The actual snapshot is saved in `middleware.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## options 8 | 9 | > Namespace option not successful 10 | 11 | { 12 | extraValidations: [ 13 | undefined, 14 | undefined, 15 | { 16 | customValidationFile: Function {}, 17 | instanceValidation: Function {}, 18 | }, 19 | ], 20 | models: { 21 | ExcludedMiddlewareMockup: { 22 | name: [ 23 | 'notEmpty', 24 | ], 25 | }, 26 | UserMiddlewareMockup: { 27 | customValidationFile: [ 28 | 'customValidationFile', 29 | ], 30 | customValidationFileTimesTwo: [ 31 | 'customValidationFileTimesTwo', 32 | ], 33 | excludedProperty: [ 34 | 'notEmpty', 35 | ], 36 | excludedValidation: [ 37 | 'notEmpty', 38 | { 39 | name: 'length', 40 | options: { 41 | min: 2, 42 | }, 43 | }, 44 | ], 45 | name: [ 46 | 'notEmpty', 47 | { 48 | name: 'length', 49 | options: { 50 | min: 2, 51 | }, 52 | }, 53 | ], 54 | }, 55 | }, 56 | nohmValidations: { 57 | alphanumeric: Function alphanumeric {}, 58 | customValidationFile: Function {}, 59 | date: Function date {}, 60 | dateISO: Function dateISO {}, 61 | digits: Function digits {}, 62 | email: Function email {}, 63 | instanceValidation: Function {}, 64 | length: Function length {}, 65 | minMax: Function minMax {}, 66 | notEmpty: Function notEmpty {}, 67 | number: Function number {}, 68 | numberEU: Function numberEU {}, 69 | numberSI: Function numberSI {}, 70 | numberUS: Function numberUS {}, 71 | regexp: Function regexp {}, 72 | url: Function url {}, 73 | }, 74 | validate: Function {}, 75 | } 76 | -------------------------------------------------------------------------------- /test/snapshots/middleware.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maritz/nohm/741a646a2515db1008d5f5e29a98c029419abeec/test/snapshots/middleware.test.ts.snap -------------------------------------------------------------------------------- /test/testArgs.ts: -------------------------------------------------------------------------------- 1 | import { NohmClass } from '../ts'; 2 | import * as NodeRedis from 'redis'; 3 | import * as IORedis from 'ioredis'; 4 | 5 | export let prefix = 'nohmtests'; 6 | export let noCleanup = false; 7 | export let setMeta = false; 8 | export let redisHost = '127.0.0.1'; 9 | export let redisPort = 6379; 10 | 11 | export let redisAuth: undefined | string; 12 | export let redis: any; 13 | export let secondaryClient: any; 14 | 15 | process.argv.forEach((val, index) => { 16 | if (val === '--nohm-prefix') { 17 | prefix = process.argv[index + 1]; 18 | } 19 | if (val === '--no-cleanup') { 20 | noCleanup = true; 21 | } 22 | if (val === '--redis-host') { 23 | redisHost = process.argv[index + 1]; 24 | } 25 | if (val === '--redis-port') { 26 | redisPort = parseInt(process.argv[index + 1], 10); 27 | } 28 | if (val === '--redis-auth') { 29 | redisAuth = process.argv[index + 1]; 30 | } 31 | }); 32 | 33 | if (process.env.NOHM_TEST_IOREDIS === 'true') { 34 | redis = new IORedis({ 35 | port: redisPort, 36 | host: redisHost, 37 | password: redisAuth, 38 | }); 39 | 40 | secondaryClient = new IORedis({ 41 | port: redisPort, 42 | host: redisHost, 43 | password: redisAuth, 44 | }); 45 | } else { 46 | redis = NodeRedis.createClient(redisPort, redisHost, { 47 | auth_pass: redisAuth, 48 | retry_strategy(options) { 49 | console.error( 50 | '\nFailed to connect to primary redis:', 51 | options.error, 52 | '\n', 53 | ); 54 | return new Error('Redis connection failed'); 55 | }, 56 | }); 57 | 58 | secondaryClient = NodeRedis.createClient(redisPort, redisHost, { 59 | auth_pass: redisAuth, 60 | retry_strategy(options: any) { 61 | console.error( 62 | '\nFailed to connect to secondary redis:', 63 | options.error, 64 | '\n', 65 | ); 66 | return new Error('Redis connection failed'); 67 | }, 68 | }); 69 | } 70 | 71 | let readyChecksAdded = 0; 72 | 73 | export const setClient = (nohm: NohmClass, client: NodeRedis.RedisClient) => { 74 | return new Promise((resolve) => { 75 | if (!client) { 76 | client = redis; 77 | } 78 | // since we potentially set a client for each test, this could be called a lot and is not indicative of a leak. 79 | // thus we set max listeners higher to prevent warnings 80 | client.setMaxListeners(10 + ++readyChecksAdded); 81 | client.once('ready', () => { 82 | nohm.setClient(client); 83 | resolve(true); 84 | }); 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "experimentalDecorators": true, 7 | "noImplicitAny": false, 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "noUnusedLocals": true, 12 | "strictNullChecks": true, 13 | "noUnusedParameters": true, 14 | "lib": ["es7"], 15 | "noEmit": true, 16 | "sourceMap": false, 17 | "declarationMap": true 18 | }, 19 | "include": ["./**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "object-literal-sort-keys": [true, "match-declaration-order-only"], 5 | "no-implicit-dependencies": [true, "dev"], 6 | "variable-name": [true, "allow-pascal-case", "allow-leading-underscore"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/typescript.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import * as args from './testArgs'; 4 | 5 | import { cleanUpPromise } from './helper'; 6 | 7 | import { Nohm, NohmModel, TTypedDefinitions, ValidationError } from '../ts'; 8 | import { 9 | integerProperty, 10 | IPropertyDiff, 11 | stringProperty, 12 | } from '../ts/model.header'; 13 | 14 | const nohm = Nohm; 15 | 16 | const redis = args.redis; 17 | 18 | /* 19 | * This file tests a bunch of Stuff for Typescript just by being compiled in addition to 20 | * being included in the nodeunit tests. 21 | */ 22 | 23 | interface IUserLinkProps { 24 | name: string; 25 | test: string; 26 | number: number; 27 | } 28 | // tslint:disable:max-classes-per-file 29 | class UserMockup extends NohmModel { 30 | public static modelName = 'UserMockup'; 31 | public publish = true; 32 | protected static idGenerator = 'increment'; 33 | protected static definitions = { 34 | name: { 35 | defaultValue: 'defaultName', 36 | type: stringProperty, 37 | validations: ['notEmpty'], 38 | }, 39 | number: { 40 | defaultValue: 123, 41 | type: integerProperty, 42 | validations: ['notEmpty'], 43 | }, 44 | test: { 45 | defaultValue: 'defaultTest', 46 | type: stringProperty, 47 | validations: ['notEmpty'], 48 | }, 49 | }; 50 | 51 | public testMethodTypecheck( 52 | _arg1: string, 53 | _arg2: number, 54 | ): undefined | string | (() => any) { 55 | return this.options.idGenerator; 56 | } 57 | } 58 | const userMockupClass = nohm.register(UserMockup); 59 | 60 | interface ICommentProps { 61 | text: string; 62 | } 63 | const commentMockup = nohm.register( 64 | class extends NohmModel { 65 | public static modelName = 'CommentMockup'; 66 | 67 | protected static definitions: TTypedDefinitions = { 68 | text: { 69 | defaultValue: 'defaultComment', 70 | type: 'string', 71 | validations: ['notEmpty'], 72 | }, 73 | }; 74 | 75 | get pName(): string { 76 | return this.allProperties().text; 77 | } 78 | 79 | set pName(value: string) { 80 | this.property('text', value); 81 | } 82 | }, 83 | ); 84 | 85 | interface IRoleLinkProps { 86 | name: string; 87 | } 88 | class RoleLinkMockup extends NohmModel { 89 | public static modelName = 'RoleLinkMockup'; 90 | protected static definitions: TTypedDefinitions = { 91 | name: { 92 | defaultValue: 'admin', 93 | type: 'string', 94 | }, 95 | }; 96 | 97 | get pName(): string { 98 | return this.allProperties().name; 99 | } 100 | } 101 | const roleLinkMockup = nohm.register(RoleLinkMockup); 102 | 103 | const prefix = args.prefix + 'typescript'; 104 | 105 | test.before(async () => { 106 | nohm.setPrefix(prefix); 107 | await args.setClient(nohm, redis); 108 | await cleanUpPromise(redis, prefix); 109 | }); 110 | 111 | test.afterEach(async () => { 112 | await cleanUpPromise(redis, prefix); 113 | }); 114 | 115 | test.serial('static methods', async (t) => { 116 | interface IAdditionalMethods { 117 | test1(): Promise; 118 | test2(player: any): Promise; 119 | } 120 | 121 | const simpleModel = nohm.model( 122 | 'SimpleModelRegistration', 123 | { 124 | properties: { 125 | name: { 126 | defaultValue: 'simple', 127 | type: 'string', 128 | }, 129 | }, 130 | // tslint:disable-next-line:object-literal-sort-keys 131 | methods: { 132 | test1(): Promise { 133 | return this.validate(); 134 | }, 135 | async test2(player: any) { 136 | await this.save(); 137 | this.link(player, 'leader'); 138 | this.link(player); 139 | }, 140 | }, 141 | }, 142 | ); 143 | 144 | t.is( 145 | typeof commentMockup.findAndLoad, 146 | 'function', 147 | 'findAndLoad was not set on commentMockup', 148 | ); 149 | t.is( 150 | typeof commentMockup.sort, 151 | 'function', 152 | 'findAndLoad was not set on commentMockup', 153 | ); 154 | t.is( 155 | typeof commentMockup.find, 156 | 'function', 157 | 'findAndLoad was not set on commentMockup', 158 | ); 159 | t.is( 160 | typeof simpleModel.findAndLoad, 161 | 'function', 162 | 'findAndLoad was not set on commentMockup', 163 | ); 164 | t.is( 165 | typeof simpleModel.sort, 166 | 'function', 167 | 'findAndLoad was not set on commentMockup', 168 | ); 169 | t.is( 170 | typeof simpleModel.find, 171 | 'function', 172 | 'findAndLoad was not set on commentMockup', 173 | ); 174 | const testInstance = new simpleModel(); 175 | testInstance.test1(); 176 | }); 177 | 178 | test.serial('instances', async (t) => { 179 | const comment = new commentMockup(); 180 | const user = await nohm.factory('UserMockup'); 181 | try { 182 | const role = new RoleLinkMockup(); 183 | // do something with role so it doesn't cause a compile error 184 | await role.remove(); 185 | t.fail('Directly constructing a class did not throw an error.'); 186 | } catch (err) { 187 | t.is( 188 | err.message, 189 | 'Class is not extended properly. Use the return Nohm.register() instead of your class directly.', 190 | 'Directly constructing a class did not throw the correct error.', 191 | ); 192 | } 193 | 194 | t.is( 195 | comment.property('text'), 196 | 'defaultComment', 197 | 'Getting property text of comment failed', 198 | ); 199 | t.is( 200 | user.property('test'), 201 | 'defaultTest', 202 | 'Getting property test of user failed', 203 | ); 204 | t.is( 205 | user.allProperties().name, 206 | 'defaultName', 207 | 'Getting allProperties().name of user failed', 208 | ); 209 | t.is(await user.validate(), true, 'Checking validity failed'); 210 | t.deepEqual(user.errors.name, [], 'Error was set?'); 211 | await user.save(); 212 | const users = await userMockupClass.findAndLoad({}); 213 | const numbers: Array = users.map((x) => x.property('number')); 214 | t.deepEqual(numbers, [123]); 215 | }); 216 | 217 | test.serial( 218 | 'method declaration is type checked & idGenerator set from static', 219 | async (t) => { 220 | const testInstance = await nohm.factory('UserMockup'); 221 | const idGenerator: 222 | | undefined 223 | | string 224 | | (() => any) = testInstance.testMethodTypecheck('asd', 123); 225 | t.is(idGenerator, 'increment', 'The type check method returned false.'); 226 | }, 227 | ); 228 | 229 | test.serial('typing in property()', async (t) => { 230 | const user = await nohm.factory('UserMockup'); 231 | 232 | const name: string = user.property('name'); 233 | const num: number = user.property('number', 456); 234 | const multiple = user.property({ 235 | name: 'changedName', 236 | number: 789, 237 | }); 238 | 239 | t.is(name, 'defaultName', 'Getting assigned and typed name of user failed.'); 240 | t.is(num, 456, 'Getting assigned and typed number of user failed.'); 241 | t.is( 242 | multiple.name, 243 | 'changedName', 244 | 'Getting assigned and typed multi.name of user failed.', 245 | ); 246 | t.is( 247 | multiple.number, 248 | 789, 249 | 'Getting assigned and typed multi.number of user failed.', 250 | ); 251 | t.is( 252 | multiple.test, 253 | undefined, 254 | 'Getting assigned and typed multi.test of user failed.', 255 | ); 256 | }); 257 | 258 | test.serial('typing in find()', async (t) => { 259 | const user = await nohm.factory('UserMockup'); 260 | 261 | try { 262 | await user.find({ 263 | name: 'changedName', 264 | number: 789, 265 | }); 266 | 267 | await userMockupClass.find({ 268 | name: 'changedName', 269 | number: 789, 270 | }); 271 | 272 | await userMockupClass.findAndLoad({ 273 | name: 'changedName', 274 | number: 789, 275 | }); 276 | } catch (e) { 277 | // properties aren't indexed, whatever... just testing that the generics are right and manually testing errors 278 | // in find options 279 | t.pass("I'm sure it's fine."); 280 | } 281 | }); 282 | 283 | test.serial('typing in subscribe()', async (t) => { 284 | await nohm.setPubSubClient(args.secondaryClient); 285 | 286 | const user = await nohm.factory('UserMockup'); 287 | const role = new roleLinkMockup(); 288 | 289 | let propertyDiff: Array>; 290 | let userId: string; 291 | 292 | const initialProps = user.allProperties(); 293 | await user.subscribe('create', (payload) => { 294 | t.is(payload.target.id, user.id, 'id in create handler was wrong'); 295 | if (user.id) { 296 | userId = user.id; // used in other tests 297 | } 298 | t.is( 299 | payload.target.modelName, 300 | user.modelName, 301 | 'modelname in create handler was wrong', 302 | ); 303 | t.deepEqual( 304 | payload.target.properties, 305 | { 306 | ...initialProps, 307 | id: userId, 308 | }, 309 | 'properties in create handler were wrong', 310 | ); 311 | }); 312 | await user.subscribe('link', (payload) => { 313 | t.is(payload.child.id, user.id, 'id in link handler CHILD was wrong'); 314 | t.is( 315 | payload.child.modelName, 316 | user.modelName, 317 | 'modelname in link handler CHILD was wrong', 318 | ); 319 | t.deepEqual( 320 | payload.child.properties, 321 | user.allProperties(), 322 | 'properties in link handler CHILD were wrong', 323 | ); 324 | t.is(payload.parent.id, role.id, 'id in link handler PARENT was wrong'); 325 | t.is( 326 | payload.parent.modelName, 327 | role.modelName, 328 | 'modelname in link handler PARENT was wrong', 329 | ); 330 | t.deepEqual( 331 | payload.parent.properties, 332 | role.allProperties(), 333 | 'properties in link handler PARENT were wrong', 334 | ); 335 | }); 336 | await user.subscribe('update', (payload) => { 337 | t.is(payload.target.id, user.id, 'id in update handler was wrong'); 338 | t.is( 339 | payload.target.modelName, 340 | user.modelName, 341 | 'modelname in update handler was wrong', 342 | ); 343 | t.deepEqual( 344 | payload.target.properties, 345 | user.allProperties(), 346 | 'properties in update handler was wrong', 347 | ); 348 | t.deepEqual( 349 | payload.target.diff, 350 | propertyDiff, 351 | 'properties in update handler were wrong', 352 | ); 353 | }); 354 | await user.subscribe('remove', (payload) => { 355 | t.not(payload.target.id, null, 'id in remove handler was null'); 356 | t.is(payload.target.id, userId, 'id in remove handler was wrong'); 357 | t.is( 358 | payload.target.modelName, 359 | user.modelName, 360 | 'modelname in remove handler was wrong', 361 | ); 362 | t.deepEqual( 363 | payload.target.properties, 364 | user.allProperties(), 365 | 'properties in remove handler were wrong', 366 | ); 367 | }); 368 | 369 | await user.save(); 370 | user.link(role); 371 | user.property('name', 'foobar'); 372 | propertyDiff = user.propertyDiff(); 373 | await user.save(); 374 | await user.remove(); 375 | }); 376 | 377 | test.serial('validation errors', async (t) => { 378 | // see above for their different ways of setup/definition 379 | const user = await nohm.factory('UserMockup'); 380 | user.property('name', ''); 381 | try { 382 | await user.save(); 383 | t.fail('No error thrown thrown'); 384 | } catch (err) { 385 | if (err instanceof ValidationError && err.modelName === 'UserMockup') { 386 | t.deepEqual((err as ValidationError).errors.name, [ 387 | 'notEmpty', 388 | ]); 389 | } else { 390 | t.fail('Wrong kind of error thrown.'); 391 | } 392 | } 393 | }); 394 | -------------------------------------------------------------------------------- /ts/errors/LinkError.ts: -------------------------------------------------------------------------------- 1 | import { ILinkSaveResult } from '../model.header'; 2 | 3 | export interface ILinkError extends Error { 4 | errors: Array; 5 | } 6 | 7 | // tslint:disable:max-line-length 8 | /** 9 | * Details about which part of linking failed. 10 | * 11 | * @type { Array.<{ success: boolean, child: NohmModel, parent: NohmModel, error: null | Error | LinkError | ValidationError}> } 12 | * @name errors 13 | * @memberof NohmErrors.LinkError# 14 | */ 15 | // tslint:enable:max-line-length 16 | 17 | /** 18 | * Error thrown whenever linking failed during {@link NohmModel#save}. 19 | * 20 | * @class LinkError 21 | * @memberof NohmErrors 22 | * @extends {Error} 23 | */ 24 | export class LinkError extends Error implements ILinkError { 25 | constructor( 26 | public errors: Array, 27 | errorMessage = 'Linking failed. See .errors on this Error object for an Array of failures.', 28 | ) { 29 | super(errorMessage); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ts/errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import { IDictionary } from '../model.header'; 2 | 3 | export interface IValidationError extends Error { 4 | modelName: string; 5 | errors: { [key in keyof TProps]?: Array }; 6 | } 7 | 8 | // tslint:disable:max-line-length 9 | /** 10 | * Details about which properties failed to validate in which way. 11 | * 12 | * The type is an object with property names as keys and then an array with validation 13 | * names of the validations that failed 14 | * 15 | * @type { Object.> } 16 | * @name errors 17 | * @memberof NohmErrors.ValidationError# 18 | */ 19 | // tslint:enable:max-line-length 20 | 21 | /** 22 | * Error thrown whenever validation failed during {@link NohmModel#validate} or {@link NohmModel#save}. 23 | * 24 | * @class ValidationError 25 | * @memberof NohmErrors 26 | * @extends {Error} 27 | */ 28 | export class ValidationError extends Error 29 | implements IValidationError { 30 | public readonly errors: IValidationError['errors']; 31 | public readonly modelName: string; 32 | 33 | constructor( 34 | errors: IValidationError['errors'], 35 | modelName: string, 36 | errorMessage: string = 'Validation failed. See .errors on this Error or the Nohm model instance for details.', 37 | ) { 38 | super(errorMessage); 39 | const emptyErrors: IValidationError['errors'] = {}; 40 | this.modelName = modelName; 41 | this.errors = Object.keys(errors).reduce< 42 | IValidationError['errors'] 43 | >((obj, key) => { 44 | const error = errors[key]; 45 | if (error && error.length > 0) { 46 | obj[key] = error; 47 | } 48 | return obj; 49 | }, emptyErrors); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ts/eventComposers.ts: -------------------------------------------------------------------------------- 1 | import { NohmModel } from './model'; 2 | import { 3 | IChangeEventPayload, 4 | IDefaultEventPayload, 5 | IPropertyDiff, 6 | IRelationChangeEventPayload, 7 | IDictionary, 8 | } from './model.header'; 9 | 10 | // The default (base) message creator 11 | export function defaultComposer( 12 | this: NohmModel, 13 | ): IDefaultEventPayload { 14 | return { 15 | target: { 16 | id: this.id, 17 | modelName: this.modelName, 18 | properties: this.allProperties(), 19 | }, 20 | }; 21 | } 22 | 23 | export { defaultComposer as create }; 24 | 25 | // This populates the diff property for `save` and `update` events. 26 | function changeComposer( 27 | this: NohmModel, 28 | diff: Array>, 29 | ): IChangeEventPayload { 30 | const result = defaultComposer.apply(this); 31 | result.target.diff = diff; 32 | return result; 33 | } 34 | 35 | export { changeComposer as update, changeComposer as save }; 36 | 37 | // This sets the id and properties 38 | export function remove(this: NohmModel, id: string) { 39 | const result = defaultComposer.apply(this); 40 | result.target.id = id; 41 | return result; 42 | } 43 | 44 | function relationComposer( 45 | this: NohmModel, 46 | parent: NohmModel, 47 | relationName: string, 48 | ): IRelationChangeEventPayload { 49 | const childPayload: IDefaultEventPayload = defaultComposer.call(this); 50 | const parentPayload: IDefaultEventPayload = defaultComposer.call( 51 | parent, 52 | ); 53 | return { 54 | child: childPayload.target, 55 | parent: parentPayload.target, 56 | relation: relationName, 57 | }; 58 | } 59 | 60 | export { relationComposer as link, relationComposer as unlink }; 61 | -------------------------------------------------------------------------------- /ts/helpers.ts: -------------------------------------------------------------------------------- 1 | export interface INohmPrefixes { 2 | channel: string; 3 | hash: string; 4 | incrementalIds: string; 5 | idsets: string; 6 | index: string; 7 | meta: { 8 | version: string; 9 | idGenerator: string; 10 | properties: string; 11 | }; 12 | relationKeys: string; 13 | relations: string; 14 | scoredindex: string; 15 | unique: string; 16 | } 17 | 18 | export function getPrefix(prefix: string): INohmPrefixes { 19 | return { 20 | channel: prefix + ':channel:', 21 | hash: prefix + ':hash:', 22 | idsets: prefix + ':idsets:', 23 | incrementalIds: prefix + ':ids:', 24 | index: prefix + ':index:', 25 | meta: { 26 | idGenerator: prefix + ':meta:idGenerator:', 27 | properties: prefix + ':meta:properties:', 28 | version: prefix + ':meta:version:', 29 | }, 30 | relationKeys: prefix + ':relationKeys:', 31 | relations: prefix + ':relations:', 32 | scoredindex: prefix + ':scoredindex:', 33 | unique: prefix + ':uniques:', 34 | }; 35 | } 36 | 37 | export function checkEqual(obj1: any, obj2: any): boolean { 38 | if (obj1 === obj2) { 39 | return true; 40 | } 41 | if (!obj1 || (obj1 && !obj2)) { 42 | return false; 43 | } 44 | if ( 45 | Object.hasOwnProperty.call(obj1, 'modelName') && 46 | Object.hasOwnProperty.call(obj2, 'modelName') && 47 | obj1.modelName === obj2.modelName 48 | ) { 49 | // both must have the same id. 50 | if (obj1.id && obj2.id && obj1.id === obj2.id) { 51 | return true; 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | export function callbackError(...args: Array) { 58 | if (args.length > 0) { 59 | const lastArgument = args[args.length - 1]; 60 | if (typeof lastArgument === 'function') { 61 | throw new Error( 62 | 'Callback style has been removed. Use the returned promise.', 63 | ); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ts/idGenerators.ts: -------------------------------------------------------------------------------- 1 | import { v1 as uuid } from 'uuid'; 2 | import * as redis from 'redis'; 3 | import * as Debug from 'debug'; 4 | 5 | const debug = Debug('nohm:idGenerator'); 6 | 7 | export interface IGenerators { 8 | [key: string]: ( 9 | client: redis.RedisClient, 10 | idPrefix: string, 11 | ) => Promise; 12 | } 13 | 14 | export const idGenerators: IGenerators = { 15 | default: async function defaultGenerator(): Promise { 16 | const newId = uuid(); 17 | debug('Generated default (uuid) id: %s.', newId); 18 | return newId; 19 | }, 20 | 21 | increment: function incrementGenerator( 22 | client: redis.RedisClient, 23 | idPrefix: string, 24 | ): Promise { 25 | return new Promise((resolve, reject) => { 26 | client.incr(idPrefix, (err, newId) => { 27 | if (err) { 28 | reject(err); 29 | } else { 30 | debug('Generated incremental id: %s.', newId); 31 | resolve(newId.toString(10)); 32 | } 33 | }); 34 | }); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /ts/middleware.ts: -------------------------------------------------------------------------------- 1 | import * as Debug from 'debug'; 2 | import * as fs from 'fs'; 3 | import { IncomingMessage, ServerResponse } from 'http'; 4 | 5 | import { nohm as instantiatedNohm, NohmClass } from '.'; 6 | import { NohmModel } from './model'; 7 | import { universalValidatorPath } from './validators'; 8 | 9 | export interface IExclusionsOption { 10 | [key: string]: Array | boolean; 11 | } 12 | 13 | export type TRequestHandler = ( 14 | req: IncomingMessage, 15 | res: ServerResponse, 16 | next?: any, 17 | ) => void; 18 | 19 | export interface IMiddlewareOptions { 20 | url?: string; 21 | namespace?: string; 22 | maxAge?: number; 23 | exclusions?: { 24 | [key: string]: IExclusionsOption | boolean; 25 | }; 26 | extraFiles?: string | Array; 27 | uglify?: any; 28 | } 29 | 30 | const debug = Debug('nohm:middleware'); 31 | 32 | const MAX_DEPTH = 5; 33 | 34 | function customToString(obj: any, depth: number = 0): string { 35 | if (depth > MAX_DEPTH) { 36 | console.warn( 37 | new Error('nohm: middleware customToString() maxdepth exceeded').stack, 38 | ); 39 | return ''; 40 | } 41 | switch (typeof obj) { 42 | case 'string': 43 | return '"' + obj + '"'; 44 | case 'number': 45 | return obj.toString(); 46 | case 'boolean': 47 | return obj ? 'true' : 'false'; 48 | case 'function': 49 | if (obj instanceof RegExp) { 50 | return obj.toString(); 51 | } 52 | break; 53 | case 'object': 54 | if (Array.isArray(obj)) { 55 | const arr: Array = []; 56 | obj.forEach((val) => { 57 | arr.push(customToString(val, depth + 1)); 58 | }); 59 | return '[' + arr.join(',') + ']'; 60 | } else if (obj instanceof RegExp) { 61 | return obj.toString(); 62 | } else { 63 | const arr: Array = []; 64 | Object.keys(obj).forEach((val) => { 65 | arr.push('"' + val + '":' + customToString(obj[val], depth + 1)); 66 | }); 67 | return '{' + arr.join(',') + '}'; 68 | } 69 | default: 70 | return ''; 71 | } 72 | return ''; 73 | } 74 | 75 | function validationsFlatten( 76 | model: new (...args: Array) => NohmModel, 77 | exclusions: IExclusionsOption = {}, 78 | ): string { 79 | const instance = new model(); 80 | const definitions = instance.getDefinitions(); 81 | let str = instance.modelName + ': {'; 82 | 83 | /* 84 | * example exclusions object 85 | * { 86 | * // this will ignore the first validation in the validation definition array for name in the model definition 87 | * name: [0], 88 | * // this will completely ignore all validations for the salt property 89 | * salt: true 90 | * }, 91 | */ 92 | 93 | const exclusionsStrings: Array = []; 94 | const exclusionsObject: { [key: string]: Array } = {}; 95 | Object.keys(exclusions).forEach((key) => { 96 | const value = exclusions[key]; 97 | if (Array.isArray(value)) { 98 | exclusionsObject[key] = value.map((x) => !!x); 99 | } 100 | exclusionsStrings.push(key); 101 | }); 102 | 103 | Object.keys(definitions).forEach((key) => { 104 | const isExcepted = 105 | exclusionsStrings.indexOf(key) !== -1 && 106 | !exclusionsObject.hasOwnProperty(key); 107 | if (!isExcepted) { 108 | const vals = definitions[key].validations; 109 | if (Array.isArray(vals) && vals.length > 0) { 110 | str += `${key}: [`; 111 | const strVals: Array = []; 112 | vals.forEach((val, index) => { 113 | if (!exclusionsObject[key] || exclusionsObject[key][index]) { 114 | strVals.push(customToString(val)); 115 | } 116 | }); 117 | str += strVals.join(',') + '], '; 118 | } 119 | } 120 | }); 121 | return str + '}'; 122 | } 123 | 124 | let extraFilesIndex = 0; 125 | 126 | function wrapFile(fileStr: string, namespace: string) { 127 | let str = `${namespace}.extraValidations[${extraFilesIndex}]={};(function (exports) {`; 128 | str += fileStr; 129 | str += `})(${namespace}.extraValidations[${extraFilesIndex}]);`; 130 | extraFilesIndex++; 131 | return str; 132 | } 133 | 134 | function wrapExtraFiles(files: Array, namespace: string) { 135 | let str = ''; 136 | files.forEach((path) => { 137 | const fileStr = fs.readFileSync(path, 'utf-8'); 138 | str += wrapFile(fileStr, namespace); 139 | }); 140 | return str; 141 | } 142 | 143 | /** 144 | * Returns a middleware that can deliver the validations as a javascript file 145 | * and the modelspecific validations as a JSON object to the browser. 146 | * This is useful if you want to save some bandwith by doing the validations 147 | * in the browser before saving to the server. 148 | * 149 | * Example: 150 | * 151 | * ``` 152 | * server.use(nohm.middleware( 153 | * // options object 154 | * { 155 | * url: '/nohm.js', 156 | * namespace: 'nohm', 157 | * exclusions: { 158 | * 159 | * User: { // modelName 160 | * 161 | * // this will ignore the second validation in the validation definition array for 162 | * // the property 'name' in the model definition 163 | * name: [false, true], 164 | * 165 | * // this will completely ignore all validations for the salt property 166 | * salt: true 167 | * }, 168 | * 169 | * Privileges: true // this will completely ignore the Priviledges model 170 | * } 171 | * } 172 | * )); 173 | * ``` 174 | * 175 | * @see https://maritz.github.io/nohm/#browser-validation 176 | * @param {Object} options Options for the middleware 177 | * @param {string} [options.url='/nomValidations.js'] Url under which the js file will be available. 178 | * @param {object.} [options.exclusions={}] Object containing exclusions for the 179 | * validations export - see example for details 180 | * @param {string} [options.namespace='nomValidations'] Namespace to be used by the js file in the browser. 181 | * @param {string} [options.extraFiles=[]] Extra files containing validations. 182 | * You should only use this if they are not already set via Nohm.setExtraValidations 183 | * as this automatically includes those. 184 | * @param {number} [options.maxAge=3600] Cache control in seconds. (Default is one hour) 185 | * @param {boolean} [options.uglify=false] True to enable minification. 186 | * Requires uglify-js to be installed in your project! 187 | * @return {Middleware~callback} 188 | * @instance 189 | * @memberof NohmClass 190 | */ 191 | export function middleware( 192 | options: IMiddlewareOptions, 193 | nohm: NohmClass = instantiatedNohm, 194 | ): TRequestHandler { 195 | options = options || {}; 196 | const url = options.url || '/nohmValidations.js'; 197 | const namespace = options.namespace || 'nohmValidations'; 198 | const maxAge = options.maxAge || 3600; // 1 hour 199 | const exclusions = options.exclusions || {}; 200 | let extraFiles = options.extraFiles || []; 201 | let uglify = options.uglify || false; 202 | if (!Array.isArray(extraFiles)) { 203 | extraFiles = [extraFiles]; 204 | } 205 | 206 | // collect models 207 | const collectedModels: Array = []; 208 | const models = nohm.getModels(); 209 | Object.keys(models).forEach((name) => { 210 | const model = models[name]; 211 | let exclusion = exclusions[name]; 212 | if (exclusion === true) { 213 | return; // exception set, but no fields 214 | } else { 215 | if (exclusion === true || exclusion === false) { 216 | exclusion = {}; 217 | } 218 | collectedModels.push(validationsFlatten(model, exclusion)); 219 | } 220 | }); 221 | 222 | let str = `var nohmValidationsNamespaceName = "${namespace}"; 223 | var ${namespace}={"extraValidations": [], "models":{${collectedModels.join( 224 | ',', 225 | )}}}; 226 | // extrafiles 227 | ${wrapExtraFiles(extraFiles, namespace)} 228 | // extravalidations 229 | ${ 230 | wrapExtraFiles( 231 | nohm.getExtraValidatorFileNames(), 232 | namespace, 233 | ) /* needs to somehow access the same thing */ 234 | } 235 | // validators.js 236 | ${fs.readFileSync(universalValidatorPath, 'utf-8')}`; 237 | 238 | if (uglify) { 239 | try { 240 | // tslint:disable-next-line:no-implicit-dependencies 241 | uglify = require('uglify-js'); 242 | } catch (e) { 243 | console.warn( 244 | 'You tried to use the uglify option in Nohm.connect but uglify-js is not requirable.', 245 | 'Continuing without uglify.', 246 | e, 247 | ); 248 | } 249 | if (uglify.parser && uglify.uglify) { 250 | const jsp = uglify.parser; 251 | const pro = uglify.uglify; 252 | 253 | const ast = jsp.parse(str); 254 | // ast = pro.ast_mangle(ast); // TODO: test if this works with our globals 255 | const squeezed = pro.ast_squeeze(ast); 256 | str = pro.gen_code(squeezed); 257 | } 258 | } 259 | debug( 260 | `Setting up middleware to be served on '%s' with namespace '%s' and collected these models: %o`, 261 | url, 262 | namespace, 263 | collectedModels, 264 | ); 265 | 266 | /** 267 | * This function is what is returned by {@link NohmClass#middleware}. 268 | * 269 | * @callback Middleware~callback 270 | * @name MiddlewareCallback 271 | * @function 272 | * @param {Object} req http IncomingMessage 273 | * @param {Object} res http ServerResponse 274 | * @param {function} [next] Optional next function for express/koa 275 | * @memberof Nohm 276 | */ 277 | 278 | return (req: IncomingMessage, res: ServerResponse, next?: any) => { 279 | if (req.url === url) { 280 | res.statusCode = 200; 281 | res.setHeader('Content-Type', 'text/javascript'); 282 | res.setHeader('Content-Length', str.length.toString()); 283 | res.setHeader('Cache-Control', 'public, max-age=' + maxAge); 284 | res.end(str); 285 | } else if (next && typeof next === 'function') { 286 | next(); 287 | } 288 | }; 289 | } 290 | -------------------------------------------------------------------------------- /ts/model.header.ts: -------------------------------------------------------------------------------- 1 | import { NohmModel } from './model'; 2 | 3 | export type TPropertyTypeNames = 4 | | 'string' 5 | | 'bool' 6 | | 'boolean' 7 | | 'integer' 8 | | 'int' 9 | | 'float' 10 | | 'number' 11 | | 'date' 12 | | 'time' 13 | | 'timestamp' 14 | | 'json'; 15 | export const stringProperty: TPropertyTypeNames = 'string'; 16 | export const boolProperty: TPropertyTypeNames = 'bool'; 17 | export const integerProperty: TPropertyTypeNames = 'integer'; 18 | export const floatProperty: TPropertyTypeNames = 'float'; 19 | export const numberProperty: TPropertyTypeNames = 'number'; 20 | export const dateProperty: TPropertyTypeNames = 'date'; 21 | export const timeProperty: TPropertyTypeNames = 'time'; 22 | export const timestampProperty: TPropertyTypeNames = 'timestamp'; 23 | export const jsonProperty: TPropertyTypeNames = 'json'; 24 | 25 | export interface IDictionary { 26 | [index: string]: any; 27 | } 28 | 29 | export type PropertyBehavior = ( 30 | this: TModel, 31 | newValue: string, 32 | key: string, 33 | oldValue: string, 34 | ) => any; 35 | 36 | export interface IStaticMethods { 37 | new (): T; 38 | load

        (id: any): Promise

        ; 39 | loadMany

        (id: Array): Promise>; 40 | findAndLoad

        ( 41 | searches?: Partial< 42 | { 43 | [key in keyof TProps]: 44 | | string 45 | | number 46 | | boolean 47 | | Partial; 48 | } 49 | >, 50 | ): Promise>; 51 | sort( 52 | sortOptions: ISortOptions, 53 | ids?: Array | false, 54 | ): Promise>; 55 | find( 56 | searches: Partial< 57 | { 58 | [key in keyof TProps]: 59 | | string 60 | | number 61 | | boolean 62 | | Partial; 63 | } 64 | >, 65 | ): Promise>; 66 | remove(id: any): Promise; 67 | } 68 | 69 | export type validatiorFunction = (value: any, options: any) => Promise; 70 | 71 | export interface IValidationObject { 72 | name: string; 73 | options: { [index: string]: any }; 74 | validator: validatiorFunction; 75 | } 76 | 77 | export type TValidationDefinition = 78 | | string 79 | | { name: string; options: any } 80 | | validatiorFunction; 81 | 82 | export interface IModelPropertyDefinition { 83 | /** 84 | * Whether the property should be indexed. Depending on type this creates different keys/collections. 85 | * Does not work for all types. TODO: specify here which types. 86 | * 87 | * @type {boolean} 88 | * @memberof IModelPropertyDefinition 89 | */ 90 | index?: boolean; 91 | defaultValue?: any; 92 | load_pure?: boolean; 93 | type: TPropertyTypeNames | PropertyBehavior; 94 | unique?: boolean; 95 | validations?: Array; 96 | } 97 | 98 | export type TTypedDefinitions = { 99 | [props in keyof TProps]: IModelPropertyDefinition; 100 | }; 101 | 102 | export interface IModelPropertyDefinitions { 103 | [propName: string]: IModelPropertyDefinition; 104 | } 105 | 106 | export type TIdGenerators = 'default' | 'increment'; 107 | 108 | export interface IModelOptions { 109 | metaCallback?: (error: string | Error | null, version?: string) => any; 110 | methods?: { 111 | [name: string]: (this: NohmModel, ...args: Array) => any; 112 | }; 113 | properties: IModelPropertyDefinitions; 114 | publish?: boolean; 115 | idGenerator?: TIdGenerators | (() => any); 116 | } 117 | 118 | export interface ISaveOptions { 119 | silent?: boolean; 120 | skip_validation_and_unique_indexes?: boolean; 121 | } 122 | 123 | export interface IProperty { 124 | value: any; 125 | __updated: boolean; 126 | __oldValue: any; 127 | __numericIndex: boolean; // this is static but private so for now it might be better here than in definitions 128 | } 129 | 130 | export interface IPropertyDiff { 131 | key: TKeys; 132 | before: any; 133 | after: any; 134 | } 135 | 136 | export interface IValidationResult { 137 | key: string; 138 | valid: boolean; 139 | errors?: Array; 140 | } 141 | 142 | export interface IRelationChange { 143 | action: 'link' | 'unlink'; 144 | callback?: (...args: Array) => any; 145 | object: NohmModel; 146 | options: ILinkOptionsWithName; 147 | } 148 | 149 | export interface ILinkOptions { 150 | error?: (err: Error | string, otherObject: NohmModel) => any; 151 | name?: string; 152 | silent?: boolean; 153 | } 154 | 155 | export interface ILinkOptionsWithName extends ILinkOptions { 156 | name: string; 157 | } 158 | 159 | export interface ILinkSaveResult { 160 | success: boolean; 161 | child: NohmModel; 162 | parent: NohmModel; 163 | error: null | Error; 164 | } 165 | 166 | export interface IUnlinkKeyMapItem { 167 | ownIdsKey: string; 168 | otherIdsKey: string; 169 | } 170 | 171 | export interface ISearchOption { 172 | endpoints: '()' | '[]' | '[)' | '(]' | '(' | ')'; 173 | limit: number; 174 | min: number | '-inf' | '+inf'; 175 | max: number | '-inf' | '+inf'; 176 | offset: number; 177 | } 178 | 179 | export type TKey = keyof TProps; 180 | 181 | export interface IStructuredSearch { 182 | type: 'undefined' | 'unique' | 'set' | 'zset'; 183 | options: Partial; 184 | key: keyof TProps; 185 | value: any; 186 | } 187 | 188 | export interface ISortOptions { 189 | alpha?: 'ALPHA' | ''; 190 | direction?: 'ASC' | 'DESC'; 191 | field?: keyof TProps; 192 | limit?: Array; 193 | } 194 | 195 | export type TLinkCallback = ( 196 | action: string, 197 | ownModelName: string, 198 | relationName: string, 199 | other: T, 200 | ) => void; 201 | 202 | export interface IDefaultEventPayload { 203 | target: { 204 | id: null | string; 205 | modelName: string; 206 | properties: TProps & { id: string }; 207 | }; 208 | } 209 | 210 | export interface IChangeEventPayload { 211 | target: { 212 | id: string; 213 | modelName: string; 214 | properties: TProps; 215 | diff: Array>>; 216 | }; 217 | } 218 | 219 | export interface IRelationChangeEventPayload { 220 | child: IDefaultEventPayload['target']; 221 | parent: IDefaultEventPayload['target']; 222 | relation: string; 223 | } 224 | -------------------------------------------------------------------------------- /ts/typed-redis-helper.ts: -------------------------------------------------------------------------------- 1 | import { Multi, RedisClient } from 'redis'; 2 | import * as IORedis from 'ioredis'; 3 | 4 | export const errorMessage = 5 | 'Supplied redis client does not have the correct methods.'; 6 | 7 | export function get( 8 | client: RedisClient | Multi, 9 | key: string, 10 | ): Promise { 11 | return new Promise((resolve, reject) => { 12 | if (!client.get) { 13 | return reject(new Error(errorMessage)); 14 | } 15 | client.get(key, (err, value) => { 16 | if (err) { 17 | reject(err); 18 | } else { 19 | resolve(value && String(value)); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | export function exists( 26 | client: RedisClient | Multi, 27 | key: string, 28 | ): Promise { 29 | return new Promise((resolve, reject) => { 30 | if (!client.exists) { 31 | return reject(new Error(errorMessage)); 32 | } 33 | client.exists(key, (err, reply) => { 34 | if (err) { 35 | reject(err); 36 | } else { 37 | resolve(reply); 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | export function del( 44 | client: RedisClient | Multi, 45 | key: string | Array, 46 | ): Promise { 47 | return new Promise((resolve, reject) => { 48 | if (!client.del) { 49 | return reject(new Error(errorMessage)); 50 | } 51 | client.del(key, (err) => { 52 | if (err) { 53 | reject(err); 54 | } else { 55 | resolve(); 56 | } 57 | }); 58 | }); 59 | } 60 | 61 | export function set( 62 | client: RedisClient | Multi, 63 | key: string, 64 | value: string, 65 | ): Promise { 66 | return new Promise((resolve, reject) => { 67 | if (!client.set) { 68 | return reject(new Error(errorMessage)); 69 | } 70 | client.set(key, value, (err) => { 71 | if (err) { 72 | reject(err); 73 | } else { 74 | resolve(); 75 | } 76 | }); 77 | }); 78 | } 79 | 80 | export function mset( 81 | client: RedisClient | Multi, 82 | keyValueArrayOrString: string | Array, 83 | ...keyValuePairs: Array 84 | ): Promise { 85 | return new Promise((resolve, reject) => { 86 | if (!client.mset) { 87 | return reject(new Error(errorMessage)); 88 | } 89 | client.mset.apply(client, [ 90 | keyValueArrayOrString, 91 | ...keyValuePairs, 92 | (err: Error | null) => { 93 | if (err) { 94 | reject(err); 95 | } else { 96 | resolve(); 97 | } 98 | }, 99 | ]); 100 | }); 101 | } 102 | 103 | export function setnx( 104 | client: RedisClient | Multi, 105 | key: string, 106 | value: string, 107 | ): Promise { 108 | return new Promise((resolve, reject) => { 109 | if (!client.setnx) { 110 | return reject(new Error(errorMessage)); 111 | } 112 | client.setnx(key, value, (err, reply) => { 113 | if (err) { 114 | reject(err); 115 | } else { 116 | resolve(reply); 117 | } 118 | }); 119 | }); 120 | } 121 | 122 | export function smembers( 123 | client: RedisClient | Multi, 124 | key: string, 125 | ): Promise> { 126 | return new Promise>((resolve, reject) => { 127 | if (!client.smembers) { 128 | return reject(new Error(errorMessage)); 129 | } 130 | client.smembers(key, (err, values) => { 131 | if (err) { 132 | reject(err); 133 | } else { 134 | resolve(values); 135 | } 136 | }); 137 | }); 138 | } 139 | 140 | export function scard( 141 | client: RedisClient | Multi, 142 | key: string, 143 | ): Promise { 144 | return new Promise((resolve, reject) => { 145 | if (!client.scard) { 146 | return reject(new Error(errorMessage)); 147 | } 148 | client.scard(key, (err, value) => { 149 | if (err) { 150 | reject(err); 151 | } else { 152 | resolve(value); 153 | } 154 | }); 155 | }); 156 | } 157 | 158 | export function sismember( 159 | client: RedisClient | Multi, 160 | key: string, 161 | value: string, 162 | ): Promise { 163 | return new Promise((resolve, reject) => { 164 | if (!client.sismember) { 165 | return reject(new Error(errorMessage)); 166 | } 167 | client.sismember(key, value, (err, numFound) => { 168 | if (err) { 169 | reject(err); 170 | } else { 171 | resolve(numFound); 172 | } 173 | }); 174 | }); 175 | } 176 | 177 | export function sadd( 178 | client: RedisClient | Multi, 179 | key: string, 180 | value: string, 181 | ): Promise { 182 | return new Promise((resolve, reject) => { 183 | if (!client.sadd) { 184 | return reject(new Error(errorMessage)); 185 | } 186 | client.sadd(key, value, (err, numInserted) => { 187 | if (err) { 188 | reject(err); 189 | } else { 190 | resolve(numInserted); 191 | } 192 | }); 193 | }); 194 | } 195 | 196 | export function sinter( 197 | client: RedisClient | Multi, 198 | keyArrayOrString: string | Array, 199 | ...intersectKeys: Array 200 | ): Promise> { 201 | return new Promise>((resolve, reject) => { 202 | if (!client.sinter) { 203 | return reject(new Error(errorMessage)); 204 | } 205 | client.sinter.apply(client, [ 206 | keyArrayOrString, 207 | ...intersectKeys, 208 | (err: Error | null, values: Array) => { 209 | if (err) { 210 | reject(err); 211 | } else { 212 | resolve(values); 213 | } 214 | }, 215 | ]); 216 | }); 217 | } 218 | 219 | export function hgetall( 220 | client: RedisClient | Multi, 221 | key: string, 222 | ): Promise<{ [key: string]: string }> { 223 | return new Promise<{ [key: string]: string }>((resolve, reject) => { 224 | if (!client.hgetall) { 225 | return reject(new Error(errorMessage)); 226 | } 227 | client.hgetall(key, (err, values) => { 228 | if (err) { 229 | reject(err); 230 | } else { 231 | resolve(values); 232 | } 233 | }); 234 | }); 235 | } 236 | 237 | export function exec(client: Multi): Promise> { 238 | return new Promise>((resolve, reject) => { 239 | if (!client.exec) { 240 | return reject(new Error(errorMessage)); 241 | } 242 | client.exec((err, results) => { 243 | if (err) { 244 | return reject(err); 245 | } else { 246 | // detect if it's ioredis, which has a different return structure. 247 | // better methods for doing this would be very welcome! 248 | if ( 249 | Array.isArray(results[0]) && 250 | (results[0][0] === null || 251 | // once ioredis has proper typings, this any casting can be changed 252 | results[0][0] instanceof (IORedis as any).ReplyError) 253 | ) { 254 | // transform ioredis format to node_redis format 255 | results = results.map((result: Array) => { 256 | const error = result[0]; 257 | if (error instanceof (IORedis as any).ReplyError) { 258 | return error.message; 259 | } 260 | return result[1]; 261 | }); 262 | } 263 | resolve(results); 264 | } 265 | }); 266 | }); 267 | } 268 | 269 | export function psubscribe( 270 | client: RedisClient, 271 | patternOrPatternArray: string | Array, 272 | ...patterns: Array 273 | ): Promise { 274 | return new Promise((resolve, reject) => { 275 | if (!client.psubscribe) { 276 | return reject(new Error(errorMessage)); 277 | } 278 | client.psubscribe.apply(client, [ 279 | patternOrPatternArray, 280 | ...patterns, 281 | (err: Error | null) => { 282 | if (err) { 283 | reject(err); 284 | } else { 285 | resolve(); 286 | } 287 | }, 288 | ]); 289 | }); 290 | } 291 | 292 | export function punsubscribe( 293 | client: RedisClient, 294 | patternOrPatternArray: string | Array, 295 | ...patterns: Array 296 | ): Promise { 297 | return new Promise((resolve, reject) => { 298 | if (!client.punsubscribe) { 299 | return reject(new Error(errorMessage)); 300 | } 301 | client.punsubscribe.apply(client, [ 302 | patternOrPatternArray, 303 | ...patterns, 304 | (err: Error | null) => { 305 | if (err) { 306 | reject(err); 307 | } else { 308 | resolve(); 309 | } 310 | }, 311 | ]); 312 | }); 313 | } 314 | 315 | export function keys( 316 | client: RedisClient | Multi, 317 | searchString: string, 318 | ): Promise> { 319 | return new Promise>((resolve, reject) => { 320 | if (!client.keys) { 321 | return reject(new Error(errorMessage)); 322 | } 323 | client.keys(searchString, (err, value) => { 324 | if (err) { 325 | reject(err); 326 | } else { 327 | resolve(value); 328 | } 329 | }); 330 | }); 331 | } 332 | 333 | export function zscore( 334 | client: RedisClient | Multi, 335 | key: string, 336 | member: string, 337 | ): Promise { 338 | return new Promise((resolve, reject) => { 339 | if (!client.zscore) { 340 | return reject(new Error(errorMessage)); 341 | } 342 | client.zscore(key, member, (err, value) => { 343 | if (err) { 344 | reject(err); 345 | } else { 346 | if (value === null) { 347 | resolve(null); 348 | } else { 349 | resolve(parseFloat(value)); 350 | } 351 | } 352 | }); 353 | }); 354 | } 355 | 356 | export function hset( 357 | client: RedisClient | Multi, 358 | key: string, 359 | field: string, 360 | value: string, 361 | ): Promise { 362 | return new Promise((resolve, reject) => { 363 | if (!client.hset) { 364 | return reject(new Error(errorMessage)); 365 | } 366 | client.hset(key, field, value, (err, numAdded) => { 367 | if (err) { 368 | reject(err); 369 | } else { 370 | resolve(numAdded); 371 | } 372 | }); 373 | }); 374 | } 375 | 376 | export function hget( 377 | client: RedisClient | Multi, 378 | key: string, 379 | field: string, 380 | ): Promise { 381 | return new Promise((resolve, reject) => { 382 | if (!client.hget) { 383 | return reject(new Error(errorMessage)); 384 | } 385 | client.hget(key, field, (err, value) => { 386 | if (err) { 387 | reject(err); 388 | } else { 389 | resolve(value); 390 | } 391 | }); 392 | }); 393 | } 394 | -------------------------------------------------------------------------------- /ts/validators.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { validatiorFunction } from './model.header'; 4 | 5 | export const universalValidatorPath = path.join( 6 | __dirname, 7 | '..', 8 | 'ts', 9 | 'universalValidators.js', 10 | ); 11 | // tslint:disable-next-line:no-var-requires 12 | const newRawValidators = require(universalValidatorPath); 13 | 14 | /** 15 | * @namespace Validators 16 | */ 17 | export const validators: { 18 | [index: string]: validatiorFunction; 19 | } = 20 | newRawValidators.validators; 21 | 22 | exports.regexps = newRawValidators.regexps; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "experimentalDecorators": true, 7 | "noImplicitAny": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "noUnusedLocals": true, 12 | "strictNullChecks": true, 13 | "noUnusedParameters": true, 14 | "lib": ["es7"], 15 | "outDir": "./tsOut", 16 | "sourceMap": true, 17 | "keyofStringsOnly": true, 18 | "declarationMap": true, 19 | "skipLibCheck": true 20 | }, 21 | "files": ["./ts/index.ts"], 22 | "exclude": ["./lib/*.js"] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | /* 5 | * Any rules specified here will override those from the base config we are extending 6 | */ 7 | "variable-name": [ 8 | true, 9 | "ban-keywords", 10 | "check-format", 11 | "allow-leading-underscore" 12 | ], 13 | "quotemark": [true, "avoid-escape", "single"], 14 | "ordered-imports": [false], 15 | "member-ordering": [false], 16 | "no-trailing-whitespace": true, 17 | "no-consecutive-blank-lines": [false], 18 | "no-console": [false], 19 | "array-type": [true, "generic"], 20 | "prefer-conditional-expression": false, 21 | "trailing-comma": [ 22 | true, 23 | { "multiline": "always", "singleline": "never", "esSpecCompliant": true } 24 | ], 25 | "object-literal-key-quotes": [true, "as-needed"] 26 | } 27 | } 28 | --------------------------------------------------------------------------------