UNPKG

15.6 kBMarkdownView Raw
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
11import Not from 'you-are-not' // ES import syntax
12const Not = require('you-are-not') // CJS require syntax
13
14let schema = { id: "number" } // endpoint only expects param "id"
15let malicious = { id: 1, role: "admin" } //payload with malicious "role: admin"
16
17let sanitised = Not.scrub(
18 "objectName",
19 schema
20 payload
21)
22console.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```
38npm install --save you-are-not
39```
40
41```ts
42import Not from 'you-are-not'
43```
44
45## Simple Usage
46
47### 1. For API input type-checking, validation, sanitisation and error messaging:
48
49User makes a request with the following payload:
50```js
51const payload = {
52 id: 1,
53 name: 2 // error made by requestor
54}
55```
56
57API receiving payload defines a schema, followed by scrubbing the payload:
58```js
59const schema = {
60 id: 'number',
61 name: 'string' // note that name is expected to be in `string`
62}
63
64let 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```
73TypeError (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```
82If you are using express or fastify, thrown errors can be seamlessly used for production:
83```js
84//express
85res.status(sanitised.statusCode)
86res.send({
87 message: `You have provided erroneous inputs. \n\nMore info:\n${sanitised.trace.join('\n')}`
88})
89
90//fastify
91reply.code(sanitised.statusCode)
92reply.send({
93 message: `You have provided erroneous inputs. \n\nMore info:\n${sanitised.trace.join('\n')}`
94})
95```
96This will produce a `400` error with the following `message` property in response body:
97```
98You have provided erroneous inputs.
99
100More info:
101Wrong Type (payloadWithTypeError.id): Expecting type `number` but got `string` with value of `1`.
102```
103
104Suppose additional properties are provided in possibly malicious payloads, they can be sanitised:
105```js
106let payloadWithMaliciousPayload = {
107 id: 1,
108 name: "foo",
109 role: "admin" // simulating malicious payload. this will be sanitised
110}
111
112var sanitised = Not.scrub(
113 'payloadWithMaliciousPayload',
114 schema,
115 payloadWithTypeError
116)
117
118console.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
129Besides being a payload sanitiser, *Not* is a type-checker under-the-hood.
130
131```js
132import NotProto from 'you-are-not'
133const Not = Not.create() // this creates another instance of Not
134const not = Not.createNot() // this exposes a simplified #not with no overloads
135const is = Not.createIs()
136const notNerfed = Not.create({ throw: false }) // creates an instance that will not throw errors.
137```
138
139Use *Not* to cut down runtime type-checking verbiage. Instead of:
140
141```js
142if (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
149You write:
150
151```js
152not(['string', 'number', 'array'], foo)
153// or
154is(['string', 'number', 'array'], foo)
155
156// code will reach here if the above don't error
157startMyFunction()
158```
159When *Not* fails, **it throws an error by default**. You can pass `throw: false` to prevent throwing errors and handle them yourself:
160
161```js
162const not = Not.createNot({ throw: false })
163// instead of throwing, `not` will return string
164
165let input = ['a', 'sentence']
166let result = not('string', input) // returns a string, which can evaluate `true`
167
168if (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 :)
172input.toLowerCase()
173```
174
175## Full Usage
176
177### 1. Valid types
178
179**The valid types you can check for are:**
180
181```js
182Primitives:
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
194Aggregated:
195'optional' // which means 'null' and 'undefined'
196
197Other 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
205Not.scrub(objectName, schema, payload, options)
206
207Not.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
222Not.checkObject(objectName, schema, payload, (errors, payload) => { /* handle errors yourself*/ })
223
224// options
225Not.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
242info__optional: {
243 gender: 'string',
244 age__optional: 'number'
245}
246//is same as
247info__optional: {
248 gender: 'string',
249 age: ['number', 'optional']
250}
251```
252
253Check for multiple type by passing an array:
254
255```js
256info: {
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
2631. If `callback/options` is a `callback` function, it will run the `callback`:
264
265```js
266Not.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
2732. 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
276let sanitised = Not.checkObject(
277 name,
278 schema,
279 payload,
280 { returnPayload: true }
281)
282if (Array.isArray(sanitised) {
283 // do something with the errors
284 return
285}
286// or continue using the sanitised payload.
287DB.find(sanitised)
288```
289
2903. If `callback/options` is `{ callback: function() {}, returnPayload: true }`:
291
292```js
293let 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
302Not.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
314You 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
316let not = Not.create()
317let id = "123"
318let anotherId = 123
319let emailOptional = undefined
320
321not(['string', 'number'], id)
322not(['string', 'number'], anotherId)
323not(['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
333Not.scrub(objectName, schema, payload)
334Not.checkObject(objectName, schema, payload, options)
335
336Not.not(expect, got, name, note)
337Not.is(expect, got, name, note)
338
339Not.lodge(expect, got, name, note)
340Not.resolve([callback]) // this is used with #lodge.
341
342Not.defineType(options)
343```
344
345### 5. Methods: `#not` and `#is`
346```js
347Not.not(expect, got, name, note)
348Not.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:**
3591. If passed: `false`.
3602. 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
368Not.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
378let schema = { age: 'integer' }
379
380Not.scrub('name', schema, {
381 age: 22.4 // this will fail
382})
383Not.not('integer', 4.4) // gives error message
384Not.is('integer', 4.4) // returns false
385
386```
387#### Advanced example
388
389Having trouble with empty `[]` or `{}` that sometimes is `false` or `null` or `undefined`?
390Define a "falsey" type like this:
391
392```js
393let is = Not.createIs({ throw: false })
394Not.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
407Not.not('falsey', {}) // returns false
408Not.not('falsey', [null]) // returns error message
409Not.is('falsey', []) // returns true
410Not.is('falsey', undefined) // returns true
411Not.is(['falsey', 'function'], function() {}) // returns true
412```
413
414### 7. Methods: `#lodge` and `#resolve`
415
416You can also use `#lodge` and `#resolve` to bulk checking with more control:
417
418```js
419// create a descendant
420let apiNot = Object.create(Not)
421// or
422let apiNot = Not.create()
423
424apiNot.lodge('string', request.name, 'name')
425apiNot.lodge('boolean', request.subscribe, 'subscribe')
426apiNot.lodge(['string', 'array'], request.friends, 'friends')
427apiNot.lodge(['number', 'string'], request.age, 'age')
428// and many more lines
429
430apiNot.resolve()
431/* OR */
432apiNot.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
443let not = Not.create({
444 verbose: true,
445 throw: false
446})
447not('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
464typeof [] // object
465typeof null // object
466typeof NaN // number
467```
468Those are technically not wrong (or debatable), but often gets in the way.
469
470**By default, *Not* will apply the following treatment:**
4711. `NaN` is not a **'number'**, and will be **'nan'**.
4722. `Array` and `[]` are of **'array'** type, and not **'object'**.
4733. `null` is **'null'** and not an **'object'**.
474
475**Switch Off *Not*'s Opinions:**
476
477You can switch off opinionated type-checking:
478```js
479let not = Not.createNot({ isOpinionated: false })
480```
481When 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
483not('object', []) // returns false -- `[]` is an object
484not('array', []) // returns false -- `[]` is an array
485not('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
490let NotWithPartialOpinions = Not.createIs({
491 opinionatedOnNaN: false,
492 opinionatedOnArray: false,
493 opinionatedOnNull: false
494})
495
496// or mutate the object before instantiating.
497let NotWithPartialOpinions = Object.create(Not)
498Object.assign(NotWithPartialOptions, {
499 opinionatedOnNaN: false,
500 opinionatedOnArray: false,
501 opinionatedOnNull: false
502})
503let not = NotWithPartialOpinions.create()
504let is = NotWithPartialOpinions.createIs()
505```
506
507## More Advanced Usage
508### Customise your message, by replacing the #msg method
509You have to mutate the prototype:
510```js
511import Not from 'you-are-not'
512const CustomNot = Not.create()
513
514//overwrite the msg function with your own
515CustomNot.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
525let customNot = CustomNot.create()
526global.isDeveloperMode = true
527
528let sanitised = customNot.scrub('someWrongInput', {
529 someValue: 'string' // schema
530}, {
531 someValue: []
532})
533
534// or if using just the type checker:
535customNot('string', [], 'someWrongInput', 'file.js - xxx function')
536```
537Will give error:
538```
539Hey 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.