diff --git a/CHANGELOG.md b/CHANGELOG.md index 56dfb70..0a4c4c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,13 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [5.2.0] - 2024-05-01 +### Added +- `validate` method to check shortIds for simple or "complete" validity + ## [5.1.0] - 2024-04-30 ### Added -- Added `uuid25Base36` constant to support uuid25 style +- `uuid25Base36` constant to support uuid25 style ## [5.0.1] - 2024-04-30 ### Changed diff --git a/README.md b/README.md index e72d6a6..8710a60 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Generate and translate standard UUIDs into shorter - or just *different* - formats and back. -## v5.1.0 +## v5.2.0 +5.2.0 adds `validate` method to check short IDs. Requested by [@U-4-E-A](https://github.com/U-4-E-A) 5.1.0 adds translation support for the [uuid25](https://github.com/uuid25/javascript) (Base36) format with the `uuid25Base36` constant. @@ -19,14 +20,15 @@ with the `uuid25Base36` constant. const short = require('short-uuid'); // Quick start with flickrBase58 format -short.generate(); // 73WakrfVbNJBaAmhQtEeDv +short.generate(); // '73WakrfVbNJBaAmhQtEeDv' ``` ### Details short-uuid starts with RFC4122 v4-compliant UUIDs and translates them into other, usually shorter formats. It also provides translators -to convert back and forth from RFC compliant UUIDs to the shorter formats. +to convert back and forth from RFC compliant UUIDs to the shorter formats, +and validate the IDs. As of 4.0.0, formats return consistent-length values unless specifically requested. This is done by padding the start with the first (`[0]`) character in the alphabet. @@ -35,6 +37,9 @@ Previous versions can translate padded formats back to UUID. ```javascript const short = require('short-uuid'); +// Generate a flickrBase58 short ID from without creating a translator +const shortId = short.generate(); + const translator = short(); // Defaults to flickrBase58 const decimalTranslator = short("0123456789"); // Provide a specific alphabet for translation const cookieTranslator = short(short.constants.cookieBase90); // Use a constant for translation @@ -47,11 +52,12 @@ translator.generate(); // An alias for new. translator.toUUID(shortId); // a44521d0-0fb8-4ade-8002-3385545c3318 translator.fromUUID(regularUUID); // mhvXdrZT4jP5T8vBxuvm75 -// Generate plain UUIDs -// - From the library without creating a translator -short.uuid(); // fd5c084c-ff7c-4651-9a52-37096242d81c -// - Each translator provides the uuid.v4() function, too -translator.uuid(); // 3023b0f5-ec55-4e75-9cd8-104700698052 +// Check if a string is a valid ID (length and alphabet) +translator.validate(shortId); // true + +// Check if a string is valid *AND* translates to a valid UUID +translator.validate(shortId, true); // true +translator.validate('0000000000000000000000', true) // false // See the alphabet used by a translator translator.alphabet; @@ -64,6 +70,12 @@ translator.maxLength; short.constants.cookieBase90; // Safe for HTTP cookies values for smaller IDs. short.constants.flickrBase58; // Avoids similar characters (0/O, 1/I/l, etc.) short.constants.uuid25Base36; // The uuid25 (string length 25) format + +// Generate plain UUIDs +// - From the library without creating a translator +short.uuid(); // fd5c084c-ff7c-4651-9a52-37096242d81c +// - Each translator provides the uuid.v4() function, too +translator.uuid(); // 3023b0f5-ec55-4e75-9cd8-104700698052 ``` ### Options @@ -87,7 +99,7 @@ translator.new(); // mhvXdrZT4jP5T8vBxuvm75 ## Support -short-uuid [5.x](https://github.com/oculus42/short-uuid/blob/v5.1.0/README.md) +short-uuid [5.x](https://github.com/oculus42/short-uuid/blob/v5.2.0/README.md) and later is tested on Node 14.x and later. short-uuid [4.x](https://github.com/oculus42/short-uuid/blob/v3.2.2/README.md) diff --git a/index.d.ts b/index.d.ts index 50eae89..14a25f2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -41,6 +41,9 @@ declare module 'short-uuid' { /** long -> short */ fromUUID(regularUUID: string | UUID): SUUID; + + /** validate short */ + validate(shortId: string | SUUID, rigorous?: boolean): boolean; } } diff --git a/index.js b/index.js index 09483b1..a178401 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,7 @@ * Simple wrapper functions to produce shorter UUIDs for cookies, maybe everything? */ -const { v4: uuidV4 } = require('uuid'); +const { v4: uuidV4, validate: uuidValidate } = require('uuid'); const anyBase = require('any-base'); const constants = { @@ -22,7 +22,7 @@ let toFlickr; /** * Takes a UUID, strips the dashes, and translates. * @param {string} longId - * @param {function(string)} translator + * @param {function(string):string} translator * @param {Object} [paddingParams] * @returns {string} */ @@ -53,20 +53,27 @@ const enlargeUUID = (shortId, translator) => { return [m[1], m[2], m[3], m[4], m[5]].join('-'); }; -// Calculate length for the shortened ID +/** + * Calculate length for the shortened ID + * @param {number} alphabetLength + * @returns {number} + */ const getShortIdLength = (alphabetLength) => ( Math.ceil(Math.log(2 ** 128) / Math.log(alphabetLength))); module.exports = (() => { /** - * @param {string} toAlphabet - Defaults to flickrBase58 if not provided - * @param {Object} [options] - * - * @returns {{new: (function()), - * uuid: (function()), - * fromUUID: (function(string)), - * toUUID: (function(string)), - * alphabet: (string)}} + * @param {string} toAlphabet + * @param {{ consistentLength: boolean }} [options] + * @returns {{ + * alphabet: string, + * fromUUID: (function(*): string), + * generate: (function(): string), + * maxLength: number, + * new: (function(): string), + * toUUID: (function(*): string), + * uuid: ((function(*, *, *): (*))|*), + * validate: ((function(*, boolean=false): (boolean))|*)}} */ const makeConvertor = (toAlphabet, options) => { // Default to Flickr 58 @@ -92,16 +99,38 @@ module.exports = (() => { // UUIDs are in hex, so we translate to and from. const fromHex = anyBase(anyBase.HEX, useAlphabet); const toHex = anyBase(useAlphabet, anyBase.HEX); + /** + * @returns {string} - short id + */ const generate = () => shortenUUID(uuidV4(), fromHex, paddingParams); + /** + * Confirm if string is a valid id. Checks length and alphabet. + * If the second parameter is true it will translate to standard UUID + * and check the result for UUID validity. + * @param {string} shortId - The string to check for validity + * @param {boolean} [rigorous=false] - If true, also check for a valid UUID + * @returns {boolean} + */ + const validate = (shortId, rigorous = false) => { + if (!shortId || typeof shortId !== 'string') return false; + const isCorrectLength = selectedOptions.consistentLength + ? shortId.length === shortIdLength + : shortId.length <= shortIdLength; + const onlyAlphabet = shortId.split('').every((letter) => useAlphabet.includes(letter)); + if (rigorous === false) return isCorrectLength && onlyAlphabet; + return isCorrectLength && onlyAlphabet && uuidValidate(enlargeUUID(shortId, toHex)); + }; + const translator = { - new: generate, - generate, - uuid: uuidV4, - fromUUID: (uuid) => shortenUUID(uuid, fromHex, paddingParams), - toUUID: (shortUuid) => enlargeUUID(shortUuid, toHex), alphabet: useAlphabet, + fromUUID: (uuid) => shortenUUID(uuid, fromHex, paddingParams), maxLength: shortIdLength, + generate, + new: generate, + toUUID: (shortUuid) => enlargeUUID(shortUuid, toHex), + uuid: uuidV4, + validate, }; Object.freeze(translator); diff --git a/package-lock.json b/package-lock.json index 29faa27..f793c5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "short-uuid", - "version": "5.1.0", + "version": "5.2.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 2faea7d..9ebb824 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "short-uuid", - "version": "5.1.0", + "version": "5.2.0", "description": "Create and translate standard UUIDs with shorter formats.", "main": "index.js", "typings": "index.d.ts", diff --git a/test/index.js b/test/index.js index ea7db1e..15046ad 100644 --- a/test/index.js +++ b/test/index.js @@ -269,9 +269,11 @@ test('uuid25 should be compatible with uuid25 examples', (t) => { }); }); -test('uuid25 translator should provide maxLength 25', (t) => { - t.plan(1); - t.equal(b36.maxLength, 25); +test('Translator should provide correct maxLength', (t) => { + t.plan(3); + t.equal(b36.maxLength, 25, 'uuid25 is 25'); + t.equal(b58.maxLength, 22, 'flickr is 22'); + t.equal(b90.maxLength, 20, 'cookie is 20'); }); test('Default generate quantity tests', (t) => { @@ -286,3 +288,52 @@ test('Default generate quantity tests', (t) => { t.equal(underLength, 0, 'Ensure default is padded'); }); + +test('Validate', (t) => { + t.plan(25); + + // Bad type + t.notOk(b36.validate(100), 'only strings'); + + // Too short + t.notOk(b36.validate('123'), 'uuid25 too short'); + t.notOk(b58.validate('123'), 'flickr too short'); + t.notOk(b90.validate('123'), 'cookie too short'); + + // Short is valid without consistentLength + const s36 = short(short.constants.uuid25Base36, { consistentLength: false }); + const s58 = short(short.constants.flickrBase58, { consistentLength: false }); + const s90 = short(short.constants.cookieBase90, { consistentLength: false }); + t.ok(s36.validate('111')); + t.ok(s58.validate('111')); + t.ok(s90.validate('111')); + + // Too long + t.notOk(b36.validate('123456789012345678901234567890'), 'uuid25 too long'); + t.notOk(b58.validate('123456789012345678901234567890'), 'flickr too long'); + t.notOk(b90.validate('123456789012345678901234567890'), 'cookie too long'); + + // Bad alphabet + t.notOk(b36.validate('123456789012345678901234"'), 'uuid25 validates alphabet'); + t.notOk(b58.validate('123456789012345678901"'), 'flickr validates alphabet'); + t.notOk(b90.validate('1234567890123456789"'), 'cookie validates alphabet'); + + // Generated value passes + t.ok(b36.validate(b36.generate()), 'uuid25 validates'); + t.ok(b58.validate(b58.generate()), 'flickr validates'); + t.ok(b90.validate(b90.generate()), 'cookie validates'); + + // "empty" value passes without uuid check + t.notOk(b36.validate('0'), 'uuid25 passes bad uuid without check'); + t.notOk(b58.validate('0'), 'flickr fails bad uuid without check'); + t.notOk(b90.validate('0'), 'cookie fails bad uuid without check'); + + // With uuid check + t.ok(b36.validate(b36.generate(), true), 'uuid25 validates uuid'); + t.ok(b58.validate(b58.generate(), true), 'flickr validates uuid'); + t.ok(b90.validate(b90.generate(), true), 'cookie validates uuid'); + + t.notOk(b36.validate('0', true), 'uuid25 fails bad uuid'); + t.notOk(b58.validate('0', true), 'flickr fails bad uuid'); + t.notOk(b90.validate('0', true), 'cookie fails bad uuid'); +});