Skip to content

Commit

Permalink
Merge pull request #139 from AthennaIO/develop
Browse files Browse the repository at this point in the history
feat(ulid): add support to ulid
  • Loading branch information
jlenon7 authored Dec 29, 2024
2 parents 22fb481 + 6d2a9d0 commit 029491f
Show file tree
Hide file tree
Showing 9 changed files with 1,382 additions and 2,118 deletions.
3,236 changes: 1,133 additions & 2,103 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 13 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@athenna/common",
"version": "5.1.0",
"version": "5.2.0",
"description": "The Athenna common helpers to use in any Node.js ESM project.",
"license": "MIT",
"author": "João Lenon <lenon@athenna.io>",
Expand Down Expand Up @@ -77,17 +77,17 @@
"@fastify/formbody": "^7.4.0",
"bytes": "^3.1.2",
"callsite": "^1.0.0",
"chalk": "^5.3.0",
"chalk": "^5.4.1",
"change-case": "^4.1.2",
"collect.js": "^4.36.1",
"csv-parser": "^3.0.0",
"csv-parser": "^3.1.0",
"execa": "^8.0.1",
"fastify": "^4.28.1",
"fastify": "^4.29.0",
"got": "^12.6.1",
"http-status-codes": "^2.3.0",
"is-wsl": "^2.2.0",
"js-yaml": "^4.1.0",
"json-2-csv": "^5.5.5",
"json-2-csv": "^5.5.7",
"kind-of": "^6.0.3",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
Expand All @@ -96,30 +96,31 @@
"parent-module": "^3.1.0",
"pluralize": "^8.0.0",
"prepend-file": "^2.0.1",
"ulid": "^2.3.0",
"uuid": "^8.3.2",
"validator-brazil": "^1.2.2",
"youch": "^3.3.3",
"youch": "^3.3.4",
"youch-terminal": "^2.2.3"
},
"devDependencies": {
"@athenna/test": "^5.0.0",
"@athenna/test": "^5.1.0",
"@athenna/tsconfig": "^5.0.0",
"@types/bytes": "^3.1.4",
"@types/bytes": "^3.1.5",
"@types/callsite": "^1.0.34",
"@types/debug": "^4.1.12",
"@types/kind-of": "^6.0.3",
"@types/lodash": "^4.17.7",
"@types/lodash": "^4.17.13",
"@types/ms": "^0.7.34",
"@types/pluralize": "^0.0.29",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"commitizen": "^4.3.0",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.6.0",
Expand Down
20 changes: 20 additions & 0 deletions src/exceptions/InvalidUlidException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @athenna/common
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Exception } from '#src/helpers/Exception'

export class InvalidUlidException extends Exception {
public constructor(value: string) {
super({
code: 'E_INVALID_ULID',
help: 'Use a valid ULID instead.',
message: `The value ${value} is not a valid ULID.`
})
}
}
11 changes: 11 additions & 0 deletions src/helpers/Is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kindOf from 'kind-of'
import { isIP } from 'node:net'
import { File } from '#src/helpers/File'
import { Uuid } from '#src/helpers/Uuid'
import { Ulid } from '#src/helpers/Ulid'
import { Exception } from '#src/helpers/Exception'
import { isCep, isCnpj, isCpf } from 'validator-brazil'

Expand Down Expand Up @@ -83,6 +84,16 @@ export class Is {
return Uuid.verify(value, options)
}

/**
* Verify if is valid Ulid.
*/
public static Ulid(
value: string,
options?: { prefix?: string; ignorePrefix?: boolean }
): boolean {
return Ulid.verify(value, options)
}

/**
* Verify if the value is defined, even
* with falsy values like false and ''.
Expand Down
126 changes: 126 additions & 0 deletions src/helpers/Ulid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* @athenna/common
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { ulid } from 'ulid'
import { Options } from '#src/helpers/Options'
import { InvalidUlidException } from '#src/exceptions/InvalidUlidException'

const pattern = /^[0-9A-HJKMNP-TV-Z]{26}$/

export function validate(value: unknown): boolean {
return typeof value === 'string' && pattern.test(value)
}

export class Ulid {
/**
* Verify if string is a valid ulid.
*/
public static verify(
token: string,
options: { prefix?: string; ignorePrefix?: boolean } = {}
): boolean {
if (!token) {
return false
}

options = Options.create(options, { ignorePrefix: true })

if (options.prefix) {
const prefix = this.getPrefix(token)

if (prefix !== options.prefix) {
return false
}

return validate(this.getToken(token))
}

if (options.ignorePrefix) {
return validate(this.getToken(token))
}

return validate(token)
}

/**
* Generate an ulid token
*/
public static generate(prefix?: string): string {
if (prefix) {
return `${prefix}::${ulid()}`
}

return ulid()
}

/**
* Return the token without his prefix.
*/
public static getToken(token: string): string {
const prefix = Ulid.getPrefix(token)

if (!prefix) {
return token
}

return token.split(`${prefix}::`)[1]
}

