Files
landing-ply/kits/clickup/src/session.ts
T

160 lines
4.9 KiB
TypeScript

import type { Ref } from "./core";
import { asRef, idOf, LiveError, pathOf } from "./core";
import type { Hierarchy, HierarchyReads } from "./hierarchy/read";
import { createHierarchyReads, HIERARCHY_KINDS } from "./hierarchy/read";
import type { LiveBridge } from "./host/bridge";
import { memo } from "./memo";
import type { Team, User } from "./people/people";
import type { Members } from "./people/read";
import { loadMembers } from "./people/read";
import type { Store } from "./store";
import { loadTaskTypes } from "./tasks/taskTypes";
import type { TaskType } from "./tasks/tasks";
/** The ids of the space, folder and list an asset sits in. */
export interface LocationIds {
spaceId?: string;
folderId?: string;
listId?: string;
}
type LocationRef = Ref<"space" | "folder" | "list">;
/**
* One connection to a host: its bridge, its query store, and the bundles
* read once and shared by every query, until a refetch or an invalidation
* of one of their kinds drops them.
*/
export interface Session {
host: LiveBridge;
store: Store;
hierarchy: HierarchyReads;
/** The members bundle; rejects when the grant is withheld. */
members(): Promise<Members>;
/** The workspace member with `id`, if any. */
member(id: string): Promise<User | undefined>;
/** The workspace team with `id`, if any. */
team(id: string): Promise<Team | undefined>;
/** The task type catalog; rejects when the grant is withheld. */
taskTypes(): Promise<TaskType[]>;
/**
* The refs of a location's ids, root first, as far as its space's tree
* lists them; a level the tree does not list (hidden, inaccessible,
* archived, or no hierarchy grant) is left out.
*/
path(ids: LocationIds): Promise<LocationRef[]>;
}
export function createSession(host: LiveBridge, store: Store): Session {
const hierarchy = createHierarchyReads(host);
const members = memo(() => loadMembers(host));
const users = memo(
async () => new Map((await members()).users.map((user) => [user.id, user])),
);
const teams = memo(
async () => new Map((await members()).teams.map((team) => [team.id, team])),
);
const taskTypes = memo(() => loadTaskTypes(host));
store.onRefresh(HIERARCHY_KINDS, hierarchy.reset);
store.onRefresh(["user", "team"], () => {
members.reset();
users.reset();
teams.reset();
});
store.onRefresh(["taskType"], taskTypes.reset);
return {
host,
store,
hierarchy,
members,
async member(id) {
const known = await users().catch(() => undefined);
return known?.get(id);
},
async team(id) {
const known = await teams().catch(() => undefined);
return known?.get(id);
},
taskTypes,
async path({ spaceId, folderId, listId }) {
if (!spaceId) {
return [];
}
const tree = await hierarchy.tree(spaceId).catch(() => undefined);
if (!tree) {
return [];
}
const deepest: LocationRef | undefined =
tree.lists.find((list) => list.id === listId) ??
tree.folders.find((folder) => folder.id === folderId) ??
tree.spaces.find((space) => space.id === spaceId);
return deepest
? [...(pathOf(deepest) as LocationRef[]), asRef(deepest)]
: [];
},
};
}
/** The full asset behind a ref or id: the copy on screen, else one read through the kind's `get`. */
export async function resolve<T extends Ref>(
s: Session,
kind: T["kind"],
target: Ref | string,
get: (s: Session, ids: string[]) => Promise<T[]>,
): Promise<T> {
const id = idOf(target);
const held = s.store.find<T>(kind, id);
if (held) {
return held;
}
const [found] = await get(s, [id]);
if (!found) {
throw new LiveError(
"invalid",
`No ${kind} with id ${id}, or it is not visible to you.`,
);
}
return found;
}
/** The `ancestor` or `parent` a selector names; its schema guarantees one. */
export function scopeOf<P, A>(selector: {
parent?: P;
ancestor?: A;
}): NonNullable<A | P> {
return (selector.ancestor ?? selector.parent)!;
}
/** The tree of the space a space or folder ref sits in. */
export async function treeOf(
s: Session,
ref: Ref<"space" | "folder">,
): Promise<Hierarchy> {
return s.hierarchy.tree(await spaceIdOf(s, ref));
}
/** The space a ref sits in: from its path when it is a full asset, else looked up by id. */
async function spaceIdOf(
s: Session,
ref: Ref<"space" | "folder">,
): Promise<string> {
if (ref.kind === "space") {
return ref.id;
}
const known = pathOf(ref).find((ancestor) => ancestor.kind === "space");
if (known) {
return known.id;
}
const { folders } = await s.hierarchy.lookup("folder", [ref.id]);
const folder = folders.find((candidate) => candidate.id === ref.id);
const space = folder?.ancestors.find((ancestor) => ancestor.kind === "space");
if (!space) {
throw new LiveError(
"invalid",
`No folder with id ${ref.id}, or it is not visible to you.`,
);
}
return space.id;
}