fix: Use a better ID format

This commit is contained in:
Wanjohi
2025-10-27 08:25:31 +03:00
parent 3d3a303eb9
commit 2dbfb02c52
7 changed files with 126 additions and 36 deletions

View File

@@ -1,8 +1,8 @@
import { prefixes } from "./utils/id.js";
import { Identifier } from "./id/index.js";
export namespace Examples {
export const Id = (prefix: keyof typeof prefixes) =>
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
export const Id = (prefix: keyof typeof Identifier.prefixes) =>
`${Identifier.prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
export const User = {
id: Id("user"),

View File

@@ -0,0 +1,89 @@
import { z } from "zod/v4";
import { randomBytes } from "crypto";
export namespace Identifier {
export const prefixes = {
app: "app",
user: "usr",
steam: "stm",
member: "mbr",
session: "ses",
apiSecret: "sec",
apiPersonal: "pat",
organisation: "org",
} as const;
export function schema(prefix: keyof typeof prefixes) {
return z.string().startsWith(prefixes[prefix]);
}
const LENGTH = 26;
// State for monotonic ID generation
let lastTimestamp = 0;
let counter = 0;
export function ascending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, false, given);
}
export function descending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, true, given);
}
function generateID(
prefix: keyof typeof prefixes,
descending: boolean,
given?: string,
): string {
if (!given) {
return create(prefix, descending);
}
if (!given.startsWith(prefixes[prefix])) {
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`);
}
return given;
}
function randomBase62(length: number): string {
const chars =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
let result = "";
const bytes = randomBytes(length);
for (let i = 0; i < length; i++) {
result += chars[bytes[i]! % 62];
}
return result;
}
export function create(
prefix: keyof typeof prefixes,
descending: boolean,
timestamp?: number,
): string {
const currentTimestamp = timestamp ?? Date.now();
if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp;
counter = 0;
}
counter++;
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter);
now = descending ? ~now : now;
const timeBytes = Buffer.alloc(6);
for (let i = 0; i < 6; i++) {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff));
}
return (
prefixes[prefix] +
"_" +
timeBytes.toString("hex") +
randomBase62(LENGTH - 12)
);
}
}

View File

@@ -1,10 +1,11 @@
import { z } from "zod/v4";
import { Examples } from "../examples.js";
import { Identifier } from "../id/index.js";
export namespace Organisation {
export const Info = z
.object({
id: z.string().meta({
id: Identifier.schema("organisation").meta({
description: "The unique identifier of the organisation",
example: Examples.Organisation.id,
}),

View File

@@ -1,15 +1,16 @@
import { z } from "zod/v4";
import { Examples } from "../examples.js";
import { Limitations, steamTable } from "./steam.sql.js";
import { fn } from "../utils/fn.js";
import { Actor } from "../actor.js";
import { Database } from "../drizzle/index.js";
import { Examples } from "../examples.js";
import { Identifier } from "../id/index.js";
import { eq, isNull, and } from "drizzle-orm";
import { Database } from "../drizzle/index.js";
import { Limitations, steamTable } from "./steam.sql.js";
export namespace Steam {
export const Info = z
.object({
id: z.string().meta({
id: Identifier.schema("steam").meta({
description: "The unique Steam ID of the account",
example: Examples.SteamAccount.id,
}),

View File

@@ -1,16 +1,16 @@
import { z } from "zod/v4";
import { fn } from "../utils/fn.js";
import { userTable } from "./user.sql.js";
import { createID } from "../utils/id.js";
import { Examples } from "../examples.js";
import { Polar } from "../polar/index.js";
import { Identifier } from "../id/index.js";
import { Database } from "../drizzle/index.js";
import { eq, and, isNotNull } from "drizzle-orm";
export namespace User {
export const Info = z
.object({
id: z.string().meta({
id: Identifier.schema("user").meta({
description: "The unique identifier of the user",
example: Examples.User.id,
}),
@@ -38,7 +38,7 @@ export namespace User {
export const create = fn(
Info.omit({ customerID: true }).partial({ id: true }),
async (input) => {
const id = input.id ?? createID("user");
const id = Identifier.ascending("user", input.id);
const customerID = await Polar.fromUserEmail(input.email).then(
(z) => z.id,

View File

@@ -1,25 +0,0 @@
import { ulid } from "ulid";
export const prefixes = {
user: "usr",
steam: "stm",
app: "app",
member: "mbr",
apiSecret: "sec",
apiPersonal: "pat",
organisation: "org",
} as const;
/**
* Generates a unique identifier by concatenating a predefined prefix with a ULID.
*
* Given a key from the predefined prefixes mapping (e.g.,"user", "member", "steam"),
* this function retrieves the corresponding prefix and combines it with a ULID using an underscore
* as a separator. The resulting identifier is formatted as "prefix_ulid".
*
* @param prefix - A key from the prefixes mapping.
* @returns A unique identifier string.
*/
export function createID(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], ulid()].join("_");
}