/**
* Return the prefix without his token.
*/
public static getPrefix(token: string): string | null {
const prefix = token.split('::')[0]

/**
* Means that the "::" char has not been
* found. So there is no prefix in the token.
*/
if (prefix === token) {
return null
}

return prefix
}

/**
* Inject a prefix in the ulid token.
*/
public static injectPrefix(prefix: string, token: string): string {
if (!this.verify(token)) {
throw new InvalidUlidException(token)
}

return `${prefix}::${token}`
}

/**
* Change the prefix of an ulid token
*/
public static changePrefix(newPrefix: string, token: string): string {
const ulid = this.getToken(token)

if (!this.verify(ulid)) {
throw new InvalidUlidException(ulid)
}

return `${newPrefix}::${ulid}`
}

/**
* Change the token prefix or generate a new one
*/
public static changeOrGenerate(prefix: string, token?: string): string {
if (token) {
return this.changePrefix(prefix, token)
}

return this.generate(prefix)
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export * from '#src/helpers/Path'
export * from '#src/helpers/Route'
export * from '#src/helpers/String'
export * from '#src/helpers/Uuid'
export * from '#src/helpers/Ulid'
5 changes: 4 additions & 1 deletion tests/unit/ExecTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ export default class ExecTest {

@Test()
public async shouldBeAbleToDownloadFiles({ assert }: Context) {
const file = await Exec.download(Path.storage('downloads/node.pkg'), 'https://nodejs.org/dist/latest/node.pkg')
const file = await Exec.download(
Path.storage('downloads/node.pkg'),
'https://nodejs.org/dist/v23.5.0/node-23.5.0.pkg'
)

assert.equal(file.base, 'node.pkg')
assert.isTrue(await File.exists(file.path))
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/ModuleTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ export default class ModuleTest {
public async shouldBeAbleToGetAllModulesFirstExportMatchOrDefaultFromAnyPath({ assert }: Context) {
const modules = await Module.getAllFrom(Path.src('helpers'))

assert.lengthOf(modules, 20)
assert.lengthOf(modules, 21)
assert.equal(modules[0].name, 'Clean')
}

@Test()
public async shouldBeAbleToGetAllModulesFirstExportMatchOrDefaultFromAnyPathWithAlias({ assert }: Context) {
const modules = await Module.getAllFromWithAlias(Path.src('helpers'), 'App/Helpers')

assert.lengthOf(modules, 20)
assert.lengthOf(modules, 21)
assert.equal(modules[0].module.name, 'Clean')
assert.equal(modules[0].alias, 'App/Helpers/Clean')
}
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/UlidTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @athenna/common
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { ulid } from 'ulid'
import { Ulid } from '#src'
import { Test, type Context } from '@athenna/test'
import { InvalidUlidException } from '#src/exceptions/InvalidUlidException'

export default class UlidTest {
private ulid = ulid()

@Test()
public shouldVerifyIfUlidIsAValidUlidEventIfItIsPrefixed({ assert }: Context) {
const tokenPrefixed = Ulid.generate('tkn')

const verify = Ulid.verify(this.ulid)
const verifyError = Ulid.verify('falseUlid')
const verifyPrefixed = Ulid.verify(tokenPrefixed)

assert.isTrue(verify)
assert.isFalse(verifyError)
assert.isTrue(verifyPrefixed)
}

@Test()
public shouldGetOnlyTheTokenFromPrefixedUlid({ assert }: Context) {
const tokenUlid = Ulid.generate('tkn')

assert.equal(Ulid.getToken(tokenUlid), tokenUlid.replace('tkn::', ''))
}

@Test()
public shouldGetOnlyThePrefixFromPrefixedUlid({ assert }: Context) {
const tokenUlid = Ulid.generate('tkn')

assert.isNull(Ulid.getPrefix(this.ulid), null)
assert.equal(Ulid.getPrefix(tokenUlid), 'tkn')
}

@Test()
public shouldInjectThePrefixInTheToken({ assert }: Context) {
const tokenUlid = Ulid.generate()
const injectedPrefix = Ulid.injectPrefix('tkn', tokenUlid)
const tokenPrefixedChange = Ulid.changePrefix('any', injectedPrefix)

assert.equal(injectedPrefix, `tkn::${tokenUlid}`)
assert.equal(tokenPrefixedChange, `any::${tokenUlid}`)

const useCase = () => Ulid.injectPrefix('tkn', 'not-valid-ulid')

assert.throws(useCase, InvalidUlidException)
}

@Test()
public shouldChangeOrGenerateANewToken({ assert }: Context) {
const tokenGenerated = Ulid.changeOrGenerate('tkn', undefined)
const tokenChanged = Ulid.changeOrGenerate('tkn', `ooo::${this.ulid}`)

assert.isDefined(tokenGenerated)
assert.equal(tokenChanged, `tkn::${this.ulid}`)

const useCase = () => Ulid.changePrefix('tkn', 'not-valid-ulid')

assert.throws(useCase, InvalidUlidException)
}
}

0 comments on commit 029491f

Please sign in to comment.