From 2acba513672f0fe5d2deb2eb229310a75957ad8f Mon Sep 17 00:00:00 2001 From: nvms Date: Wed, 26 Mar 2025 19:03:31 -0400 Subject: [PATCH] refactor: convert ids to functional API with improved tests - Replace class-based implementation with functional approach - Switch from Manten to Vitest for testing - Update README with clearer API documentation - Fix alphabet randomization test --- packages/ids/README.md | 40 ++++++++----- packages/ids/bun.lockb | Bin 92409 -> 99702 bytes packages/ids/package.json | 7 ++- packages/ids/src/index.ts | 100 ++++++++++++++++--------------- packages/ids/tests/index.test.ts | 51 ++++++++++++++++ packages/ids/tests/index.ts | 36 ----------- packages/ids/vitest.config.ts | 7 +++ 7 files changed, 139 insertions(+), 102 deletions(-) create mode 100644 packages/ids/tests/index.test.ts delete mode 100644 packages/ids/tests/index.ts create mode 100644 packages/ids/vitest.config.ts diff --git a/packages/ids/README.md b/packages/ids/README.md index ff49ea5..dde1d04 100644 --- a/packages/ids/README.md +++ b/packages/ids/README.md @@ -2,29 +2,41 @@ [![NPM version](https://img.shields.io/npm/v/@prsm/ids?color=a1b858&label=)](https://www.npmjs.com/package/@prsm/ids) -Short, obfuscated, collision-proof, and reversible identifiers. +Short, obfuscated, collision-proof, reversible identifiers. -Because sometimes internal identifiers are sensitive, or you just don't want to let a user know that their ID is 1. +## Usage ```typescript -import ID from "@prsm/ids"; +import id from "@prsm/ids"; -ID.encode(12389125); // phsV8T -ID.decode("phsV8T"); // 12389125 +id.encode(12389125); // "7rYTs_" +id.decode("7rYTs_"); // 12389125 ``` -You can (and should) set your own alphabet string: +## Configuration +Set custom alphabet: ```typescript -ID.alphabet = "GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"; -ID.alphabet = "TgzMhJXtRSVBnHFksZQc5j-yGx84W3rNDfK6p_Cbqd29YLm7Pwv"; -ID.alphabet = "kbHn53dZphT2FvGMBxYJKqS7-cPV_Ct6LwjWRDfXmygzrQ48N9s"; +id.setAlphabet("GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"); ``` -If your use case makes sense, you can also generate a random alphabet string with `randomizeAlphabet`. - -When the alphabet changes, though, the encoded IDs will change as well. Decoding will still work, but the decoded value will be different. - +Randomize alphabet: ```typescript -ID.randomizeAlphabet(); +id.randomizeAlphabet(); ``` + +## API + +| Function | Description | +|-----------------------|-------------------------------------------| +| `encode(num)` | Converts number to obfuscated string | +| `decode(str)` | Converts obfuscated string back to number | +| `setAlphabet(str)` | Sets custom alphabet for encoding | +| `getAlphabet()` | Returns current alphabet | +| `randomizeAlphabet()` | Shuffles alphabet characters randomly | + +## Notes + +- Maximum encodable value: 2,147,483,647 (MAX_INT32) +- Changing alphabet changes encoded values +- Encoded values must be decoded with same alphabet diff --git a/packages/ids/bun.lockb b/packages/ids/bun.lockb index 342b14cf6bbd927cf3b9b71bf1e8f4e1f9ebd3da..0709c3ff6914ba02c1fbe0a3391fb8be571a5f19 100755 GIT binary patch delta 27007 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 F{{bOK6+{34 diff --git a/packages/ids/package.json b/packages/ids/package.json index bede136..1518712 100644 --- a/packages/ids/package.json +++ b/packages/ids/package.json @@ -13,7 +13,8 @@ }, "scripts": { "build": "tsup", - "test": "bun tests/index.ts", + "test": "vitest run", + "test:watch": "vitest", "release": "bumpp package.json && npm publish --access public" }, "author": "nvms", @@ -23,8 +24,8 @@ }, "devDependencies": { "bumpp": "^9.5.1", - "manten": "^0.6.0", "tsup": "^8.2.4", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "vitest": "^3.0.9" } } diff --git a/packages/ids/src/index.ts b/packages/ids/src/index.ts index 9dbe743..22579ec 100644 --- a/packages/ids/src/index.ts +++ b/packages/ids/src/index.ts @@ -1,74 +1,76 @@ import long from "long"; -export default class ID { - private static MAX_INT32 = 2_147_483_647; - private static MULTIPLIER = 4_294_967_296; +const MAX_INT32 = 2_147_483_647; +const PRIME = 1_125_812_041; +const INVERSE = 348_986_105; +const RANDOM = 998_048_641; +const DEFAULT_ALPHABET = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ-_"; - static alphabet: string = - "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ-_"; - static prime: number = 1_125_812_041; - static inverse: number = 348_986_105; - static random: number = 998_048_641; +let alphabet = DEFAULT_ALPHABET; - static get base(): number { - return ID.alphabet.length; +const getBase = () => alphabet.length; + +const shorten = (id: number): string => { + let result = ""; + const base = getBase(); + + while (id > 0) { + result = alphabet[id % base] + result; + id = Math.floor(id / base); } - private static shorten(id: number): string { - let result = ""; + return result; +}; - while (id > 0) { - result = ID.alphabet[id % ID.base] + result; - id = Math.floor(id / ID.base); - } +const unshorten = (str: string): number => { + let result = 0; + const base = getBase(); - return result; + for (let i = 0; i < str.length; i++) { + result = result * base + alphabet.indexOf(str[i]); } - private static unshorten(str: string): number { - let result = 0; + return result; +}; - for (let i = 0; i < str.length; i++) { - result = result * ID.base + ID.alphabet.indexOf(str[i]); - } +const id = { + MAX_INT32, + DEFAULT_ALPHABET, - return result; - } - - static encode = (num: number): string => { - if (num > ID.MAX_INT32) { + encode: (num: number): string => { + if (num > MAX_INT32) { throw new Error( - `Number (${num}) is too large to encode. MAX_INT32 is ${ID.MAX_INT32}`, + `Number (${num}) is too large to encode. MAX_INT32 is ${MAX_INT32}`, ); } - const n: long = long.fromInt(num); + const n = long.fromInt(num); - return ID.shorten( - n - .multiply(ID.prime) - .and(long.fromInt(ID.MAX_INT32)) - .xor(ID.random) - .toInt(), + return shorten( + n.multiply(PRIME).and(long.fromInt(MAX_INT32)).xor(RANDOM).toInt(), ); - }; + }, - static decode = (str: string): number => { - const n: long = long.fromInt(ID.unshorten(str)); + decode: (str: string): number => { + const n = long.fromInt(unshorten(str)); - return n - .xor(ID.random) - .multiply(ID.inverse) - .and(long.fromInt(ID.MAX_INT32)) - .toInt(); - }; + return n.xor(RANDOM).multiply(INVERSE).and(long.fromInt(MAX_INT32)).toInt(); + }, - static randomizeAlphabet(): void { - const array = ID.alphabet.split(''); + getAlphabet: (): string => alphabet, + + setAlphabet: (newAlphabet: string): void => { + alphabet = newAlphabet; + }, + + randomizeAlphabet: (): void => { + const array = alphabet.split(""); for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } - ID.alphabet = array.join(''); - } -} + alphabet = array.join(""); + }, +}; + +export default id; diff --git a/packages/ids/tests/index.test.ts b/packages/ids/tests/index.test.ts new file mode 100644 index 0000000..e9e102f --- /dev/null +++ b/packages/ids/tests/index.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import id from "../src"; + +describe("ids", () => { + // Reset alphabet before each test + beforeEach(() => { + id.setAlphabet(id.DEFAULT_ALPHABET); + }); + + it("encodes as expected", () => { + const encoded = id.encode(12389125); + expect(encoded).toBe("7rYTs_"); + }); + + it("decodes as expected", () => { + const decoded = id.decode("7rYTs_"); + expect(decoded).toBe(12389125); + }); + + it("changing the alphabet is effective", () => { + id.setAlphabet("GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"); + expect(id.encode(12389125)).toBe("phsV8T"); + expect(id.decode("phsV8T")).toBe(12389125); + }); + + it("shuffling the alphabet changes encoding but preserves round-trip integrity", () => { + // First randomization + id.randomizeAlphabet(); + const encoded1 = id.encode(12389125); + const decoded1 = id.decode(encoded1); + expect(decoded1).toBe(12389125); + + // Store the current alphabet + const alphabet1 = id.getAlphabet(); + + // Second randomization + id.randomizeAlphabet(); + const alphabet2 = id.getAlphabet(); + + // Encode with the new alphabet + const encoded2 = id.encode(12389125); + const decoded2 = id.decode(encoded2); + + // Each alphabet should produce different encodings for the same number + expect(alphabet1).not.toBe(alphabet2); + expect(encoded1).not.toBe(encoded2); + + // But round-trip encoding/decoding should work with each alphabet + expect(decoded2).toBe(12389125); + }); +}); diff --git a/packages/ids/tests/index.ts b/packages/ids/tests/index.ts deleted file mode 100644 index 85e1fda..0000000 --- a/packages/ids/tests/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect } from "manten"; -import ID from "../src"; - -describe("ids", async ({ test }) => { - test("encodes as expected", () => { - const encoded = ID.encode(12389125); - expect(encoded).toBe("7rYTs_"); - }); - - test("decodes as expected", () => { - const decoded = ID.decode("7rYTs_"); - expect(decoded).toBe(12389125); - }); - - test("changing the alphabet is effective", () => { - ID.alphabet = "GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"; - expect(ID.encode(12389125)).toBe("phsV8T"); - expect(ID.decode("phsV8T")).toBe(12389125); - }); - - test("shuffling the alphabet still allows you to decode things", () => { - ID.randomizeAlphabet(); - const encoded = ID.encode(12389125); - const decoded = ID.decode(encoded); - expect(decoded).toBe(12389125); - - console.log(ID.alphabet); - - ID.randomizeAlphabet(); - // const encoded2 = ID.encode(12389125); - const decoded2 = ID.decode(encoded); - expect(decoded2).toBe(12389125); - - // expect(encoded).not.toBe(encoded2); - }) -}); diff --git a/packages/ids/vitest.config.ts b/packages/ids/vitest.config.ts new file mode 100644 index 0000000..4ac6027 --- /dev/null +++ b/packages/ids/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + }, +});