[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { assertEquals, assertThrows } from "@std/assert";
2import {
3 CreatedAtDidKeyset,
4 IndexedAtDidKeyset,
5 IsoTimeKey,
6 LikeCountCidKeyset,
7 RkeyKey,
8 TimeCidKeyset,
9} from "../data-plane/db/pagination.ts";
10import { InvalidRequestError } from "@atp/xrpc-server";
11
12// GenericKeyset (tested via TimeCidKeyset)
13
14Deno.test("GenericKeyset", async (t) => {
15 const keyset = new TimeCidKeyset();
16
17 await t.step("packCursor uses colon separator", () => {
18 const cursor = { primary: "abc123", secondary: "def456" };
19 const packed = keyset.packCursor(cursor);
20 assertEquals(packed, "abc123:def456");
21 });
22
23 await t.step("unpackCursor splits on first colon", () => {
24 const unpacked = keyset.unpackCursor("abc123:def456");
25 assertEquals(unpacked?.primary, "abc123");
26 assertEquals(unpacked?.secondary, "def456");
27 });
28
29 await t.step("unpackCursor handles colons in secondary", () => {
30 const unpacked = keyset.unpackCursor("abc123:def:456:789");
31 assertEquals(unpacked?.primary, "abc123");
32 assertEquals(unpacked?.secondary, "def:456:789");
33 });
34
35 await t.step("unpackCursor throws on missing separator", () => {
36 assertThrows(
37 () => keyset.unpackCursor("noseparatorhere"),
38 InvalidRequestError,
39 "Malformed cursor: missing separator",
40 );
41 });
42
43 await t.step("unpackCursor throws on empty primary", () => {
44 assertThrows(
45 () => keyset.unpackCursor(":secondary"),
46 InvalidRequestError,
47 "Malformed cursor: missing primary or secondary",
48 );
49 });
50
51 await t.step("unpackCursor throws on empty secondary", () => {
52 assertThrows(
53 () => keyset.unpackCursor("primary:"),
54 InvalidRequestError,
55 "Malformed cursor: missing primary or secondary",
56 );
57 });
58
59 await t.step("getFilter returns descending filter by default", () => {
60 const labeled = {
61 primary: "2024-01-15T12:00:00.000Z",
62 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq",
63 };
64 const filter = keyset.getFilter(labeled);
65 assertEquals(filter, {
66 $or: [
67 { indexedAt: { $lt: "2024-01-15T12:00:00.000Z" } },
68 {
69 indexedAt: "2024-01-15T12:00:00.000Z",
70 cid: { $lt: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq" },
71 },
72 ],
73 });
74 });
75
76 await t.step("getFilter returns ascending filter when specified", () => {
77 const labeled = {
78 primary: "2024-01-15T12:00:00.000Z",
79 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq",
80 };
81 const filter = keyset.getFilter(labeled, "asc");
82 assertEquals(filter, {
83 $or: [
84 { indexedAt: { $gt: "2024-01-15T12:00:00.000Z" } },
85 {
86 indexedAt: "2024-01-15T12:00:00.000Z",
87 cid: { $gt: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq" },
88 },
89 ],
90 });
91 });
92
93 await t.step("pack and unpack roundtrip", () => {
94 const original = {
95 primary: "2024-01-15T12:00:00.000Z",
96 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq",
97 };
98
99 const packed = keyset.pack(original);
100 const unpacked = keyset.unpack(packed);
101
102 assertEquals(unpacked?.primary, original.primary);
103 assertEquals(unpacked?.secondary, original.secondary);
104 });
105
106 await t.step("packFromResult with array uses last result", () => {
107 const results = [
108 { indexedAt: "2024-01-15T12:00:00.000Z", cid: "first" },
109 { indexedAt: "2024-01-15T13:00:00.000Z", cid: "second" },
110 { indexedAt: "2024-01-15T14:00:00.000Z", cid: "last" },
111 ];
112
113 const packed = keyset.packFromResult(results);
114 const unpacked = keyset.unpack(packed);
115
116 assertEquals(unpacked?.secondary, "last");
117 });
118});
119
120// TimeCidKeyset (extends GenericKeyset)
121
122Deno.test("TimeCidKeyset", async (t) => {
123 const keyset = new TimeCidKeyset();
124
125 await t.step(
126 "labelResult uses current time when indexedAt is missing",
127 () => {
128 const result = {
129 cid: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq",
130 };
131
132 const before = new Date();
133 const labeled = keyset.labelResult(result);
134 const after = new Date();
135
136 const labeledDate = new Date(labeled.primary);
137 assertEquals(labeledDate >= before, true);
138 assertEquals(labeledDate <= after, true);
139 },
140 );
141
142 await t.step("labeledResultToCursor converts to base36 seconds", () => {
143 const labeled = {
144 primary: "2024-01-15T12:00:00.000Z",
145 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq",
146 };
147
148 const cursor = keyset.labeledResultToCursor(labeled);
149
150 const expectedBase36 = Math.floor(
151 new Date("2024-01-15T12:00:00.000Z").getTime() / 1000,
152 ).toString(36);
153 assertEquals(cursor.primary, expectedBase36);
154 });
155
156 await t.step("labeledResultToCursor throws on invalid date", () => {
157 const labeled = {
158 primary: "not-a-valid-date",
159 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq",
160 };
161
162 assertThrows(
163 () => keyset.labeledResultToCursor(labeled),
164 InvalidRequestError,
165 "Invalid date for cursor",
166 );
167 });
168
169 await t.step("cursorToLabeledResult converts from base36", () => {
170 const base36Seconds = (1705320000).toString(36);
171 const cursor = {
172 primary: base36Seconds,
173 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq",
174 };
175
176 const labeled = keyset.cursorToLabeledResult(cursor);
177
178 assertEquals(labeled.primary, "2024-01-15T12:00:00.000Z");
179 });
180});
181
182Deno.test("TimeCidKeyset subclasses", async (t) => {
183 await t.step("CreatedAtDidKeyset works with createdAt field", () => {
184 const keyset = new CreatedAtDidKeyset();
185 const packed = keyset.packFromResult({
186 createdAt: "2024-01-15T12:00:00.000Z",
187 authorDid: "did:plc:testuser123",
188 });
189 assertEquals(typeof packed, "string");
190 });
191
192 await t.step("IndexedAtDidKeyset works with indexedAt field", () => {
193 const keyset = new IndexedAtDidKeyset();
194 const packed = keyset.packFromResult({
195 indexedAt: "2024-01-15T12:00:00.000Z",
196 authorDid: "did:plc:testuser123",
197 });
198 assertEquals(typeof packed, "string");
199 });
200});
201
202// LikeCountCidKeyset (extends GenericKeyset)
203
204Deno.test("LikeCountCidKeyset", async (t) => {
205 await t.step("cursorToLabeledResult throws on invalid like count", () => {
206 const keyset = new LikeCountCidKeyset();
207 const cursor = {
208 primary: "not-a-number",
209 secondary: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjq",
210 };
211
212 assertThrows(
213 () => keyset.cursorToLabeledResult(cursor),
214 InvalidRequestError,
215 "Malformed cursor: invalid like count",
216 );
217 });
218});
219
220// GenericSingleKey (tested via RkeyKey)
221
222Deno.test("GenericSingleKey", async (t) => {
223 const keyset = new RkeyKey();
224
225 await t.step("getFilter returns descending filter by default", () => {
226 const labeled = { primary: "3jui7kd2zcysk" };
227 const filter = keyset.getFilter(labeled);
228 assertEquals(filter, {
229 key: { $lt: "3jui7kd2zcysk" },
230 });
231 });
232
233 await t.step("getFilter returns ascending filter when specified", () => {
234 const labeled = { primary: "3jui7kd2zcysk" };
235 const filter = keyset.getFilter(labeled, "asc");
236 assertEquals(filter, {
237 key: { $gt: "3jui7kd2zcysk" },
238 });
239 });
240
241 await t.step("unpackCursor throws on colon separator", () => {
242 assertThrows(
243 () => keyset.unpackCursor("has:colon"),
244 InvalidRequestError,
245 "Malformed cursor: unexpected separator",
246 );
247 });
248
249 await t.step("unpackCursor throws on double underscore separator", () => {
250 assertThrows(
251 () => keyset.unpackCursor("has__underscore"),
252 InvalidRequestError,
253 "Malformed cursor: unexpected separator",
254 );
255 });
256});
257
258// IsoTimeKey (extends GenericSingleKey)
259
260Deno.test("IsoTimeKey", async (t) => {
261 const keyset = new IsoTimeKey();
262
263 await t.step("labeledResultToCursor throws on invalid date", () => {
264 const labeled = { primary: "not-a-valid-date" };
265 assertThrows(
266 () => keyset.labeledResultToCursor(labeled),
267 InvalidRequestError,
268 "Invalid date for cursor",
269 );
270 });
271
272 await t.step("cursorToLabeledResult throws on invalid date", () => {
273 const cursor = { primary: "invalid-date" };
274 assertThrows(
275 () => keyset.cursorToLabeledResult(cursor),
276 InvalidRequestError,
277 "Malformed cursor: invalid date",
278 );
279 });
280
281 await t.step("pack produces ISO date but unpack fails due to colons", () => {
282 // IsoTimeKey produces ISO dates with colons, but GenericSingleKey.unpackCursor
283 // rejects strings with colons. This is a known limitation.
284 const result = { indexedAt: "2024-01-15T12:00:00.000Z" };
285 const packed = keyset.packFromResult(result);
286
287 assertEquals(packed, "2024-01-15T12:00:00.000Z");
288
289 assertThrows(
290 () => keyset.unpack(packed),
291 InvalidRequestError,
292 "Malformed cursor: unexpected separator",
293 );
294 });
295});
296
297// RkeyKey (extends GenericSingleKey)
298
299Deno.test("RkeyKey", async (t) => {
300 await t.step("cursorToLabeledResult throws on invalid record key", () => {
301 const keyset = new RkeyKey();
302 const cursor = { primary: "invalid/key" };
303 assertThrows(
304 () => keyset.cursorToLabeledResult(cursor),
305 InvalidRequestError,
306 "Malformed cursor",
307 );
308 });
309});