feat(monorepo): migrate to typescript monorepo
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
/**
|
||||
* Generates a new API key with the format mid_{random_string}
|
||||
* @returns A new API key string
|
||||
*/
|
||||
export function generateApiKey(): string {
|
||||
// Generate 32 random bytes and convert to hex
|
||||
const randomString = randomBytes(32).toString("hex");
|
||||
return `basango_${randomString}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string is a valid API key format
|
||||
* @param key The key to validate
|
||||
* @returns True if the key starts with 'basango_' and has the correct length
|
||||
*/
|
||||
export function isValidApiKeyFormat(key: string): boolean {
|
||||
return key.startsWith("basango_") && key.length === 68; // basango_ (8) + 64 hex chars
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "@/client";
|
||||
|
||||
export async function checkHealth() {
|
||||
await db.execute(sql`SELECT 1`);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./api-keys";
|
||||
export * from "./health";
|
||||
export * from "./pagination";
|
||||
export * from "./search-query";
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
export type SortDirection = "asc" | "desc";
|
||||
|
||||
export interface PageRequest {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
cursor?: string | null;
|
||||
}
|
||||
|
||||
export interface PageState {
|
||||
page: number;
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface CursorPayload {
|
||||
id: string;
|
||||
date?: string | null;
|
||||
}
|
||||
|
||||
export interface PaginationMeta {
|
||||
current: number;
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
const DEFAULT_LIMIT = 5;
|
||||
const MAX_LIMIT = 100;
|
||||
|
||||
export function createPageState(request: PageRequest = {}): PageState {
|
||||
const page =
|
||||
Number.isFinite(request.page) && (request.page ?? 0) > 0
|
||||
? Math.trunc(request.page!)
|
||||
: DEFAULT_PAGE;
|
||||
|
||||
let limit =
|
||||
Number.isFinite(request.limit) && (request.limit ?? 0) > 0
|
||||
? Math.trunc(request.limit!)
|
||||
: DEFAULT_LIMIT;
|
||||
|
||||
if (limit < DEFAULT_LIMIT) {
|
||||
limit = DEFAULT_LIMIT;
|
||||
}
|
||||
|
||||
if (limit > MAX_LIMIT) {
|
||||
limit = MAX_LIMIT;
|
||||
}
|
||||
|
||||
const cursor = request.cursor ?? null;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
return { page, limit, cursor, offset };
|
||||
}
|
||||
|
||||
export function encodeCursor(
|
||||
row: Record<string, unknown>,
|
||||
keyset: { id: string; date?: string | null },
|
||||
): string {
|
||||
const payload: CursorPayload = {
|
||||
id: String(row[keyset.id] ?? ""),
|
||||
};
|
||||
|
||||
if (keyset.date) {
|
||||
const value = row[keyset.date];
|
||||
if (value !== undefined && value !== null) {
|
||||
payload.date = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
||||
}
|
||||
|
||||
export function decodeCursor(cursor?: string | null): CursorPayload | null {
|
||||
if (!cursor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(cursor, "base64").toString("utf8");
|
||||
const payload = JSON.parse(decoded) as CursorPayload;
|
||||
|
||||
if (!payload || typeof payload.id !== "string" || payload.id.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPaginationResult<T extends Record<string, unknown>>(
|
||||
rows: T[],
|
||||
page: PageState,
|
||||
keyset: { id: string; date?: string | null },
|
||||
): { data: T[]; pagination: PaginationMeta } {
|
||||
const hasNext = rows.length > page.limit;
|
||||
const data = hasNext ? rows.slice(0, page.limit) : rows;
|
||||
|
||||
let cursor: string | null = null;
|
||||
if (data.length > 0) {
|
||||
const lastRow = data[data.length - 1];
|
||||
cursor = encodeCursor(lastRow, keyset);
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
pagination: {
|
||||
current: page.page,
|
||||
limit: page.limit,
|
||||
cursor,
|
||||
hasNext,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export const buildSearchQuery = (input: string) => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return trimmed
|
||||
.split(/\s+/)
|
||||
.map((term) => {
|
||||
// Escape special characters for PostgreSQL full-text search
|
||||
// Special characters: & | ! ( ) : * ' " + - ~
|
||||
const escaped = term.toLowerCase().replace(/[&|!():*'"+~-]/g, "\\$&");
|
||||
return `${escaped}:*`;
|
||||
})
|
||||
.join(" & ");
|
||||
};
|
||||
Reference in New Issue
Block a user