1 | ![Not.Js - "All-in-one" type checking, validation, error handling and messaging.](https://user-images.githubusercontent.com/6825277/132091763-bad840b2-1b33-479d-be49-63e13aa11b24.png)
|
2 | [![npm version](https://img.shields.io/npm/v/you-are-not.svg?style=flat-square)](https://www.npmjs.com/package/you-are-not)
|
3 | [![Build Status](https://badgen.net/travis/calvintwr/you-are-not?style=flat-square)](https://travis-ci.com/calvintwr/you-are-not)
|
4 | [![Coverage Status](https://badgen.net/coveralls/c/github/calvintwr/you-are-not?style=flat-square)](https://coveralls.io/r/calvintwr/you-are-not)
|
5 | [![license](https://img.shields.io/npm/l/you-are-not.svg?style=flat-square)](https://www.npmjs.com/package/you-are-not)
|
6 | [![install size](https://badgen.net/packagephobia/install/you-are-not?style=flat-square)](https://packagephobia.now.sh/result?p=you-are-not)
|
7 |
|
8 | >*Not* is the minimal and blazingly fast "implement-and-forget" runtime type-checking library written in TypeScript for instant API payload checking and sanitisation, with ready-to-use error response messages to your API requestors -- all in a small and neat pack.
|
9 |
|
10 | ```ts
|
11 | import Not from 'you-are-not' // ES import syntax
|
12 | const Not = require('you-are-not') // CJS require syntax
|
13 |
|
14 | let schema = { id: "number" } // endpoint only expects param "id"
|
15 | let malicious = { id: 1, role: "admin" } //payload with malicious "role: admin"
|
16 |
|
17 | let sanitised = Not.scrub(
|
18 | "objectName",
|
19 | schema
|
20 | payload
|
21 | )
|
22 | console.log(sanitised)
|
23 | // outputs:
|
24 | // { id: 1 }
|
25 | ```
|
26 | `role: "admin"` is removed. Payload sanitised.
|
27 |
|
28 | ## Why *Not*?
|
29 | *Not* gives **actionable** error messages, so you know exactly what has gone wrong with your inputs/arguments/API. Use the messages directly as API replies. Build friendly APIs. Meet project deadlines.
|
30 |
|
31 | ![Not.TS - "All-in-one" type checking, validation, error handling and messaging.](https://dev-to-uploads.s3.amazonaws.com/i/l74jrtmfy2p305fw9wvy.gif)
|
32 |
|
33 | *This module has no dependencies.*
|
34 |
|
35 | ## Installation
|
36 |
|
37 | ```
|
38 | npm install --save you-are-not
|
39 | ```
|
40 |
|
41 | ```ts
|
42 | import Not from 'you-are-not'
|
43 | ```
|
44 |
|
45 | ## Simple Usage
|
46 |
|
47 | ### 1. For API input type-checking, validation, sanitisation and error messaging:
|
48 |
|
49 | User makes a request with the following payload:
|
50 | ```js
|
51 | const payload = {
|
52 | id: 1,
|
53 | name: 2 // error made by requestor
|
54 | }
|
55 | ```
|
56 |
|
57 | API receiving payload defines a schema, followed by scrubbing the payload:
|
58 | ```js
|
59 | const schema = {
|
60 | id: 'number',
|
61 | name: 'string' // note that name is expected to be in `string`
|
62 | }
|
63 |
|
64 | let sanitised = Not.scrub(
|
65 | 'payloadWithTypeError', // give your payload a name
|
66 | schema,
|
67 | payload,
|
68 | { exact: true } // use exact: true if you need the payload to match the schema 100%, else, additional properties will be removed without throwing errors.
|
69 | )
|
70 | ```
|
71 | **Not throws an actionable error message ready for sending back to the requestor:**
|
72 | ```
|
73 | TypeError (NotTS): Wrong types provided. See `trace`.
|
74 | ... stack trace ...
|
75 | {
|
76 | statusCode: 400,
|
77 | trace: [
|
78 | 'Wrong Type (payloadWithTypeError.id): Expecting type `number` but got `string` with value of `1`.'
|
79 | ]
|
80 | }
|
81 | ```
|
82 | If you are using express or fastify, thrown errors can be seamlessly used for production:
|
83 | ```js
|
84 | //express
|
85 | res.status(sanitised.statusCode)
|
86 | res.send({
|
87 | message: `You have provided erroneous inputs. \n\nMore info:\n${sanitised.trace.join('\n')}`
|
88 | })
|
89 |
|
90 | //fastify
|
91 | reply.code(sanitised.statusCode)
|
92 | reply.send({
|
93 | message: `You have provided erroneous inputs. \n\nMore info:\n${sanitised.trace.join('\n')}`
|
94 | })
|
95 | ```
|
96 | This will produce a `400` error with the following `message` property in response body:
|
97 | ```
|
98 | You have provided erroneous inputs.
|
99 |
|
100 | More info:
|
101 | Wrong Type (payloadWithTypeError.id): Expecting type `number` but got `string` with value of `1`.
|
102 | ```
|
103 |
|
104 | Suppose additional properties are provided in possibly malicious payloads, they can be sanitised:
|
105 | ```js
|
106 | let payloadWithMaliciousPayload = {
|
107 | id: 1,
|
108 | name: "foo",
|
109 | role: "admin" // simulating malicious payload. this will be sanitised
|
110 | }
|
111 |
|
112 | var sanitised = Not.scrub(
|
113 | 'payloadWithMaliciousPayload',
|
114 | schema,
|
115 | payloadWithTypeError
|
116 | )
|
117 |
|
118 | console.log(sanitised)
|
119 | // outputs:
|
120 | // {
|
121 | // id: 1,
|
122 | // name: "foo"
|
123 | // }
|
124 | ```
|
125 | `role: "admin"` is removed. Payload sanitised.
|
126 |
|
127 | ### 2. Lightweight type-checking
|
128 |
|
129 | Besides being a payload sanitiser, *Not* is a type-checker under-the-hood.
|
130 |
|
131 | ```js
|
132 | import NotProto from 'you-are-not'
|
133 | const Not = Not.create() // this creates another instance of Not
|
134 | const not = Not.createNot() // this exposes a simplified #not with no overloads
|
135 | const is = Not.createIs()
|
136 | const notNerfed = Not.create({ throw: false }) // creates an instance that will not throw errors.
|
137 | ```
|
138 |
|
139 | Use *Not* to cut down runtime type-checking verbiage. Instead of:
|
140 |
|
141 | ```js
|
142 | if (typeof foo !== 'string' ||
|
143 | typeof foo !== 'number' ||
|
144 | (typeof foo === 'number' && !isNaN(foo)) ||
|
145 | !Array.isArray(foo)
|
146 | ) { throw Error("Not valid, but I don't know why.") }
|
147 | ```
|
148 |
|
149 | You write:
|
150 |
|
151 | ```js
|
152 | not(['string', 'number', 'array'], foo)
|
153 | // or
|
154 | is(['string', 'number', 'array'], foo)
|
155 |
|
156 | // code will reach here if the above don't error
|
157 | startMyFunction()
|
158 | ```
|
159 | When *Not* fails, **it throws an error by default**. You can pass `throw: false` to prevent throwing errors and handle them yourself:
|
160 |
|
161 | ```js
|
162 | const not = Not.createNot({ throw: false })
|
163 | // instead of throwing, `not` will return string
|
164 |
|
165 | let input = ['a', 'sentence']
|
166 | let result = not('string', input) // returns a string, which can evaluate `true`
|
167 |
|
168 | if (result) input = input.join(' ')
|
169 | // so you can do your own error handling, or transformation
|
170 |
|
171 | // code below can safely use `input` as string :)
|
172 | input.toLowerCase()
|
173 | ```
|
174 |
|
175 | ## Full Usage
|
176 |
|
177 | ### 1. Valid types
|
178 |
|
179 | **The valid types you can check for are:**
|
180 |
|
181 | ```js
|
182 | Primitives:
|
183 | 'string'
|
184 | 'number'
|
185 | 'array'
|
186 | 'object'
|
187 | 'function'
|
188 | 'boolean'
|
189 | 'null'
|
190 | 'undefined'
|
191 | 'symbol'
|
192 | 'nan' // this is an opinion. NaN should not be of type number in the literal sense.
|
193 |
|
194 | Aggregated:
|
195 | 'optional' // which means 'null' and 'undefined'
|
196 |
|
197 | Other custom types:
|
198 | 'integer'
|
199 | ```
|
200 |
|
201 | ### 2. #scrub/#checkObject
|
202 | #checkObject is #scrub under the hood. Use #scrub for simplified usage (example above), and #checkObject when you want more control.
|
203 |
|
204 | ```js
|
205 | Not.scrub(objectName, schema, payload, options)
|
206 |
|
207 | Not.checkObject(objectName, schema, payload, callback/options)
|
208 | ```
|
209 |
|
210 | `objectName`: (string) Name of object.
|
211 |
|
212 | `schema`: (object) An object depicting your schema.
|
213 |
|
214 | `payload`: (object) The payload to check for.
|
215 |
|
216 | `options` (#scrub): (object | optional). Define `exact: true` if you want to throw an error if there are additional properties.
|
217 |
|
218 | `callback/options` (#checkObject): (object | optional). See example below:
|
219 |
|
220 | ```js
|
221 | // callback
|
222 | Not.checkObject(objectName, schema, payload, (errors, payload) => { /* handle errors yourself*/ })
|
223 |
|
224 | // options
|
225 | Not.checkObject(objectName, schema, payload, {
|
226 | callback: (errors, payload) => { /* handle errors yourself*/ },
|
227 | returnPayload: true/false, // define if you need the payload returned. if not requires, switch to false for better performance
|
228 | exact: true/false // if true, will throw errors if there are additiona properties
|
229 | })
|
230 | ```
|
231 |
|
232 |
|
233 | #### Defining Schema
|
234 |
|
235 | ```js
|
236 | // you can use optional notations like this:
|
237 | "info?": {
|
238 | gender: 'string',
|
239 | "age?": 'number'
|
240 | }
|
241 | //is same as
|
242 | info__optional: {
|
243 | gender: 'string',
|
244 | age__optional: 'number'
|
245 | }
|
246 | //is same as
|
247 | info__optional: {
|
248 | gender: 'string',
|
249 | age: ['number', 'optional']
|
250 | }
|
251 | ```
|
252 |
|
253 | Check for multiple type by passing an array:
|
254 |
|
255 | ```js
|
256 | info: {
|
257 | age: ['number', 'string'], // age can be of type number or string
|
258 | email: ['email'] // suppose you have created your own email validation checking. To create your own types, check examples below.
|
259 | }
|
260 | ```
|
261 |
|
262 | #### #checkObject advanced usage
|
263 | 1. If `callback/options` is a `callback` function, it will run the `callback`:
|
264 |
|
265 | ```js
|
266 | Not.checkObject(name, schema, payload, function(errors) {
|
267 | // do something with errors.
|
268 | })
|
269 | ```
|
270 |
|
271 | (Note: When callback is provided, Not assumes you want to handle things yourself, and will not throw errors regardless of the `throw` flag.)
|
272 |
|
273 | 2. If `callback/options` is `{ returnPayload: true }`, `#checkObject` returns (a) the sanitised payload (object) when check passes, or (b) an array of errors if check fails:
|
274 |
|
275 | ```js
|
276 | let sanitised = Not.checkObject(
|
277 | name,
|
278 | schema,
|
279 | payload,
|
280 | { returnPayload: true }
|
281 | )
|
282 | if (Array.isArray(sanitised) {
|
283 | // do something with the errors
|
284 | return
|
285 | }
|
286 | // or continue using the sanitised payload.
|
287 | DB.find(sanitised)
|
288 | ```
|
289 |
|
290 | 3. If `callback/options` is `{ callback: function() {}, returnPayload: true }`:
|
291 |
|
292 | ```js
|
293 | let callback = function(errors, payload) {
|
294 | if(errors.length > 0) {
|
295 | // do something with the errors
|
296 | return
|
297 | }
|
298 |
|
299 | DB.find(payload)
|
300 | }
|
301 |
|
302 | Not.checkObject(
|
303 | name,
|
304 | schema,
|
305 | payload,
|
306 | {
|
307 | returnPayload: true,
|
308 | callback: callback
|
309 | }
|
310 | )
|
311 | ```
|
312 |
|
313 | ### 3. *Not* as simple type checker
|
314 | You can also check for multiple types by passing an array. This is useful when you want your API to accept both string and number:
|
315 | ```js
|
316 | let not = Not.create()
|
317 | let id = "123"
|
318 | let anotherId = 123
|
319 | let emailOptional = undefined
|
320 |
|
321 | not(['string', 'number'], id)
|
322 | not(['string', 'number'], anotherId)
|
323 | not(['optional', 'string'], emailOptional)
|
324 |
|
325 | // code reaches this point when all checks passed
|
326 |
|
327 | ```
|
328 |
|
329 | ### 4. Methods Available
|
330 |
|
331 | **The *Not* prototype has the following methods available:**
|
332 | ```js
|
333 | Not.scrub(objectName, schema, payload)
|
334 | Not.checkObject(objectName, schema, payload, options)
|
335 |
|
336 | Not.not(expect, got, name, note)
|
337 | Not.is(expect, got, name, note)
|
338 |
|
339 | Not.lodge(expect, got, name, note)
|
340 | Not.resolve([callback]) // this is used with #lodge.
|
341 |
|
342 | Not.defineType(options)
|
343 | ```
|
344 |
|
345 | ### 5. Methods: `#not` and `#is`
|
346 | ```js
|
347 | Not.not(expect, got, name, note)
|
348 | Not.is(expect, got, name, note)
|
349 | ```
|
350 | `expect`: (string or array of strings) The types to check for (see below on "3. Types to check for".)
|
351 |
|
352 | `got`: (any) This is the the subject/candidate/payload you are checking.
|
353 |
|
354 | `name`: (string | optional) You can define a name of the subject/candidate/payload, which will be included in the error message.
|
355 |
|
356 | `note`: (string | optional) Any additional notes you wish to add to the error message.
|
357 |
|
358 | **Returns:**
|
359 | 1. If passed: `false`.
|
360 | 2. If failed: throws `TypeError` (default), `string` (if `willNotThrow: false`) or `POJO/JSON` (if `messageInPOJO: true`).
|
361 |
|
362 |
|
363 | ### 6. Methods: `#defineType`: Define your own checks
|
364 |
|
365 | #### Simple example
|
366 | *Not* has a built-in custom type called `integer`, and suppose if you were to define it yourself, it will look like this:
|
367 | ```js
|
368 | Not.defineType({
|
369 | primitive: 'number', // you must define your primitives
|
370 | type: 'integer', // name your test
|
371 | pass: function(candidate) {
|
372 | return candidate.toFixed(0) === candidate.toString()
|
373 | // or ES6:
|
374 | // return Number.isInteger(candidate)
|
375 | }
|
376 | })
|
377 |
|
378 | let schema = { age: 'integer' }
|
379 |
|
380 | Not.scrub('name', schema, {
|
381 | age: 22.4 // this will fail
|
382 | })
|
383 | Not.not('integer', 4.4) // gives error message
|
384 | Not.is('integer', 4.4) // returns false
|
385 |
|
386 | ```
|
387 | #### Advanced example
|
388 |
|
389 | Having trouble with empty `[]` or `{}` that sometimes is `false` or `null` or `undefined`?
|
390 | Define a "falsey" type like this:
|
391 |
|
392 | ```js
|
393 | let is = Not.createIs({ throw: false })
|
394 | Not.defineType({
|
395 | primitive: ['null', 'undefined', 'boolean', 'object', 'nan', 'array' ],
|
396 | type: 'falsey',
|
397 | pass: function(candidate) {
|
398 | if (is('object', candidate)) return Object.keys(candidate).length === 0
|
399 | if (is('array', candidate)) return candidate.length === 0
|
400 | if (is('boolean', candidate)) return candidate === false
|
401 | // its the other primitives null, undefined and nan
|
402 | // which is to be passed as falsey straight away without checking
|
403 | return true
|
404 | }
|
405 | })
|
406 |
|
407 | Not.not('falsey', {}) // returns false
|
408 | Not.not('falsey', [null]) // returns error message
|
409 | Not.is('falsey', []) // returns true
|
410 | Not.is('falsey', undefined) // returns true
|
411 | Not.is(['falsey', 'function'], function() {}) // returns true
|
412 | ```
|
413 |
|
414 | ### 7. Methods: `#lodge` and `#resolve`
|
415 |
|
416 | You can also use `#lodge` and `#resolve` to bulk checking with more control:
|
417 |
|
418 | ```js
|
419 | // create a descendant
|
420 | let apiNot = Object.create(Not)
|
421 | // or
|
422 | let apiNot = Not.create()
|
423 |
|
424 | apiNot.lodge('string', request.name, 'name')
|
425 | apiNot.lodge('boolean', request.subscribe, 'subscribe')
|
426 | apiNot.lodge(['string', 'array'], request.friends, 'friends')
|
427 | apiNot.lodge(['number', 'string'], request.age, 'age')
|
428 | // and many more lines
|
429 |
|
430 | apiNot.resolve()
|
431 | /* OR */
|
432 | apiNot.resolve(errors => {
|
433 | // optional callback, custom handling
|
434 | throw errors
|
435 | })
|
436 | ```
|
437 | (Note: This will not return any payload, since you intended to micro-manage.)
|
438 |
|
439 |
|
440 | ### 8. Verbose Output
|
441 | `verbose: true`
|
442 | ```js
|
443 | let not = Not.create({
|
444 | verbose: true,
|
445 | throw: false
|
446 | })
|
447 | not('array', { wrong: "stuff" }, 'payload', 'I screwed up.')
|
448 | //outputs:
|
449 | {
|
450 | message: 'Wrong Type (payload): Expect type `array` but got `object`: { wrong: "stuff" }. I screwed up.',
|
451 | expect: 'array',
|
452 | got: { wrong: "stuff" },
|
453 | gotType: 'object',
|
454 | name: 'payload',
|
455 | note: 'I screwed up.',
|
456 | timestamp: 167384950
|
457 | }
|
458 | ```
|
459 |
|
460 |
|
461 | ## Info: *Not*'s Type-Checking Logic ("Opinions")
|
462 | **Native Javscript typing has a few quirks:**
|
463 | ```js
|
464 | typeof [] // object
|
465 | typeof null // object
|
466 | typeof NaN // number
|
467 | ```
|
468 | Those are technically not wrong (or debatable), but often gets in the way.
|
469 |
|
470 | **By default, *Not* will apply the following treatment:**
|
471 | 1. `NaN` is not a **'number'**, and will be **'nan'**.
|
472 | 2. `Array` and `[]` are of **'array'** type, and not **'object'**.
|
473 | 3. `null` is **'null'** and not an **'object'**.
|
474 |
|
475 | **Switch Off *Not*'s Opinions:**
|
476 |
|
477 | You can switch off opinionated type-checking:
|
478 | ```js
|
479 | let not = Not.createNot({ isOpinionated: false })
|
480 | ```
|
481 | When false, all of the Javascript quirks will be restored, on top of *Not*'s opinions: An `Array` will both be an **'array'** as well as **'object'**, and `null` will both be **'null'** and **'object'**:
|
482 | ```js
|
483 | not('object', []) // returns false -- `[]` is an object
|
484 | not('array', []) // returns false -- `[]` is an array
|
485 | not('object', null) // returns false -- `null` is an object
|
486 | ```
|
487 | **Switch Off Opinions Partially:**
|
488 | ```js
|
489 | // both #createIs and #create can take in the same options
|
490 | let NotWithPartialOpinions = Not.createIs({
|
491 | opinionatedOnNaN: false,
|
492 | opinionatedOnArray: false,
|
493 | opinionatedOnNull: false
|
494 | })
|
495 |
|
496 | // or mutate the object before instantiating.
|
497 | let NotWithPartialOpinions = Object.create(Not)
|
498 | Object.assign(NotWithPartialOptions, {
|
499 | opinionatedOnNaN: false,
|
500 | opinionatedOnArray: false,
|
501 | opinionatedOnNull: false
|
502 | })
|
503 | let not = NotWithPartialOpinions.create()
|
504 | let is = NotWithPartialOpinions.createIs()
|
505 | ```
|
506 |
|
507 | ## More Advanced Usage
|
508 | ### Customise your message, by replacing the #msg method
|
509 | You have to mutate the prototype:
|
510 | ```js
|
511 | import Not from 'you-are-not'
|
512 | const CustomNot = Not.create()
|
513 |
|
514 | //overwrite the msg function with your own
|
515 | CustomNot.msg = function(expect, got, gotType, name, note) {
|
516 | let msg = 'Hey there! We are sorry that something broke, please try again!'
|
517 | let hint = ` [Hint: (${name}) expect ${expect} got ${gotType} at ${note}.]`
|
518 |
|
519 | // return different messages depending on environment
|
520 | return global.isDeveloperMode ? msg += hint : msg
|
521 | }
|
522 | ```
|
523 |
|
524 | ```js
|
525 | let customNot = CustomNot.create()
|
526 | global.isDeveloperMode = true
|
527 |
|
528 | let sanitised = customNot.scrub('someWrongInput', {
|
529 | someValue: 'string' // schema
|
530 | }, {
|
531 | someValue: []
|
532 | })
|
533 |
|
534 | // or if using just the type checker:
|
535 | customNot('string', [], 'someWrongInput', 'file.js - xxx function')
|
536 | ```
|
537 | Will give error:
|
538 | ```
|
539 | Hey there! We are sorry that something broke, please try again! [Hint: (someWrongInput) expect string got array at file.js - xx function. ]
|
540 | ```
|
541 |
|
542 | ## License
|
543 |
|
544 | *Not* is MIT licensed.
|