~james/core

core/web/src/lib/api/enrich.ts -rw-r--r-- 5.4 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import type { BobbinContext, XrpcRequestInit } from "#lib/api/client.ts";
import type { Did, Handle, Nsid } from "@atcute/lexicons/syntax";
import { INVALID_HANDLE, type MiniDoc } from "#lib/api/identity.ts";
import { jsonPost } from "#lib/api/_request.ts";
import { authorDidOf, rkeyFromUri } from "#lib/api/uri.ts";
import type * as OrgTangledRepoDefs from "#lib/api/lexicons/types/org/tangled/repo/defs.ts";
import type * as OrgTangledQueryEnrichResponse from "#lib/api/lexicons/types/org/tangled/query/enrichResponse.ts";

// payload types, also the keys payloads land under in the data sidecar
export const TYPE_COUNT = "org.tangled.query.enrichResponse#count";
export const TYPE_VIEWER = "org.tangled.query.enrichResponse#viewer";
export const TYPE_MINIDOC = "blue.microcosm.identity.resolveMiniDoc";
export const TYPE_REPO_VIEW = "org.tangled.repo.defs#repoViewBasic";
export const TYPE_LABELS = "org.tangled.query.enrichResponse#labels";
export const TYPE_REACTIONS = "org.tangled.query.enrichResponse#reactions";

export type PayloadType =
	| typeof TYPE_COUNT
	| typeof TYPE_VIEWER
	| typeof TYPE_MINIDOC
	| typeof TYPE_REPO_VIEW
	| typeof TYPE_LABELS
	| typeof TYPE_REACTIONS;

export type RecordPath = string & {};
export type EnvelopePath = ".repo" | ".collection" | ".rkey" | ".";
export type LinkSource = `${Nsid}:${RecordPath | EnvelopePath}`;

// a descriptor is also the key its payload reads back under, so the readers
// take the whole descriptor, a request and its reads can't drift apart
export interface LinkDescriptor<T extends PayloadType = PayloadType> {
	source: LinkSource;
	type: T;
	targets?: RecordPath[];
}

export type CountDescriptor = LinkDescriptor<typeof TYPE_COUNT>;
export type ViewerDescriptor = LinkDescriptor<typeof TYPE_VIEWER>;
export type DocDescriptor = LinkDescriptor<typeof TYPE_MINIDOC>;
export type RepoViewDescriptor = LinkDescriptor<typeof TYPE_REPO_VIEW>;
export type LabelsDescriptor = LinkDescriptor<typeof TYPE_LABELS>;
export type ReactionsDescriptor = LinkDescriptor<typeof TYPE_REACTIONS>;

// data[ref][source][type] = payload, payload shape depends on the type
export type Sidecar = Record<string, Record<string, Record<string, unknown>>>;

export interface Enriched<O> {
	output: O;
	data: Sidecar;
}

export interface EnrichRequest {
	xrpc: string;
	params?: Record<string, unknown>;
	enrich: LinkDescriptor[];
	viewer?: string;
}

export const target = <T extends PayloadType>(
	descriptor: LinkDescriptor<T>,
	targets: RecordPath[]
): LinkDescriptor<T> => ({ ...descriptor, targets });

export const targetAll = <T extends PayloadType>(
	descriptors: LinkDescriptor<T>[],
	targets: RecordPath[]
): LinkDescriptor<T>[] => descriptors.map((descriptor) => target(descriptor, targets));

// bobbin rejects viewer payloads with no viewer, so a logged-out request drops
// them here instead of at every call site
export const enrich = <O>(
	ctx: BobbinContext,
	req: EnrichRequest,
	init?: XrpcRequestInit
): Promise<Enriched<O>> =>
	jsonPost<Enriched<O>>(
		ctx,
		"org.tangled.query.enrichResponse",
		{
			...req,
			enrich: req.viewer
				? req.enrich
				: req.enrich.filter((descriptor) => descriptor.type !== TYPE_VIEWER)
		},
		init
	);

const payloadOf = (
	data: Sidecar,
	ref: string | undefined,
	{ source, type }: LinkDescriptor
): unknown => (ref !== undefined ? data[ref]?.[source]?.[type] : undefined);

export const countOf = (data: Sidecar, ref: string | undefined, of: CountDescriptor): number =>
	(payloadOf(data, ref, of) as number | undefined) ?? 0;

// undefined means no viewer or inapplicable descriptor, null means no matching record
export const viewerUriOf = (
	data: Sidecar,
	ref: string | undefined,
	of: ViewerDescriptor
): string | null | undefined => payloadOf(data, ref, of) as string | null | undefined;

export const viewerRkeyOf = (
	data: Sidecar,
	ref: string | undefined,
	of: ViewerDescriptor
): string | null | undefined => {
	const uri = viewerUriOf(data, ref, of);
	return uri ? rkeyFromUri(uri) : uri;
};

export const miniDocOf = (
	data: Sidecar,
	did: string | undefined,
	of: DocDescriptor
): MiniDoc | undefined => payloadOf(data, did, of) as MiniDoc | undefined;

export const repoViewOf = (
	data: Sidecar,
	ref: string | undefined,
	of: RepoViewDescriptor
): OrgTangledRepoDefs.RepoViewBasic | undefined =>
	payloadOf(data, ref, of) as OrgTangledRepoDefs.RepoViewBasic | undefined;

export const handleOf = (data: Sidecar, did: string | undefined, of: DocDescriptor): Handle =>
	miniDocOf(data, did, of)?.handle ?? INVALID_HANDLE;

// bobbin only sends the writer's profile when a target asks for author or value.x-tngl-editor
// a ticket's author is who opened it, x-tngl-editor only names who wrote the latest version
export const authorOf = (
	data: Sidecar,
	record: { uri: string; value?: unknown; author?: Did },
	of: DocDescriptor
): { did: Did; handle: Handle } => {
	const did = record.author ?? authorDidOf(record);
	return { did, handle: handleOf(data, did, of) };
};

export type Label = OrgTangledQueryEnrichResponse.Labels["labels"][number];

export const labelsOf = (data: Sidecar, ref: string | undefined, of: LabelsDescriptor): Label[] =>
	(payloadOf(data, ref, of) as OrgTangledQueryEnrichResponse.Labels | undefined)?.labels ?? [];

export type ReactionSummary = OrgTangledQueryEnrichResponse.ReactionSummary;

export const reactionsOf = (
	data: Sidecar,
	ref: string | undefined,
	of: ReactionsDescriptor
): ReactionSummary[] =>
	(payloadOf(data, ref, of) as OrgTangledQueryEnrichResponse.Reactions | undefined)?.reactions ??
	[];