feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
export * from "./use-debounced-callback";
export * from "./use-unmount";
export * from "./use-date-groups";

View File

@@ -0,0 +1,96 @@
import { useMemo } from "react";
interface ItemWithDate {
createdAt: Date | string | null;
}
interface DateGroup<T> {
label: string;
items: T[];
}
const isToday = (date: Date): boolean => {
const today = new Date();
return (
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear()
);
};
const isYesterday = (date: Date): boolean => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return (
date.getDate() === yesterday.getDate() &&
date.getMonth() === yesterday.getMonth() &&
date.getFullYear() === yesterday.getFullYear()
);
};
const isThisWeek = (date: Date): boolean => {
const now = new Date();
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - now.getDay());
weekStart.setHours(0, 0, 0, 0);
return date >= weekStart && !isToday(date) && !isYesterday(date);
};
const isThisMonth = (date: Date): boolean => {
const now = new Date();
return (
date.getMonth() === now.getMonth() &&
date.getFullYear() === now.getFullYear() &&
!isThisWeek(date) &&
!isToday(date) &&
!isYesterday(date)
);
};
/**
* Groups items by date ranges: Today, Yesterday, This week, This month, Older
* Items must have a `createdAt` property (Date or string)
*/
export function useDateGroups<T extends ItemWithDate>(
items: T[],
): DateGroup<T>[] {
return useMemo(() => {
const today: T[] = [];
const yesterday: T[] = [];
const thisWeek: T[] = [];
const thisMonth: T[] = [];
const older: T[] = [];
for (const item of items) {
if (item.createdAt === null) {
older.push(item);
continue;
}
const date =
typeof item.createdAt === "string"
? new Date(item.createdAt)
: item.createdAt;
if (isToday(date)) {
today.push(item);
} else if (isYesterday(date)) {
yesterday.push(item);
} else if (isThisWeek(date)) {
thisWeek.push(item);
} else if (isThisMonth(date)) {
thisMonth.push(item);
} else {
older.push(item);
}
}
return [
{ label: "Today", items: today },
{ label: "Yesterday", items: yesterday },
{ label: "This week", items: thisWeek },
{ label: "This month", items: thisMonth },
{ label: "Older", items: older },
];
}, [items]);
}

View File

@@ -0,0 +1,63 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import debounce from "lodash/debounce";
import { useEffect, useMemo, useRef } from "react";
import { useUnmount } from "./use-unmount";
interface DebounceOptions {
leading?: boolean;
trailing?: boolean;
maxWait?: number;
}
interface ControlFunctions {
cancel: () => void;
flush: () => void;
isPending: () => boolean;
}
export type DebouncedState<T extends (...args: any[]) => ReturnType<T>> = ((
...args: Parameters<T>
) => ReturnType<T> | undefined) &
ControlFunctions;
export function useDebounceCallback<
T extends (...args: any[]) => ReturnType<T>,
>(func: T, delay = 500, options?: DebounceOptions): DebouncedState<T> {
const debouncedFunc = useRef<ReturnType<typeof debounce>>(null);
useUnmount(() => {
if (debouncedFunc.current) {
debouncedFunc.current.cancel();
}
});
const debounced = useMemo(() => {
const debouncedFuncInstance = debounce(func, delay, options);
const wrappedFunc: DebouncedState<T> = (...args: Parameters<T>) => {
return debouncedFuncInstance(...args);
};
wrappedFunc.cancel = () => {
debouncedFuncInstance.cancel();
};
wrappedFunc.isPending = () => {
return !!debouncedFunc.current;
};
wrappedFunc.flush = () => {
return debouncedFuncInstance.flush();
};
return wrappedFunc;
}, [func, delay, options]);
// Update the debounced function ref whenever func, wait, or options change
useEffect(() => {
debouncedFunc.current = debounce(func, delay, options);
}, [func, delay, options]);
return debounced;
}

View File

@@ -0,0 +1,14 @@
import { useEffect, useRef } from "react";
export function useUnmount(func: () => void) {
const funcRef = useRef(func);
funcRef.current = func;
useEffect(
() => () => {
funcRef.current();
},
[],
);
}