Utils
Collection of reusable scripts used by AdonisJS core team
This module exports a collection of re-usable utilties to avoid re-writing the same code in every other package. We also include a handful of Lodash utilities, which are used across the AdonisJS packages eco-system.
Table of contents
Installation
Install the package from npm registry as follows:
npm i @poppinss/utils
yarn add @poppinss/utils
and then use it as follows:
import { requireAll } from '@poppinss/utils'
requireAll(__dirname)
Exception
A custom exception class that extends the Error
class to add support for defining status
and error codes
.
import { Exception } from '@poppinss/utils'
throw new Exception('Something went wrong', 500, 'E_RUNTIME_EXCEPTION')
throw new Exception('Route not found', 404, 'E_ROUTE_NOT_FOUND')
fsReadAll
A utility to recursively read all script files for a given directory. This method is equivalent to
readdir + recursive + filter (.js, .json, .ts)
.
import { fsReadAll } from '@poppinss/utils'
const files = fsReadAll(__dirname)
You can also define your custom filter function. The filter function must return true
for files to be included.
const files = fsReadAll(__dirname, (file) => {
return file.endsWith('.foo.js')
})
requireAll
Same as fsReadAll
, but instead require the files. Helpful when you want to load all the config files inside a directory on app boot.
import { requireAll } from '@poppinss/utils'
const config = requireAll(join(__dirname, 'config'))
{
file1: {},
file2: {}
}
esmRequire
Utility to require script files wihtout worrying about CommonJs
and ESM
exports. This is how it works.
- Returns the exported value for
module.exports
. - Returns the default value is an ESM module has
export default
. - Returns all exports if is an ESM module and doesn't have
export default
.
foo.js
module.exports = {
greeting: 'Hello world',
}
foo.default.js
export default {
greeting: 'Hello world',
}
foo.esm.js
export const greeting = {
greeting: 'hello world',
}
import { esmRequire } from '@poppinss/utils'
esmRequire('./foo.js')
esmRequire('./foo.default.js')
esmRequire('./foo.esm.js')
esmResolver
The esmResolver
method works similar to esmRequire
. However, instead of requiring the file, it accepts the object and returns the exported as per the same logic defined above.
import { esmRequire } from '@poppinss/utils'
esmResolver({ greeting: 'hello world' })
esmResolver({
default: { greeting: 'hello world' },
__esModule: true,
})
esmResolver({
greeting: { greeting: 'hello world' },
__esModule: true,
})
resolveFrom
Works similar to require.resolve
, however it handles the absolute paths properly.
import { resolveFrom } from '@poppinss/utils'
resolveFrom(__dirname, 'npm-package')
resolveFrom(__dirname, './foo.js')
resolveFrom(__dirname, join(__dirname, './foo.js'))
resolveDir
The require.resolve
or resolveFrom
method can only resolve paths to a given file and not the directory. For example: If you pass path to a directory, then it will search for index.js
inside it and in case of a package, it will be search for main
entry point.
On the other hand, the resolveDir
method can also resolve path to directories using following resolution.
- Absolute paths are returned as it is.
- Relative paths starting with
./
or .\
are resolved using path.join
. - Path to packages inside
node_modules
are resolved as follows: - Uses require.resolve
to resolve the package.json
file. - Then replace the package-name
with the absolute resolved package path.
import { resolveDir } from '@poppinss/utils'
resolveDir(__dirname, './database/migrations')
resolveDir(__dirname, 'some-package/database/migrations')
resolveDir(__dirname, '@some/package/database/migrations')
interpolate
A small utility function to interpolate values inside a string.
import { interpolate } from '@poppinss/utils'
interpolate('hello {{ username }}', { username: 'virk' })
interpolate('hello {{ users.0.username }}', { users: [{ username: 'virk' }] })
If value is missing, it will be replaced with an undefined
string.
Lodash utilities
Lodash itself is a bulky library and most of the times, we don't need all the functions from it. For this purpose, the lodash team decided to publish individual methods to npm as packages. However, most of those individual packages are outdated.
At this point, whether we should use the complete lodash build or use outdated individual packages. Both are not acceptable.
Instead, we make use of lodash-cli
to create a custom build of all the utilities we ever need inside the AdonisJS eco-system and export it as part of this package. Why part of this package?
Well, creating custom builds in multiple packages will cause friction, so it's better to keep it at a single place.
Do note: There are no Typescript types for the lodash methods, since their CLI doesn't generate one and the one published on @types/lodash
package are again maintained by community and not the lodash core team, so at times, they can also be outdated.
import { lodash } from '@poppinss/utils'
lodash.snakeCase('HelloWorld')
Exported methods
Following is the list of exported helpers.
Base 64 Encode/Decode
Following helpers for base64 encoding/decoding also exists.
encode
import { base64 } from '@poppinss/utils'
base64.encode('hello world')
base64.encode(Buffer.from('hello world', 'binary'))
decode
import { base64 } from '@poppinss/utils'
base64.decode(base64.encode('hello world'))
base64.decode(base64.encode(Buffer.from('hello world', 'binary')), 'binary')
urlEncode
Same as encode
, but safe for URLS and Filenames
urlDecode
Same as decode
, but decodes the urlEncode
output values
Random String
A helper to generate random strings of a given length. Uses crypto
under the hood.
import { randomString } from '@poppinss/utils'
randomString(32)
randomString(128)
Safe equal
Compares two values by avoid timing attack. Accepts any input that can be passed to Buffer.from
import { safeValue } from '@poppinss/utils'
if (safeValue('foo', 'foo')) {
}
Safe stringify
Similar to JSON.stringify
, but also handles Circular references by removing them.
import { safeStringify } from '@poppinss/utils'
const o = { b: 1, a: 0 }
o.o = o
console.log(safeStringify(o))
console.log(JSON.stringify(o))
Safe parse
Similar to JSON.parse
, but protects against Prototype Poisoning
import { safeParse } from '@poppinss/utils'
const input = '{ "user": { "__proto__": { "isAdmin": true } } }'
JSON.parse(input)
safeParse(input)
Message Builder
Message builder provides a sane API for stringifying objects similar to JSON.stringify
but has a few advantages.
- It is safe from JSON poisoning vulnerability.
- You can define expiry and purpose for the encoding. The
verify
method will respect these values.
The message builder alone may seem useless, since anyone can decode the object and change its expiry or purpose. However, you can generate an hash of the stringified object and verify for tampering by validating the hash. This is what AdonisJS does for cookies.
import { MessageBuilder } from '@poppinss/utils'
const builder = new MessageBuilder()
const encoded = builder.build({ username: 'virk' }, '1 hour', 'login')
Now verify it
builder.verify(encoded)
builder.verify(encoded, 'register')
builder.verify(encoded, 'login')
defineStaticProperty
Explicitly define static properties on a class by checking for hasOwnProperty
. In case of inheritance, the properties from the parent class are cloned vs following the prototypal inheritance.
We use/need this copy from parent class behavior a lot in AdonisJS. Here's an example of Lucid models
You create an application wide base model
class AppModel extends BaseModel {
@column.datetime()
public createdAt: DateTime
}
AdonisJS will create the $columnDefinitions
property on the AppModel
class, that holds all the columns
AppModel.$columnDefinitions
Now, lets create another model inheriting the AppModel
class User extends AppModel {
@column()
public id: number
}
As per the Javascript prototypal inheritance. The User
model will not contain the columns from the AppModel
, because we just re-defined the $columnDefinitions
property. However, we don't want this behavior and instead want to copy the columns from the AppModel
and then add new columns to it.
Voila! Use the defineStaticProperty
helper from this class.
class LucidBaseModel {
static boot() {
defineStaticProperty(this, LucidBaseModel, {
propertyName: '$columnDefinitions',
defaultValue: {},
strategy: 'inherit',
})
}
}
The defineStaticProperty
takes a total of three arguments.
- The first argument is always
this
. - The second argument is the root level base class. This will usually be the class exported by your package or module.
- The third argument takes the
propertyName
, defaultValue (in case, there is nothing to copy)
, and the strategy
. - The
inherit
strategy will copy the properties from the base class. - The
define
strategy will always use the defaultValue
to define the property on the class. In other words, there is no copy behavior, but prototypal inheritance chain is also breaked by explicitly re-defining the property.