diff --git a/.changeset/chatty-words-beg.md b/.changeset/chatty-words-beg.md new file mode 100644 index 0000000..f27e9e1 --- /dev/null +++ b/.changeset/chatty-words-beg.md @@ -0,0 +1,5 @@ +--- +"@webiny/stdlib": patch +--- + +Add nanoid-based ID generators: `generateId`, `generateAlphaNumericId`, `generateAlphaNumericLowerCaseId`, `generateAlphaId`, `generateAlphaLowerCaseId`, `generateAlphaUpperCaseId`. All accept an optional `size` parameter (default 21). Import from `@webiny/stdlib`. diff --git a/AGENTS.md b/AGENTS.md index d75d9f0..8e38ad1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,6 +75,7 @@ Contains: - `CacheError` — abstract base error for all cache implementations. Subclass it for implementation-specific errors. - `MemoryCacheFeature` — registers an in-memory `Cache` implementation in singleton scope. - `AsyncMemoryCacheFeature` — registers an in-memory `AsyncCache` implementation in singleton scope. +- `generateId(size?)` — generates a URL-safe nanoid (default 21 chars). Also: `generateAlphaNumericId`, `generateAlphaNumericLowerCaseId`, `generateAlphaId`, `generateAlphaLowerCaseId`, `generateAlphaUpperCaseId` — each accepts an optional `size` parameter. ### `@webiny/stdlib/node` diff --git a/README.md b/README.md index 2db6ecc..d6cdce2 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,18 @@ The package is ESM-only and ships three subpath exports. Because each is a separ ## `@webiny/stdlib` — Common -| Feature | Description | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | -| `Result` / `ResultAsync` | Typed success/failure values — synchronous and async | -| `BaseError` | Abstract base class for typed domain errors | -| `Logger` / `ConsoleLogger` / `ConsoleLoggerFeature` | Logging abstraction + console implementation — [docs](src/common/features/Logger/README.md) | -| `Cache` / `MemoryCacheFeature` | Synchronous key-value cache — [docs](src/common/features/Cache/README.md) | -| `AsyncCache` / `AsyncMemoryCacheFeature` | Async key-value cache — [docs](src/common/features/Cache/README.md) | -| `immutableGet` / `immutableSet` / `immutableDelete` / `mutableSet` / `mutableDelete` | Dot-notation get/set/delete on nested objects — [docs](src/common/utils/dotProp/README.md) | -| `toBoolean` / `isTruthy` / `isFalsy` | Semantic boolean coercion — [docs](src/common/utils/boolean/README.md) | -| `uuid` | RFC 4122 v4 UUID generator (native + fallback) — [docs](src/common/utils/uuid/README.md) | -| `mdbid` | MongoDB-compatible ObjectId generator — [docs](src/common/utils/mdbid/README.md) | +| Feature | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `Result` / `ResultAsync` | Typed success/failure values — synchronous and async | +| `BaseError` | Abstract base class for typed domain errors | +| `Logger` / `ConsoleLogger` / `ConsoleLoggerFeature` | Logging abstraction + console implementation — [docs](src/common/features/Logger/README.md) | +| `Cache` / `MemoryCacheFeature` | Synchronous key-value cache — [docs](src/common/features/Cache/README.md) | +| `AsyncCache` / `AsyncMemoryCacheFeature` | Async key-value cache — [docs](src/common/features/Cache/README.md) | +| `immutableGet` / `immutableSet` / `immutableDelete` / `mutableSet` / `mutableDelete` | Dot-notation get/set/delete on nested objects — [docs](src/common/utils/dotProp/README.md) | +| `toBoolean` / `isTruthy` / `isFalsy` | Semantic boolean coercion — [docs](src/common/utils/boolean/README.md) | +| `uuid` | RFC 4122 v4 UUID generator (native + fallback) — [docs](src/common/utils/uuid/README.md) | +| `mdbid` | MongoDB-compatible ObjectId generator — [docs](src/common/utils/mdbid/README.md) | +| `generateId` / `generateAlphaNumericId` / `generateAlphaLowerCaseId` / ... | Nanoid-based ID generators with configurable alphabets — [docs](src/common/utils/generateId/README.md) | --- diff --git a/__tests__/generateId.test.ts b/__tests__/generateId.test.ts new file mode 100644 index 0000000..1c930cd --- /dev/null +++ b/__tests__/generateId.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect } from "vitest"; +import { + generateId, + generateAlphaNumericId, + generateAlphaNumericLowerCaseId, + generateAlphaId, + generateAlphaLowerCaseId, + generateAlphaUpperCaseId +} from "../src/common/utils/generateId/generateId.js"; + +const DEFAULT_SIZE = 21; + +describe("generateId", () => { + it("returns a string of default length", () => { + const id = generateId(); + expect(typeof id).toBe("string"); + expect(id.length).toBe(DEFAULT_SIZE); + }); + + it("accepts a custom size", () => { + const id = generateId(10); + expect(id.length).toBe(10); + }); + + it("generates unique values on successive calls", () => { + const ids = new Set(Array.from({ length: 1000 }, () => generateId())); + expect(ids.size).toBe(1000); + }); +}); + +describe("generateAlphaNumericId", () => { + it("returns a string of default length", () => { + const id = generateAlphaNumericId(); + expect(typeof id).toBe("string"); + expect(id.length).toBe(DEFAULT_SIZE); + }); + + it("contains only alphanumeric characters", () => { + const id = generateAlphaNumericId(); + expect(id).toMatch(/^[a-zA-Z0-9]+$/); + }); + + it("accepts a custom size", () => { + const id = generateAlphaNumericId(10); + expect(id.length).toBe(10); + }); + + it("generates unique values on successive calls", () => { + const ids = new Set(Array.from({ length: 1000 }, () => generateAlphaNumericId())); + expect(ids.size).toBe(1000); + }); +}); + +describe("generateAlphaNumericLowerCaseId", () => { + it("returns a string of default length", () => { + const id = generateAlphaNumericLowerCaseId(); + expect(typeof id).toBe("string"); + expect(id.length).toBe(DEFAULT_SIZE); + }); + + it("contains only lowercase alphanumeric characters", () => { + const id = generateAlphaNumericLowerCaseId(); + expect(id).toMatch(/^[a-z0-9]+$/); + }); + + it("accepts a custom size", () => { + const id = generateAlphaNumericLowerCaseId(10); + expect(id.length).toBe(10); + }); + + it("generates unique values on successive calls", () => { + const ids = new Set(Array.from({ length: 1000 }, () => generateAlphaNumericLowerCaseId())); + expect(ids.size).toBe(1000); + }); +}); + +describe("generateAlphaId", () => { + it("returns a string of default length", () => { + const id = generateAlphaId(); + expect(typeof id).toBe("string"); + expect(id.length).toBe(DEFAULT_SIZE); + }); + + it("contains only alphabetic characters", () => { + const id = generateAlphaId(); + expect(id).toMatch(/^[a-zA-Z]+$/); + }); + + it("accepts a custom size", () => { + const id = generateAlphaId(10); + expect(id.length).toBe(10); + }); + + it("generates unique values on successive calls", () => { + const ids = new Set(Array.from({ length: 1000 }, () => generateAlphaId())); + expect(ids.size).toBe(1000); + }); +}); + +describe("generateAlphaLowerCaseId", () => { + it("returns a string of default length", () => { + const id = generateAlphaLowerCaseId(); + expect(typeof id).toBe("string"); + expect(id.length).toBe(DEFAULT_SIZE); + }); + + it("contains only lowercase alphabetic characters", () => { + const id = generateAlphaLowerCaseId(); + expect(id).toMatch(/^[a-z]+$/); + }); + + it("accepts a custom size", () => { + const id = generateAlphaLowerCaseId(10); + expect(id.length).toBe(10); + }); + + it("generates unique values on successive calls", () => { + const ids = new Set(Array.from({ length: 1000 }, () => generateAlphaLowerCaseId())); + expect(ids.size).toBe(1000); + }); +}); + +describe("generateAlphaUpperCaseId", () => { + it("returns a string of default length", () => { + const id = generateAlphaUpperCaseId(); + expect(typeof id).toBe("string"); + expect(id.length).toBe(DEFAULT_SIZE); + }); + + it("contains only uppercase alphabetic characters", () => { + const id = generateAlphaUpperCaseId(); + expect(id).toMatch(/^[A-Z]+$/); + }); + + it("accepts a custom size", () => { + const id = generateAlphaUpperCaseId(10); + expect(id.length).toBe(10); + }); + + it("generates unique values on successive calls", () => { + const ids = new Set(Array.from({ length: 1000 }, () => generateAlphaUpperCaseId())); + expect(ids.size).toBe(1000); + }); +}); diff --git a/package.json b/package.json index e05d5a1..c37c5f0 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "bson-objectid": "^2.0.4", "dot-prop": "^10.1.0", "fast-glob": "^3.3.3", + "nanoid": "^5.1.11", + "nanoid-dictionary": "^5.0.0", "pino": "^10.3.1", "pino-pretty": "^13.1.3", "type-fest": "^5.7.0", diff --git a/src/common/index.ts b/src/common/index.ts index d8c897a..2a42a9a 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -22,3 +22,11 @@ export { } from "./utils/dotProp/index.js"; export { uuid } from "./utils/uuid/index.js"; export { mdbid } from "./utils/mdbid/index.js"; +export { + generateAlphaNumericId, + generateAlphaNumericLowerCaseId, + generateAlphaId, + generateAlphaLowerCaseId, + generateAlphaUpperCaseId, + generateId +} from "./utils/generateId/index.js"; diff --git a/src/common/utils/README.md b/src/common/utils/README.md index 14482de..f1d55bc 100644 --- a/src/common/utils/README.md +++ b/src/common/utils/README.md @@ -2,9 +2,10 @@ Standalone utility functions exported from `@webiny/stdlib`. No DI container required — import and call directly. -| Util | Description | -| ---------------------------- | ----------------------------------------------------------------------------- | -| [boolean](boolean/README.md) | Semantic boolean coercion (`toBoolean`, `isTruthy`, `isFalsy`) | -| [dotProp](dotProp/README.md) | Immutable and mutable get/set/delete on nested objects via dot-notation | -| [uuid](uuid/README.md) | RFC 4122 v4 UUID generator (native `randomUUID` + `getRandomValues` fallback) | -| [mdbid](mdbid/README.md) | MongoDB-compatible ObjectId generator via `bson-objectid` | +| Util | Description | +| ---------------------------------- | ----------------------------------------------------------------------------- | +| [boolean](boolean/README.md) | Semantic boolean coercion (`toBoolean`, `isTruthy`, `isFalsy`) | +| [dotProp](dotProp/README.md) | Immutable and mutable get/set/delete on nested objects via dot-notation | +| [uuid](uuid/README.md) | RFC 4122 v4 UUID generator (native `randomUUID` + `getRandomValues` fallback) | +| [mdbid](mdbid/README.md) | MongoDB-compatible ObjectId generator via `bson-objectid` | +| [generateId](generateId/README.md) | Nanoid-based ID generators with configurable alphabets and sizes | diff --git a/src/common/utils/generateId/README.md b/src/common/utils/generateId/README.md new file mode 100644 index 0000000..6e03588 --- /dev/null +++ b/src/common/utils/generateId/README.md @@ -0,0 +1,33 @@ +# generateId + +Nanoid-based ID generators with configurable alphabets and sizes. Uses `nanoid` (v5) and `nanoid-dictionary` under the hood. All generators default to 21 characters and accept an optional `size` parameter. + +## API + +```ts +function generateId(size?: number): string; +function generateAlphaNumericId(size?: number): string; +function generateAlphaNumericLowerCaseId(size?: number): string; +function generateAlphaId(size?: number): string; +function generateAlphaLowerCaseId(size?: number): string; +function generateAlphaUpperCaseId(size?: number): string; +``` + +| Function | Alphabet | Example output | +| --------------------------------- | ------------------------ | ------------------------ | +| `generateId` | URL-safe (`A-Za-z0-9_-`) | `V1StGXR8_Z5jdHi6B-myT` | +| `generateAlphaNumericId` | `A-Z a-z 0-9` | `k3Bf9xQpWm7Yz2RtJhNcA` | +| `generateAlphaNumericLowerCaseId` | `a-z 0-9` | `m7k3xq9pw2yz5rtjh8nca` | +| `generateAlphaId` | `A-Z a-z` | `kBfxQpWmYzRtJhNcAeLsG` | +| `generateAlphaLowerCaseId` | `a-z` | `kbfxqpwmyzrtjhncaelsgd` | +| `generateAlphaUpperCaseId` | `A-Z` | `KBFXQPWMYZRTJHNCAELSGD` | + +## Usage + +```ts +import { generateId, generateAlphaNumericLowerCaseId } from "@webiny/stdlib"; + +const id = generateId(); // default 21 chars, URL-safe +const short = generateId(10); // custom size +const slug = generateAlphaNumericLowerCaseId(12); // lowercase alphanumeric +``` diff --git a/src/common/utils/generateId/generateId.ts b/src/common/utils/generateId/generateId.ts new file mode 100644 index 0000000..48a3c46 --- /dev/null +++ b/src/common/utils/generateId/generateId.ts @@ -0,0 +1,21 @@ +import { customAlphabet, nanoid } from "nanoid"; +import { alphanumeric, lowercase, numbers, uppercase } from "nanoid-dictionary"; + +const DEFAULT_SIZE = 21; + +export const generateAlphaNumericId = customAlphabet(alphanumeric, DEFAULT_SIZE); + +export const generateAlphaNumericLowerCaseId = customAlphabet( + `${lowercase}${numbers}`, + DEFAULT_SIZE +); + +export const generateAlphaId = customAlphabet(`${lowercase}${uppercase}`, DEFAULT_SIZE); + +export const generateAlphaLowerCaseId = customAlphabet(lowercase, DEFAULT_SIZE); + +export const generateAlphaUpperCaseId = customAlphabet(uppercase, DEFAULT_SIZE); + +export const generateId = (size = DEFAULT_SIZE): string => { + return nanoid(size); +}; diff --git a/src/common/utils/generateId/index.ts b/src/common/utils/generateId/index.ts new file mode 100644 index 0000000..4e8e3a1 --- /dev/null +++ b/src/common/utils/generateId/index.ts @@ -0,0 +1,8 @@ +export { + generateAlphaNumericId, + generateAlphaNumericLowerCaseId, + generateAlphaId, + generateAlphaLowerCaseId, + generateAlphaUpperCaseId, + generateId +} from "./generateId.js"; diff --git a/yarn.lock b/yarn.lock index a6564e8..2c6da03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1261,6 +1261,8 @@ __metadata: dot-prop: "npm:^10.1.0" fast-glob: "npm:^3.3.3" happy-dom: "npm:^20.10.1" + nanoid: "npm:^5.1.11" + nanoid-dictionary: "npm:^5.0.0" oxfmt: "npm:^0.53.0" oxlint: "npm:^1.68.0" pino: "npm:^10.3.1" @@ -2215,6 +2217,13 @@ __metadata: languageName: node linkType: hard +"nanoid-dictionary@npm:^5.0.0": + version: 5.0.0 + resolution: "nanoid-dictionary@npm:5.0.0" + checksum: 10c0/b35de04523c0932b542254f28e4902e25f1fdfff3d46fc5a582f71594cc0cdaff2eb610a3f04f933da87c5c5f55301b7e9011b22329de3c98066ed601f5b4e2b + languageName: node + linkType: hard + "nanoid@npm:^3.3.12": version: 3.3.12 resolution: "nanoid@npm:3.3.12" @@ -2224,6 +2233,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^5.1.11": + version: 5.1.11 + resolution: "nanoid@npm:5.1.11" + bin: + nanoid: bin/nanoid.js + checksum: 10c0/91580d18c29263ac0e871734f0d86e7f906f523f974d3c30fc65354ccf387ccffd606c2a6c28acc2977a3950146347e790ce9e3f514133a48995af5ccdb308ce + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 12.4.0 resolution: "node-gyp@npm:12.4.0"