@jqgl/humanid
v0.0.1
Published
Downloads
165
Readme
HUMANID
the central source of truth for your identifiers
Why?
Typescript is a structural type system. In the end you have strings everywhere.
With branded ids:
- Mistakes are visible, no more id inversion in parameters
// before 😢
function unsubscribe(userId: string, subscriptionId: string): Promise<void>;
// after 🥳
function unsubscribe(userId: Id<"UserId">, subscriptionId: Id<"SubscriptionId">): Promise<void>;- Collections can finally express your intent
// before 😢
function findUsersSubscriptions(userIds: Set<string>): Map<string, { id: string }[]>;
// after 🥳
function findUsersSubscriptions(
userIds: Set<Id<"UserId">>,
): Map<Id<"UserId">, Id<"SubscriptionId">[]>;With a central id registry:
- It becomes impossible to create duplicated Ids by design
// typescript will prevent this by default
const ids = defineIds((h) => ({ user: h.uuid(), user: h.uuid() }));
// Brands are built by convention
// `UserId` + `Emails/UserId` + `Auth/UserId`
const ids = defineIds((h) => ({
user: h.uuid(),
emails: { user: h.uuid() },
auth: { user: h.uuid() },
}));How to use
// = ids.ts =
import { defineIds } from "humanid";
export const ids = defineIds((h) => ({
// Each entry will be unique by construction,
// and have a type alias: `UserId`, `EmailId`, `FileId`
user: h.uuid(),
email: h.uuid(),
file: h.uuid(),
}));
declare module "humanid" {
interface Registry {
/**
* The declaration merging typing
* `Id<...>` in a safe way.
* Otherwise, it accepts any string
*/
ids: typeof ids;
}
}
// = user.repository.ts =
import type { Id } from "humanid";
import { db } from "@/db.js";
type User = { id: Id<"UserId">; name: string };
// the Id type will only allow the registered Id
export async function findById(id: Id<"UserId">): Promise<User> {
const user = await db.query.findFirst({
where: { id }, // id is still a string
columns: { id: true, name: true },
});
if (!user) throw new Error();
return { id: ids.user(user.id), name: user.name };
}Namespacing
It's possible to organize your ids in namespaces (⚠️ no more than 10 level deep):
const ids = defineIds((h) => ({
users: { user: h.uuid(), subscription: h.uuid(), session: h.uuid() },
emails: { email: h.uuid(), attachment: h.uuid(), participant: h.uuid() },
}));Id formats
UUID
uses crypto.randomUUID() under the hood:
const ids = defineIds((h) => ({ user: h.uuid() }));
const userId = ids.user();
// ^ bf54ca8e-a3bf-4cc0-b4af-2f8e89faa1e6Uint8Array Buffer
uses crypto.getRandomValues with a Uint8Array under the hood.
const ids = defineIds((h) => ({ user: h.buffer() }));
const userId = ids.user();
// ^ 5h4o411hu2osn5g6wyou can configure the size of the buffer, and the radix for int-to-string conversion
const ids = defineIds((h) => ({ user: h.buffer({ size: 24, radix: 16 }) }));
const userId = ids.user();
// ^ 91d91f767e3ea0efd596d5682cfc4bfd92dcf9fda8efaPrefixed
you can prefix your id with a string, to build an ID à la Stripe.
const ids = defineIds((h) => ({ user: h.prefixed({ prefix: "usr", suffix: h.buffer() }) }));
const userId = ids.user();
// ^ usr_g4332685v5p423u3119Custom
you can use any synchronous function that returns a string
let i = 10_000n;
const ids = defineIds((h) => ({ user: h.custom(() => `usr_${++i}`) }));
const userId = ids.user();
// ^ usr_10001Branding conventions
We follow a convention to build each Id Branding, with some assumptions:
- Key should not use the
idsuffix, we add it automatically. If your key uses...Idthe brand will repeat it...IdId.
const ids = defineIds((h) => ({ userId: h.uuid() }));
// ^ Ids<'UserIdId'>- Keys should be valid JS class names. Each generated Brand will look like a class name by design.
const ids = defineIds((h) => ({ user: h.uuid() }));
// ^ Id<'UserId'> and not Id<'user'> or Id<'userId'>- Namespaces are separated by
/
const ids = defineIds((h) => ({ users: { subscription: h.uuid() } }));
// ^ Id<'Users/SubscriptionId'>