Initial Commit
This commit is contained in:
2
packages/foundation/data/bytes/src/uuid/index.ts
Normal file
2
packages/foundation/data/bytes/src/uuid/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { generateUuid, uint8ArrayToUuid, uuidToUint8Array } from "./uuid.js";
|
||||
export { UuidStringSchema } from "./uuid.schemas.js";
|
||||
97
packages/foundation/data/bytes/src/uuid/uuid.md
Normal file
97
packages/foundation/data/bytes/src/uuid/uuid.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# uuid
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `uuid` module provides three focused primitives for working with Universally Unique Identifiers in `@suaveplan/bytes`: generating cryptographically random UUIDv4 strings, converting canonical UUID strings to their compact 16-byte binary representation, and converting a 16-byte `Uint8Array` back to the canonical hyphen-separated string. These operations are essential for any system that stores entity identifiers as binary blobs in databases, network protocols, or binary serialisation formats while presenting them as human-readable strings at the API boundary.
|
||||
|
||||
All three functions validate their inputs strictly and throw typed `BytesError` values on malformed data, making them safe to use directly in request handlers and protocol parsers.
|
||||
|
||||
## Features
|
||||
|
||||
- **`generateUuid`** — generates a UUIDv4 string using the platform `crypto.randomUUID()` — no third-party uuid library, no custom random-number manipulation.
|
||||
- **`uuidToUint8Array`** — parses a canonical UUID string into exactly 16 bytes; validates format with a regex before any byte parsing.
|
||||
- **`uint8ArrayToUuid`** — formats a 16-byte `Uint8Array` as a canonical lowercase UUID string; validates length before formatting.
|
||||
- **OTel tracing** — every function is wrapped in a named active span under the `@suaveplan/bytes` tracer.
|
||||
- **Zod schema** — `UuidStringSchema` validates the canonical UUID format and is composable with any Zod object schema.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateUuid, uuidToUint8Array, uint8ArrayToUuid } from "@suaveplan/bytes/uuid";
|
||||
|
||||
// Generate a fresh UUIDv4
|
||||
const id = generateUuid();
|
||||
// → "550e8400-e29b-41d4-a716-446655440000" (random each call)
|
||||
|
||||
// Convert to binary for compact storage
|
||||
const bytes = uuidToUint8Array(id);
|
||||
// → Uint8Array of length 16
|
||||
|
||||
// Restore the canonical string
|
||||
const restored = uint8ArrayToUuid(bytes);
|
||||
// → same as `id`
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { uuidToUint8Array, uint8ArrayToUuid, BytesError } from "@suaveplan/bytes";
|
||||
import { UuidStringSchema } from "@suaveplan/bytes/uuid";
|
||||
import z from "zod";
|
||||
|
||||
// Validate and convert an incoming UUID header to binary for DB storage
|
||||
const RequestSchema = z.object({
|
||||
entityId: UuidStringSchema,
|
||||
});
|
||||
|
||||
function handleRequest(raw: unknown): Uint8Array {
|
||||
const { entityId } = RequestSchema.parse(raw);
|
||||
try {
|
||||
return uuidToUint8Array(entityId);
|
||||
} catch (err) {
|
||||
if (err instanceof BytesError) {
|
||||
// err.code === "BYTES.UUID.INVALID_FORMAT"
|
||||
throw new Error(`Malformed entity ID: ${err.message}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to string for API responses
|
||||
function toApiResponse(raw: Uint8Array): string {
|
||||
return uint8ArrayToUuid(raw);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `generateUuid(): string`
|
||||
|
||||
Generate a UUID version 4 string using `crypto.randomUUID()`. Output is always lowercase and matches the pattern `/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/`.
|
||||
|
||||
### `uuidToUint8Array(uuid: string): Uint8Array`
|
||||
|
||||
Parse a canonical UUID string (upper- or lowercase) into a 16-byte `Uint8Array`. Strips the four hyphens, then reads each pair of hex digits as a byte.
|
||||
|
||||
**Throws** `BytesError(BYTES.UUID.INVALID_FORMAT)` when `uuid` does not match the canonical pattern `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
|
||||
|
||||
### `uint8ArrayToUuid(bytes: Uint8Array): string`
|
||||
|
||||
Format a 16-byte `Uint8Array` as a canonical lowercase UUID string in the form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
|
||||
|
||||
**Throws** `BytesError(BYTES.UUID.INVALID_LENGTH)` when `bytes.length !== 16`.
|
||||
|
||||
### `UuidStringSchema` (from `uuid.schemas.ts`)
|
||||
|
||||
Zod schema that validates the canonical UUID format. Accepts both upper- and lowercase hex digits. Does not verify UUID version or variant bits.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`generateUuid` delegates entirely to the platform `crypto.randomUUID()` (Node 14.17+, Bun, all modern browsers), ensuring the UUID is generated from a cryptographically secure source of randomness with no custom bit-twiddling. A polyfill that sets `globalThis.crypto = require("node:crypto").webcrypto` is included in the package test setup so that Node `forks`-pool Vitest runs have access to the `crypto` global.
|
||||
|
||||
`uuidToUint8Array` validates the input with a compiled regex before any byte parsing. This rejects inputs that are structurally wrong (wrong group lengths, non-hex chars, missing hyphens) with a single check rather than per-byte error detection.
|
||||
|
||||
`uint8ArrayToUuid` materialises a hex string from the byte array, then splices in the four hyphens at the standard offsets (8, 12, 16, 20 hex chars). Output is always lowercase because `Number.prototype.toString(16)` produces lowercase hex on all platforms.
|
||||
15
packages/foundation/data/bytes/src/uuid/uuid.schemas.ts
Normal file
15
packages/foundation/data/bytes/src/uuid/uuid.schemas.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import z from "zod";
|
||||
|
||||
/**
|
||||
* Zod schema for a canonical UUID string.
|
||||
*
|
||||
* Validates the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` where each `x`
|
||||
* is a hexadecimal digit (0–9, a–f, A–F). Does not verify the version or
|
||||
* variant bits — use `generateUuid` to obtain a guaranteed UUIDv4.
|
||||
*/
|
||||
export const UuidStringSchema = z
|
||||
.string()
|
||||
.regex(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
"Must be a valid UUID string in canonical form"
|
||||
);
|
||||
70
packages/foundation/data/bytes/src/uuid/uuid.test.ts
Normal file
70
packages/foundation/data/bytes/src/uuid/uuid.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { generateUuid, uint8ArrayToUuid, uuidToUint8Array } from "./uuid.js";
|
||||
|
||||
describe("uuid round-trip", () => {
|
||||
it("round-trips a known UUID", { timeout: 5000 }, () => {
|
||||
const uuid = "550e8400-e29b-41d4-a716-446655440000";
|
||||
expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid);
|
||||
});
|
||||
|
||||
it("uuidToUint8Array produces 16 bytes", { timeout: 5000 }, () => {
|
||||
expect(
|
||||
uuidToUint8Array("550e8400-e29b-41d4-a716-446655440000")
|
||||
).toHaveLength(16);
|
||||
});
|
||||
|
||||
it("throws on invalid UUID", { timeout: 5000 }, () => {
|
||||
expect(() => uuidToUint8Array("not-a-uuid")).toThrow();
|
||||
});
|
||||
|
||||
it("throws on UUID with wrong structure", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
uuidToUint8Array("550e8400e29b41d4a716446655440000")
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("uint8ArrayToUuid throws on non-16-byte array", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => uint8ArrayToUuid(new Uint8Array(15))).toThrow();
|
||||
});
|
||||
|
||||
it("uint8ArrayToUuid throws on empty array", { timeout: 5000 }, () => {
|
||||
expect(() => uint8ArrayToUuid(new Uint8Array(0))).toThrow();
|
||||
});
|
||||
|
||||
it("round-trips all-zeros UUID", { timeout: 5000 }, () => {
|
||||
const uuid = "00000000-0000-0000-0000-000000000000";
|
||||
expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid);
|
||||
});
|
||||
|
||||
it("round-trips all-fs UUID", { timeout: 5000 }, () => {
|
||||
const uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff";
|
||||
expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateUuid", () => {
|
||||
it("generates a valid UUID v4 string", { timeout: 5000 }, () => {
|
||||
const uuid = generateUuid();
|
||||
expect(uuid).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
||||
);
|
||||
});
|
||||
|
||||
it("generates unique values", { timeout: 5000 }, () => {
|
||||
expect(generateUuid()).not.toBe(generateUuid());
|
||||
});
|
||||
|
||||
it("output is lowercase", { timeout: 5000 }, () => {
|
||||
const uuid = generateUuid();
|
||||
expect(uuid).toBe(uuid.toLowerCase());
|
||||
});
|
||||
|
||||
it("generated UUID can round-trip through uint8Array", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const uuid = generateUuid();
|
||||
expect(uint8ArrayToUuid(uuidToUint8Array(uuid))).toBe(uuid);
|
||||
});
|
||||
});
|
||||
87
packages/foundation/data/bytes/src/uuid/uuid.ts
Normal file
87
packages/foundation/data/bytes/src/uuid/uuid.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import { BYTES_CODES, BytesError } from "../errors/bytes-error.js";
|
||||
|
||||
const tracer = trace.getTracer("@suaveplan/bytes");
|
||||
|
||||
/** UUID canonical format: 8-4-4-4-12 lowercase hex groups separated by hyphens. */
|
||||
const UUID_RE =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
/**
|
||||
* Generate a UUID v4 string using the platform `crypto.randomUUID()`.
|
||||
*
|
||||
* The output is always lowercase and conforms to the canonical
|
||||
* `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` format.
|
||||
*/
|
||||
export function generateUuid(): string {
|
||||
return tracer.startActiveSpan("bytes.uuid.generateUuid", (span) => {
|
||||
try {
|
||||
return crypto.randomUUID();
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a canonical UUID string into a 16-byte `Uint8Array`.
|
||||
*
|
||||
* Accepts both upper- and lowercase hex. Dashes are stripped before parsing.
|
||||
*
|
||||
* @throws `BytesError(BYTES.UUID.INVALID_FORMAT)` when `uuid` does not
|
||||
* match the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` pattern.
|
||||
*/
|
||||
export function uuidToUint8Array(uuid: string): Uint8Array {
|
||||
return tracer.startActiveSpan("bytes.uuid.uuidToUint8Array", (span) => {
|
||||
try {
|
||||
if (!UUID_RE.test(uuid)) {
|
||||
throw new BytesError(
|
||||
BYTES_CODES["UUID.INVALID_FORMAT"],
|
||||
`Invalid UUID format: "${uuid}". Expected canonical form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.`
|
||||
);
|
||||
}
|
||||
|
||||
const hex = uuid.replace(/-/g, "");
|
||||
const result = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
result[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a 16-byte `Uint8Array` as a canonical lowercase UUID string.
|
||||
*
|
||||
* @throws `BytesError(BYTES.UUID.INVALID_LENGTH)` when `bytes` is not
|
||||
* exactly 16 bytes long.
|
||||
*/
|
||||
export function uint8ArrayToUuid(bytes: Uint8Array): string {
|
||||
return tracer.startActiveSpan("bytes.uuid.uint8ArrayToUuid", (span) => {
|
||||
try {
|
||||
if (bytes.length !== 16) {
|
||||
throw new BytesError(
|
||||
BYTES_CODES["UUID.INVALID_LENGTH"],
|
||||
`Invalid UUID byte array length: ${bytes.length}. Expected exactly 16 bytes.`
|
||||
);
|
||||
}
|
||||
|
||||
const hex = Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
return [
|
||||
hex.slice(0, 8),
|
||||
hex.slice(8, 12),
|
||||
hex.slice(12, 16),
|
||||
hex.slice(16, 20),
|
||||
hex.slice(20, 32),
|
||||
].join("-");
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user