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 namespace Examples {
export const Id = (prefix: keyof typeof prefixes) => export const Id = (prefix: keyof typeof Identifier.prefixes) =>
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`; `${Identifier.prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
export const User = { export const User = {
id: Id("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 { z } from "zod/v4";
import { Examples } from "../examples.js"; import { Examples } from "../examples.js";
import { Identifier } from "../id/index.js";
export namespace Organisation { export namespace Organisation {
export const Info = z export const Info = z
.object({ .object({
id: z.string().meta({ id: Identifier.schema("organisation").meta({
description: "The unique identifier of the organisation", description: "The unique identifier of the organisation",
example: Examples.Organisation.id, example: Examples.Organisation.id,
}), }),

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
import { Hono } from "hono";
import { describeRoute, resolver } from "hono-openapi";
export namespace SessionRoute {
export const route = new Hono().get(
"/",
describeRoute({
tags: ["Session"],
operationId: "session.list",
summary: "List all active sessions",
description: "List all the current user's active sessions",
responses: {
200:{
description:"List of sessions",
content:{
"application/json":{
schema: resolver(Session.Info.array())
}
}
}
},
}),
);
}