import * as Atom from "effect/unstable/reactivity/Atom";import { AdminApiClient } from "./admin-client";import { ReactivityKey } from "./reactivity-keys";// ---------------------------------------------------------------------------// Admin atoms — the tenant-wide users view behind `/admin/users*`.//// Read-only by construction: the admin plane serves GETs only (the owner policy// rejects writes at tenant reach), so there are no mutations here and every// atom carries the same reactivity key.//// Paging and the search term are part of the atom identity, so each page of// each search is its own cache entry and stepping back to a visited page is// instant. `Atom.family` (not a bare arrow) because the page component// re-derives the key object on every render — a fresh atom per render would// refetch in a loop.// ---------------------------------------------------------------------------/** How many users one page of the list shows. Well inside the contract's * 1..500 bound; the joined endpoint reads per-user connections, so a modest * page keeps that join cheap. */export const ADMIN_USERS_PAGE_SIZE = 25;export interface AdminUsersPage { readonly limit: number; readonly offset: number; /** The `?search=` term (name or email substring), already debounced by the * page. `""` is no filter and is sent as no param at all, so the unfiltered * list keeps one cache identity regardless of how the term was cleared. */ readonly search: string;}/** * One page of users joined with their connections — what the list renders. * * Asks for one row MORE than the page size so the pager can tell "there is a * next page" from "this is the last one" without a count endpoint (the contract * returns no total). The page component drops the extra row. */export const adminUsersWithConnectionsAtom = Atom.family((page: AdminUsersPage) => AdminApiClient.query("adminUsers", "listUsersWithConnections", { query: { limit: page.limit + 1, offset: page.offset, ...(page.search === "" ? {} : { search: page.search }), }, timeToLive: "30 seconds", reactivityKeys: [ReactivityKey.adminUsers], }),);/** One user's connections, for the detail pane. A separate read rather than a * slice of the list so opening a user is correct even when the list page it * came from has since gone stale. */export const adminUserConnectionsAtom = Atom.family((externalId: string) => AdminApiClient.query("adminUsers", "listUserConnections", { params: { externalId }, timeToLive: "30 seconds", reactivityKeys: [ReactivityKey.adminUsers], }),);
import * as Atom from "effect/unstable/reactivity/Atom";
import { AdminApiClient } from "./admin-client";
import { ReactivityKey } from "./reactivity-keys";
// ---------------------------------------------------------------------------
// Admin atoms — the tenant-wide users view behind `/admin/users*`.
//
// Read-only by construction: the admin plane serves GETs only (the owner policy
// rejects writes at tenant reach), so there are no mutations here and every
// atom carries the same reactivity key.
//
// Paging and the search term are part of the atom identity, so each page of
// each search is its own cache entry and stepping back to a visited page is
// instant. `Atom.family` (not a bare arrow) because the page component
// re-derives the key object on every render — a fresh atom per render would
// refetch in a loop.
// ---------------------------------------------------------------------------
/** How many users one page of the list shows. Well inside the contract's
* 1..500 bound; the joined endpoint reads per-user connections, so a modest
* page keeps that join cheap. */
export const ADMIN_USERS_PAGE_SIZE = 25;
export interface AdminUsersPage {
readonly limit: number;
readonly offset: number;
/** The `?search=` term (name or email substring), already debounced by the
* page. `""` is no filter and is sent as no param at all, so the unfiltered
* list keeps one cache identity regardless of how the term was cleared. */
readonly search: string;
}
/**
* One page of users joined with their connections — what the list renders.
*
* Asks for one row MORE than the page size so the pager can tell "there is a
* next page" from "this is the last one" without a count endpoint (the contract
* returns no total). The page component drops the extra row.
*/
export const adminUsersWithConnectionsAtom = Atom.family((page: AdminUsersPage) =>
AdminApiClient.query("adminUsers", "listUsersWithConnections", {
query: {
limit: page.limit + 1,
offset: page.offset,
...(page.search === "" ? {} : { search: page.search }),
},
timeToLive: "30 seconds",
reactivityKeys: [ReactivityKey.adminUsers],
}),
);
/** One user's connections, for the detail pane. A separate read rather than a
* slice of the list so opening a user is correct even when the list page it
* came from has since gone stale. */
export const adminUserConnectionsAtom = Atom.family((externalId: string) =>
AdminApiClient.query("adminUsers", "listUserConnections", {
params: { externalId },
timeToLive: "30 seconds",
reactivityKeys: [ReactivityKey.adminUsers],
}),
);