mirror of
https://github.com/nestriness/nestri.git
synced 2026-08-01 17:34:15 +03:00
fix: Use a better ID format
This commit is contained in:
@@ -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"),
|
||||||
|
|||||||
89
packages/core/src/id/index.ts
Normal file
89
packages/core/src/id/index.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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("_");
|
|
||||||
}
|
|
||||||
24
packages/function/src/api/routes/session.ts
Normal file
24
packages/function/src/api/routes/session.ts
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user