keyv-github 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +106 -0
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/bun.lock +235 -0
- package/data/hello-today.txt +1 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.mjs +257 -0
- package/examples/hello-today.ts +37 -0
- package/package.json +27 -0
- package/src/index.test.ts +463 -0
- package/src/index.ts +308 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import { Octokit } from "octokit";
|
|
3
|
+
import KeyvGithub, { type KeyvGithubOptions } from "./index.ts";
|
|
4
|
+
|
|
5
|
+
// ── Minimal mock for Octokit REST API ──────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
type FileRecord = { content: string; sha: string };
|
|
8
|
+
|
|
9
|
+
function makeMockClient(files: Map<string, FileRecord> = new Map()) {
|
|
10
|
+
let shaCounter = 0;
|
|
11
|
+
const nextSha = () => `sha${++shaCounter}`;
|
|
12
|
+
const messages: string[] = [];
|
|
13
|
+
|
|
14
|
+
const getContent = async ({ path, ref: _ref }: { path: string; ref?: string }) => {
|
|
15
|
+
const file = files.get(path);
|
|
16
|
+
if (!file) {
|
|
17
|
+
const err: any = new Error("Not Found");
|
|
18
|
+
err.status = 404;
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
data: {
|
|
23
|
+
type: "file" as const,
|
|
24
|
+
content: Buffer.from(file.content).toString("base64"),
|
|
25
|
+
sha: file.sha,
|
|
26
|
+
name: path.split("/").pop()!,
|
|
27
|
+
path,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const createOrUpdateFileContents = async ({
|
|
33
|
+
path,
|
|
34
|
+
content,
|
|
35
|
+
message,
|
|
36
|
+
sha: existingSha,
|
|
37
|
+
}: {
|
|
38
|
+
path: string;
|
|
39
|
+
content: string;
|
|
40
|
+
message: string;
|
|
41
|
+
sha?: string;
|
|
42
|
+
branch?: string;
|
|
43
|
+
}) => {
|
|
44
|
+
messages.push(message);
|
|
45
|
+
const decoded = Buffer.from(content, "base64").toString("utf-8");
|
|
46
|
+
const sha = existingSha ?? nextSha();
|
|
47
|
+
files.set(path, { content: decoded, sha });
|
|
48
|
+
return { data: { content: { path, sha } } };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const deleteFile = async ({
|
|
52
|
+
path,
|
|
53
|
+
message,
|
|
54
|
+
sha,
|
|
55
|
+
}: {
|
|
56
|
+
path: string;
|
|
57
|
+
message: string;
|
|
58
|
+
sha: string;
|
|
59
|
+
branch?: string;
|
|
60
|
+
}) => {
|
|
61
|
+
messages.push(message);
|
|
62
|
+
const file = files.get(path);
|
|
63
|
+
if (!file || file.sha !== sha) {
|
|
64
|
+
const err: any = new Error("Not Found");
|
|
65
|
+
err.status = 404;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
files.delete(path);
|
|
69
|
+
return { data: {} };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Git Data API – used by _batchCommit, clear, iterator, deleteMany
|
|
73
|
+
const getRef = async () => ({ data: { object: { sha: "head-sha" } } });
|
|
74
|
+
|
|
75
|
+
const getTree = async (_: { tree_sha: string; recursive?: string }) => {
|
|
76
|
+
const blobs = Array.from(files.entries()).map(([path]) => ({
|
|
77
|
+
type: "blob" as const,
|
|
78
|
+
path,
|
|
79
|
+
}));
|
|
80
|
+
return { data: { tree: blobs, truncated: false } };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getCommit = async (_: { commit_sha: string }) => ({
|
|
84
|
+
data: { tree: { sha: "base-tree-sha" } },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Applies inline content / sha:null deletions to the files map
|
|
88
|
+
const createTree = async ({ tree }: { base_tree?: string; tree: any[] }) => {
|
|
89
|
+
for (const entry of tree) {
|
|
90
|
+
if (entry.sha === null) {
|
|
91
|
+
files.delete(entry.path);
|
|
92
|
+
} else if (entry.content !== undefined) {
|
|
93
|
+
files.set(entry.path, { content: entry.content, sha: nextSha() });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { data: { sha: `tree-${nextSha()}` } };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const createCommit = async ({ message }: { message: string; tree: string; parents: string[] }) => {
|
|
100
|
+
messages.push(message);
|
|
101
|
+
return { data: { sha: `commit-${nextSha()}` } };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const updateRef = async (_: { owner: string; repo: string; ref: string; sha: string; force?: boolean }) =>
|
|
105
|
+
({ data: {} });
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
rest: {
|
|
109
|
+
repos: { getContent, createOrUpdateFileContents, deleteFile },
|
|
110
|
+
git: { getRef, getTree, getCommit, createTree, createCommit, updateRef },
|
|
111
|
+
},
|
|
112
|
+
messages,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function makeStore(files?: Map<string, FileRecord>, options?: Omit<KeyvGithubOptions, "url">) {
|
|
119
|
+
const mockFiles = files ?? new Map<string, FileRecord>();
|
|
120
|
+
const client = makeMockClient(mockFiles);
|
|
121
|
+
const store = new KeyvGithub("https://github.com/owner/repo", { branch: "main", client: client.rest as unknown as Octokit["rest"], ...(options ?? {}) });
|
|
122
|
+
return { store, mockFiles, messages: client.messages };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
describe("KeyvGithub constructor", () => {
|
|
128
|
+
test("parses HTTPS GitHub URL", () => {
|
|
129
|
+
const client = makeMockClient();
|
|
130
|
+
expect(
|
|
131
|
+
() => new KeyvGithub("https://github.com/owner/repo", { client: client.rest as unknown as Octokit["rest"] })
|
|
132
|
+
).not.toThrow();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("parses SSH-style GitHub URL", () => {
|
|
136
|
+
const client = makeMockClient();
|
|
137
|
+
expect(
|
|
138
|
+
() => new KeyvGithub("git@github.com:owner/repo.git", { client: client.rest as unknown as Octokit["rest"] })
|
|
139
|
+
).not.toThrow();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("throws when no owner/repo can be parsed", () => {
|
|
143
|
+
expect(() => new KeyvGithub("notarepo")).toThrow("Invalid GitHub repo URL");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("accepts short owner/repo form without github.com prefix", () => {
|
|
147
|
+
const client = makeMockClient();
|
|
148
|
+
const store = new KeyvGithub("owner/repo", { client: client.rest as unknown as Octokit["rest"] });
|
|
149
|
+
expect(store.owner).toBe("owner");
|
|
150
|
+
expect(store.repo).toBe("repo");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("accepts short owner/repo/tree/branch form", () => {
|
|
154
|
+
const client = makeMockClient();
|
|
155
|
+
const store = new KeyvGithub("owner/repo/tree/main", { client: client.rest as unknown as Octokit["rest"] });
|
|
156
|
+
expect(store.owner).toBe("owner");
|
|
157
|
+
expect(store.repo).toBe("repo");
|
|
158
|
+
expect(store.branch).toBe("main");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("parses branch from /tree/<branch> in URL", () => {
|
|
162
|
+
const client = makeMockClient();
|
|
163
|
+
const store = new KeyvGithub("https://github.com/owner/repo/tree/develop", { client: client.rest as unknown as Octokit["rest"] });
|
|
164
|
+
expect(store.branch).toBe("develop");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("parses multi-segment branch from URL", () => {
|
|
168
|
+
const client = makeMockClient();
|
|
169
|
+
const store = new KeyvGithub("https://github.com/owner/repo/tree/feature/my-branch", { client: client.rest as unknown as Octokit["rest"] });
|
|
170
|
+
expect(store.branch).toBe("feature/my-branch");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("options.branch overrides URL branch", () => {
|
|
174
|
+
const client = makeMockClient();
|
|
175
|
+
const store = new KeyvGithub("https://github.com/owner/repo/tree/develop", {
|
|
176
|
+
client: client.rest as unknown as Octokit["rest"],
|
|
177
|
+
branch: "override",
|
|
178
|
+
});
|
|
179
|
+
expect(store.branch).toBe("override");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("defaults to main when no branch in URL or options", () => {
|
|
183
|
+
const client = makeMockClient();
|
|
184
|
+
const store = new KeyvGithub("https://github.com/owner/repo", { client: client.rest as unknown as Octokit["rest"] });
|
|
185
|
+
expect(store.branch).toBe("main");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("key validation", () => {
|
|
190
|
+
const cases: [string, string][] = [
|
|
191
|
+
["", "empty"],
|
|
192
|
+
["/absolute", "start with '/'"],
|
|
193
|
+
["trailing/", "end with '/'"],
|
|
194
|
+
["double//slash", "contain '//'"],
|
|
195
|
+
["../escape", "'..' segment"],
|
|
196
|
+
["a/./b", "'.' segment"],
|
|
197
|
+
["null\0byte", "null bytes"],
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
for (const [bad] of cases) {
|
|
201
|
+
test(`throws for ${JSON.stringify(bad)}`, async () => {
|
|
202
|
+
const { store } = makeStore();
|
|
203
|
+
expect(store.get(bad)).rejects.toThrow();
|
|
204
|
+
expect(store.set(bad, "v")).rejects.toThrow();
|
|
205
|
+
expect(store.delete(bad)).rejects.toThrow();
|
|
206
|
+
expect(store.has(bad)).rejects.toThrow();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
test("accepts valid paths", async () => {
|
|
211
|
+
const { store } = makeStore();
|
|
212
|
+
const validPaths = ["simple", "data/file.txt", "a/b/c/d.json", "deep/nested/path"];
|
|
213
|
+
for (const path of validPaths) {
|
|
214
|
+
expect(await store.get(path)).toBeUndefined();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("get", () => {
|
|
220
|
+
test("returns undefined for missing key", async () => {
|
|
221
|
+
const { store } = makeStore();
|
|
222
|
+
expect(await store.get("missing/key")).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("returns file content for existing key", async () => {
|
|
226
|
+
const files = new Map([["data/hello", { content: "world", sha: "abc" }]]);
|
|
227
|
+
const { store } = makeStore(files);
|
|
228
|
+
expect(await store.get<string>("data/hello")).toBe("world");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("set", () => {
|
|
233
|
+
test("creates a new file", async () => {
|
|
234
|
+
const { store, mockFiles } = makeStore();
|
|
235
|
+
await store.set("notes/foo", "bar");
|
|
236
|
+
expect(mockFiles.get("notes/foo")?.content).toBe("bar");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("updates an existing file preserving key", async () => {
|
|
240
|
+
const files = new Map([["notes/foo", { content: "old", sha: "sha1" }]]);
|
|
241
|
+
const { store, mockFiles } = makeStore(files);
|
|
242
|
+
await store.set("notes/foo", "new");
|
|
243
|
+
expect(mockFiles.get("notes/foo")?.content).toBe("new");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("stores arbitrary unicode values", async () => {
|
|
247
|
+
const { store, mockFiles } = makeStore();
|
|
248
|
+
await store.set("unicode", "日本語テスト 🎉");
|
|
249
|
+
expect(mockFiles.get("unicode")?.content).toBe("日本語テスト 🎉");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("delete", () => {
|
|
254
|
+
test("returns false for missing key", async () => {
|
|
255
|
+
const { store } = makeStore();
|
|
256
|
+
expect(await store.delete("nope")).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("deletes existing key and returns true", async () => {
|
|
260
|
+
const files = new Map([["remove/me", { content: "v", sha: "s1" }]]);
|
|
261
|
+
const { store, mockFiles } = makeStore(files);
|
|
262
|
+
const result = await store.delete("remove/me");
|
|
263
|
+
expect(result).toBe(true);
|
|
264
|
+
expect(mockFiles.has("remove/me")).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("has", () => {
|
|
269
|
+
test("returns false when key does not exist", async () => {
|
|
270
|
+
const { store } = makeStore();
|
|
271
|
+
expect(await store.has("ghost")).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("returns true when key exists", async () => {
|
|
275
|
+
const files = new Map([["present", { content: "yes", sha: "s" }]]);
|
|
276
|
+
const { store } = makeStore(files);
|
|
277
|
+
expect(await store.has("present")).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("clear", () => {
|
|
282
|
+
test("throws by default (enableClear not set)", async () => {
|
|
283
|
+
const { store } = makeStore();
|
|
284
|
+
expect(store.clear()).rejects.toThrow("enableClear");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("throws when enableClear is false", async () => {
|
|
288
|
+
const { store } = makeStore(undefined, { enableClear: false });
|
|
289
|
+
expect(store.clear()).rejects.toThrow("enableClear");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("removes all files when enableClear is true", async () => {
|
|
293
|
+
const files = new Map<string, FileRecord>([
|
|
294
|
+
["a", { content: "1", sha: "s1" }],
|
|
295
|
+
["b", { content: "2", sha: "s2" }],
|
|
296
|
+
["c/d", { content: "3", sha: "s3" }],
|
|
297
|
+
]);
|
|
298
|
+
const { store, mockFiles } = makeStore(files, { enableClear: true });
|
|
299
|
+
await store.clear();
|
|
300
|
+
expect(mockFiles.size).toBe(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("no-op on empty store when enableClear is true", async () => {
|
|
304
|
+
const { store } = makeStore(undefined, { enableClear: true });
|
|
305
|
+
await store.clear(); // should not throw
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("setMany", () => {
|
|
310
|
+
test("writes multiple files in one batch", async () => {
|
|
311
|
+
const { store, mockFiles, messages } = makeStore();
|
|
312
|
+
await store.setMany([
|
|
313
|
+
{ key: "a/1.txt", value: "hello" },
|
|
314
|
+
{ key: "b/2.txt", value: "world" },
|
|
315
|
+
{ key: "c/3.txt", value: "!" },
|
|
316
|
+
]);
|
|
317
|
+
expect(mockFiles.get("a/1.txt")?.content).toBe("hello");
|
|
318
|
+
expect(mockFiles.get("b/2.txt")?.content).toBe("world");
|
|
319
|
+
expect(mockFiles.get("c/3.txt")?.content).toBe("!");
|
|
320
|
+
expect(messages).toHaveLength(1); // single commit
|
|
321
|
+
expect(messages[0]).toBe("batch update 3 files");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("uses msg hook for single-entry batch", async () => {
|
|
325
|
+
const { store, messages } = makeStore(undefined, {
|
|
326
|
+
msg: (key: string, value: string | null) => `custom: ${key}=${value}`,
|
|
327
|
+
});
|
|
328
|
+
await store.setMany([{ key: "x/y", value: "v" }]);
|
|
329
|
+
expect(messages[0]).toBe("custom: x/y=v");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("no-op for empty array", async () => {
|
|
333
|
+
const { store, messages } = makeStore();
|
|
334
|
+
await store.setMany([]);
|
|
335
|
+
expect(messages).toHaveLength(0);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("validates all keys before writing", async () => {
|
|
339
|
+
const { store, mockFiles } = makeStore();
|
|
340
|
+
expect(store.setMany([{ key: "good/key", value: "v" }, { key: "../bad", value: "v" }])).rejects.toThrow();
|
|
341
|
+
expect(mockFiles.size).toBe(0);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("deleteMany", () => {
|
|
346
|
+
test("deletes multiple existing files in one batch, returns true", async () => {
|
|
347
|
+
const files = new Map<string, FileRecord>([
|
|
348
|
+
["a", { content: "1", sha: "s1" }],
|
|
349
|
+
["b", { content: "2", sha: "s2" }],
|
|
350
|
+
["c", { content: "3", sha: "s3" }],
|
|
351
|
+
]);
|
|
352
|
+
const { store, mockFiles, messages } = makeStore(files);
|
|
353
|
+
const result = await store.deleteMany(["a", "b"]);
|
|
354
|
+
expect(result).toBe(true);
|
|
355
|
+
expect(mockFiles.has("a")).toBe(false);
|
|
356
|
+
expect(mockFiles.has("b")).toBe(false);
|
|
357
|
+
expect(mockFiles.has("c")).toBe(true);
|
|
358
|
+
expect(messages).toHaveLength(1); // single commit
|
|
359
|
+
expect(messages[0]).toBe("batch delete 2 files");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("returns false when no keys exist", async () => {
|
|
363
|
+
const { store } = makeStore();
|
|
364
|
+
const result = await store.deleteMany(["missing"]);
|
|
365
|
+
expect(result).toBe(false);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("returns false for empty array", async () => {
|
|
369
|
+
const { store, messages } = makeStore();
|
|
370
|
+
const result = await store.deleteMany([]);
|
|
371
|
+
expect(result).toBe(false);
|
|
372
|
+
expect(messages).toHaveLength(0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("uses msg hook for single-key batch", async () => {
|
|
376
|
+
const files = new Map([["k", { content: "v", sha: "s" }]]);
|
|
377
|
+
const { store, messages } = makeStore(files, {
|
|
378
|
+
msg: (key: string, value: string | null) => `rm: ${key} (${value ?? "null"})`,
|
|
379
|
+
});
|
|
380
|
+
await store.deleteMany(["k"]);
|
|
381
|
+
expect(messages[0]).toBe("rm: k (null)");
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe("iterator", () => {
|
|
386
|
+
test("yields all key-value pairs", async () => {
|
|
387
|
+
const files = new Map<string, FileRecord>([
|
|
388
|
+
["x", { content: "1", sha: "s1" }],
|
|
389
|
+
["y/z", { content: "2", sha: "s2" }],
|
|
390
|
+
]);
|
|
391
|
+
const { store } = makeStore(files);
|
|
392
|
+
const results: [string, unknown][] = [];
|
|
393
|
+
for await (const entry of store.iterator()) {
|
|
394
|
+
results.push(entry);
|
|
395
|
+
}
|
|
396
|
+
expect(results.length).toBe(2);
|
|
397
|
+
expect(results.find(([k]) => k === "x")?.[1]).toBe("1");
|
|
398
|
+
expect(results.find(([k]) => k === "y/z")?.[1]).toBe("2");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("filters by prefix when provided", async () => {
|
|
402
|
+
const files = new Map<string, FileRecord>([
|
|
403
|
+
["ns/a", { content: "1", sha: "s1" }],
|
|
404
|
+
["ns/b", { content: "2", sha: "s2" }],
|
|
405
|
+
["other/c", { content: "3", sha: "s3" }],
|
|
406
|
+
]);
|
|
407
|
+
const { store } = makeStore(files);
|
|
408
|
+
const results: [string, unknown][] = [];
|
|
409
|
+
for await (const entry of store.iterator("ns/")) {
|
|
410
|
+
results.push(entry);
|
|
411
|
+
}
|
|
412
|
+
expect(results.length).toBe(2);
|
|
413
|
+
expect(results.every(([k]) => k.startsWith("ns/"))).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe("msg hook", () => {
|
|
418
|
+
test("custom msg is called with key and value on set", async () => {
|
|
419
|
+
const calls: [string, string | null][] = [];
|
|
420
|
+
const { store, messages } = makeStore(undefined, {
|
|
421
|
+
msg: (key: string, value: string | null) => { calls.push([key, value]); return `chore: put ${key}`; },
|
|
422
|
+
});
|
|
423
|
+
await store.set("notes/foo", "bar");
|
|
424
|
+
expect(calls).toEqual([["notes/foo", "bar"]]);
|
|
425
|
+
expect(messages[messages.length - 1]).toBe("chore: put notes/foo");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("custom msg is called with key and null on delete", async () => {
|
|
429
|
+
const calls: [string, string | null][] = [];
|
|
430
|
+
const files = new Map([["to/remove", { content: "v", sha: "s1" }]]);
|
|
431
|
+
const { store, messages } = makeStore(files, {
|
|
432
|
+
msg: (key: string, value: string | null) => { calls.push([key, value]); return `chore: rm ${key}`; },
|
|
433
|
+
});
|
|
434
|
+
await store.delete("to/remove");
|
|
435
|
+
expect(calls).toEqual([["to/remove", null]]);
|
|
436
|
+
expect(messages[messages.length - 1]).toBe("chore: rm to/remove");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("default msg falls back to 'update <key>' / 'delete <key>'", async () => {
|
|
440
|
+
const { store, messages } = makeStore();
|
|
441
|
+
await store.set("k", "v");
|
|
442
|
+
expect(messages[messages.length - 1]).toBe("update k");
|
|
443
|
+
|
|
444
|
+
const files2 = new Map([["k2", { content: "v", sha: "s1" }]]);
|
|
445
|
+
const { store: store2, messages: messages2 } = makeStore(files2);
|
|
446
|
+
await store2.delete("k2");
|
|
447
|
+
expect(messages2[messages2.length - 1]).toBe("delete k2");
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("integration with Keyv", () => {
|
|
452
|
+
test("works as a Keyv storage adapter", async () => {
|
|
453
|
+
const { default: Keyv } = await import("keyv");
|
|
454
|
+
const { store } = makeStore();
|
|
455
|
+
const keyv = new Keyv({ store });
|
|
456
|
+
|
|
457
|
+
await keyv.set("greeting", "hello");
|
|
458
|
+
expect(await keyv.get("greeting") as unknown).toBe("hello");
|
|
459
|
+
|
|
460
|
+
await keyv.delete("greeting");
|
|
461
|
+
expect(await keyv.get("greeting") as unknown).toBe(undefined);
|
|
462
|
+
});
|
|
463
|
+
});
|