keyv-github 1.3.0 → 1.4.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/README.md +14 -0
- package/dist/index.d.mts +18 -1
- package/dist/index.mjs +15 -4
- package/package.json +1 -1
- package/src/index.test.ts +5 -5
- package/src/index.ts +34 -5
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ const store = new KeyvGithub("owner/repo", {
|
|
|
99
99
|
|
|
100
100
|
## Key rules
|
|
101
101
|
|
|
102
|
+
⚠️ **Keys are validated but NOT sanitized.** You must sanitize keys yourself before passing them to this adapter. Invalid keys will throw an error.
|
|
103
|
+
|
|
102
104
|
Keys must be valid relative file paths:
|
|
103
105
|
|
|
104
106
|
- Non-empty
|
|
@@ -109,6 +111,18 @@ Keys must be valid relative file paths:
|
|
|
109
111
|
|
|
110
112
|
Invalid keys throw synchronously before any API request.
|
|
111
113
|
|
|
114
|
+
```ts
|
|
115
|
+
// ✗ These will throw errors
|
|
116
|
+
await store.set("/absolute/path", "value"); // leading slash
|
|
117
|
+
await store.set("path/", "value"); // trailing slash
|
|
118
|
+
await store.set("path/../escape", "value"); // directory traversal
|
|
119
|
+
await store.set("path//double", "value"); // double slashes
|
|
120
|
+
|
|
121
|
+
// ✓ Valid keys
|
|
122
|
+
await store.set("data/file.txt", "value");
|
|
123
|
+
await store.set("nested/path/key.json", "value");
|
|
124
|
+
```
|
|
125
|
+
|
|
112
126
|
## See Also
|
|
113
127
|
|
|
114
128
|
Other Keyv storage adapters by the same author:
|
package/dist/index.d.mts
CHANGED
|
@@ -12,8 +12,20 @@ interface KeyvGithubOptions {
|
|
|
12
12
|
url: string;
|
|
13
13
|
branch?: string;
|
|
14
14
|
client?: Octokit | Octokit["rest"];
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Customize the commit message for single-key operations. value is null for deletes.
|
|
17
|
+
* @warning Consider adding `[skip ci]` to your commit messages to prevent
|
|
18
|
+
* triggering CI workflows on each key-value update.
|
|
19
|
+
*/
|
|
16
20
|
msg?: (key: string, value: string | null) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Customize the commit message for batch operations (setMany, deleteMany, clear).
|
|
23
|
+
* @param operation - 'set' | 'delete' | 'clear'
|
|
24
|
+
* @param paths - array of file paths being modified
|
|
25
|
+
* @warning Consider adding `[skip ci]` to your commit messages to prevent
|
|
26
|
+
* triggering CI workflows on each key-value update.
|
|
27
|
+
*/
|
|
28
|
+
batchMsg?: (operation: "set" | "delete" | "clear", paths: string[]) => string;
|
|
17
29
|
/** clear() deletes every file in the repo and is disabled by default. Set to true to allow it. */
|
|
18
30
|
enableClear?: boolean;
|
|
19
31
|
/** Path prefix prepended to every key (e.g. 'data/'). Defaults to ''. */
|
|
@@ -28,6 +40,10 @@ interface KeyvGithubOptions {
|
|
|
28
40
|
*
|
|
29
41
|
* Each key is a file path in the repo; the file content is the value.
|
|
30
42
|
* Example: new KeyvGithub("https://github.com/owner/repo/tree/main", { client })
|
|
43
|
+
*
|
|
44
|
+
* @warning **Keys are validated but NOT sanitized.** You must ensure keys are valid
|
|
45
|
+
* GitHub file paths before calling any method. Invalid keys throw an error.
|
|
46
|
+
* Requirements: non-empty, no leading/trailing `/`, no `//`, no `.`/`..` segments, no null bytes.
|
|
31
47
|
*/
|
|
32
48
|
declare class KeyvGithub extends EventEmitter implements KeyvStoreAdapter {
|
|
33
49
|
opts: KeyvGithubOptions;
|
|
@@ -39,6 +55,7 @@ declare class KeyvGithub extends EventEmitter implements KeyvStoreAdapter {
|
|
|
39
55
|
get branch(): string;
|
|
40
56
|
rest: Octokit["rest"];
|
|
41
57
|
private msg;
|
|
58
|
+
private batchMsg;
|
|
42
59
|
readonly enableClear: boolean;
|
|
43
60
|
readonly prefix: string;
|
|
44
61
|
readonly suffix: string;
|
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,10 @@ import { Octokit } from "octokit";
|
|
|
7
7
|
*
|
|
8
8
|
* Each key is a file path in the repo; the file content is the value.
|
|
9
9
|
* Example: new KeyvGithub("https://github.com/owner/repo/tree/main", { client })
|
|
10
|
+
*
|
|
11
|
+
* @warning **Keys are validated but NOT sanitized.** You must ensure keys are valid
|
|
12
|
+
* GitHub file paths before calling any method. Invalid keys throw an error.
|
|
13
|
+
* Requirements: non-empty, no leading/trailing `/`, no `//`, no `.`/`..` segments, no null bytes.
|
|
10
14
|
*/
|
|
11
15
|
var KeyvGithub = class KeyvGithub extends EventEmitter {
|
|
12
16
|
opts;
|
|
@@ -20,6 +24,7 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
|
|
|
20
24
|
}
|
|
21
25
|
rest;
|
|
22
26
|
msg;
|
|
27
|
+
batchMsg;
|
|
23
28
|
enableClear;
|
|
24
29
|
prefix;
|
|
25
30
|
suffix;
|
|
@@ -37,7 +42,12 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
|
|
|
37
42
|
...options
|
|
38
43
|
};
|
|
39
44
|
this.rest = options.client instanceof Octokit ? options.client.rest : options.client ?? new Octokit().rest;
|
|
40
|
-
this.msg = options.msg ?? ((key, value) => value === null ? `delete ${key}` : `update ${key}`);
|
|
45
|
+
this.msg = options.msg ?? ((key, value) => value === null ? `delete ${key} [skip ci]` : `update ${key} [skip ci]`);
|
|
46
|
+
this.batchMsg = options.batchMsg ?? ((op, paths) => {
|
|
47
|
+
const n = paths.length;
|
|
48
|
+
if (op === "clear") return `clear: remove ${n} files [skip ci]`;
|
|
49
|
+
return `batch ${op} ${n} files [skip ci]`;
|
|
50
|
+
});
|
|
41
51
|
this.enableClear = options.enableClear ?? false;
|
|
42
52
|
this.prefix = options.prefix ?? "";
|
|
43
53
|
this.suffix = options.suffix ?? "";
|
|
@@ -223,7 +233,8 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
|
|
|
223
233
|
this.validatePath(this.toPath(key));
|
|
224
234
|
}
|
|
225
235
|
const entries = values.map(({ key, value }) => [this.toPath(key), String(value)]);
|
|
226
|
-
const
|
|
236
|
+
const paths = entries.map(([p]) => p);
|
|
237
|
+
const message = entries.length === 1 ? this.msg(entries[0][0], entries[0][1]) : this.batchMsg("set", paths);
|
|
227
238
|
await this._batchCommit({
|
|
228
239
|
set: entries,
|
|
229
240
|
message
|
|
@@ -250,7 +261,7 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
|
|
|
250
261
|
const existingPaths = new Set(treeData.tree.filter((i) => i.type === "blob" && i.path).map((i) => i.path));
|
|
251
262
|
const toDelete = keys.map((k) => this.toPath(k)).filter((p) => existingPaths.has(p));
|
|
252
263
|
if (toDelete.length === 0) return false;
|
|
253
|
-
const message = toDelete.length === 1 ? this.msg(toDelete[0], null) :
|
|
264
|
+
const message = toDelete.length === 1 ? this.msg(toDelete[0], null) : this.batchMsg("delete", toDelete);
|
|
254
265
|
await this._batchCommit({
|
|
255
266
|
delete: toDelete,
|
|
256
267
|
message
|
|
@@ -273,7 +284,7 @@ var KeyvGithub = class KeyvGithub extends EventEmitter {
|
|
|
273
284
|
const allPaths = treeData.tree.filter((i) => i.type === "blob" && i.path && i.path.startsWith(this.prefix) && i.path.endsWith(this.suffix)).map((i) => i.path);
|
|
274
285
|
if (allPaths.length > 0) await this._batchCommit({
|
|
275
286
|
delete: allPaths,
|
|
276
|
-
message:
|
|
287
|
+
message: this.batchMsg("clear", allPaths)
|
|
277
288
|
});
|
|
278
289
|
}
|
|
279
290
|
async *iterator(prefix) {
|
package/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -386,7 +386,7 @@ describe("setMany", () => {
|
|
|
386
386
|
expect(mockFiles.get("b/2.txt")?.content).toBe("world");
|
|
387
387
|
expect(mockFiles.get("c/3.txt")?.content).toBe("!");
|
|
388
388
|
expect(messages).toHaveLength(1); // single commit
|
|
389
|
-
expect(messages[0]).toBe("batch
|
|
389
|
+
expect(messages[0]).toBe("batch set 3 files [skip ci]");
|
|
390
390
|
});
|
|
391
391
|
|
|
392
392
|
test("uses msg hook for single-entry batch", async () => {
|
|
@@ -440,7 +440,7 @@ describe("deleteMany", () => {
|
|
|
440
440
|
expect(mockFiles.has("b")).toBe(false);
|
|
441
441
|
expect(mockFiles.has("c")).toBe(true);
|
|
442
442
|
expect(messages).toHaveLength(1); // single commit
|
|
443
|
-
expect(messages[0]).toBe("batch delete 2 files");
|
|
443
|
+
expect(messages[0]).toBe("batch delete 2 files [skip ci]");
|
|
444
444
|
});
|
|
445
445
|
|
|
446
446
|
test("returns false when no keys exist", async () => {
|
|
@@ -527,15 +527,15 @@ describe("msg hook", () => {
|
|
|
527
527
|
expect(messages[messages.length - 1]).toBe("chore: rm to/remove");
|
|
528
528
|
});
|
|
529
529
|
|
|
530
|
-
test("default msg falls back to 'update <key>' / 'delete <key>'", async () => {
|
|
530
|
+
test("default msg falls back to 'update <key> [skip ci]' / 'delete <key> [skip ci]'", async () => {
|
|
531
531
|
const { store, messages } = makeStore();
|
|
532
532
|
await store.set("k", "v");
|
|
533
|
-
expect(messages[messages.length - 1]).toBe("update k");
|
|
533
|
+
expect(messages[messages.length - 1]).toBe("update k [skip ci]");
|
|
534
534
|
|
|
535
535
|
const files2 = new Map([["k2", { content: "v", sha: "s1" }]]);
|
|
536
536
|
const { store: store2, messages: messages2 } = makeStore(files2);
|
|
537
537
|
await store2.delete("k2");
|
|
538
|
-
expect(messages2[messages2.length - 1]).toBe("delete k2");
|
|
538
|
+
expect(messages2[messages2.length - 1]).toBe("delete k2 [skip ci]");
|
|
539
539
|
});
|
|
540
540
|
});
|
|
541
541
|
|
package/src/index.ts
CHANGED
|
@@ -12,8 +12,20 @@ export interface KeyvGithubOptions {
|
|
|
12
12
|
url: string;
|
|
13
13
|
branch?: string;
|
|
14
14
|
client?: Octokit | Octokit["rest"];
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Customize the commit message for single-key operations. value is null for deletes.
|
|
17
|
+
* @warning Consider adding `[skip ci]` to your commit messages to prevent
|
|
18
|
+
* triggering CI workflows on each key-value update.
|
|
19
|
+
*/
|
|
16
20
|
msg?: (key: string, value: string | null) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Customize the commit message for batch operations (setMany, deleteMany, clear).
|
|
23
|
+
* @param operation - 'set' | 'delete' | 'clear'
|
|
24
|
+
* @param paths - array of file paths being modified
|
|
25
|
+
* @warning Consider adding `[skip ci]` to your commit messages to prevent
|
|
26
|
+
* triggering CI workflows on each key-value update.
|
|
27
|
+
*/
|
|
28
|
+
batchMsg?: (operation: "set" | "delete" | "clear", paths: string[]) => string;
|
|
17
29
|
/** clear() deletes every file in the repo and is disabled by default. Set to true to allow it. */
|
|
18
30
|
enableClear?: boolean;
|
|
19
31
|
/** Path prefix prepended to every key (e.g. 'data/'). Defaults to ''. */
|
|
@@ -29,6 +41,10 @@ export interface KeyvGithubOptions {
|
|
|
29
41
|
*
|
|
30
42
|
* Each key is a file path in the repo; the file content is the value.
|
|
31
43
|
* Example: new KeyvGithub("https://github.com/owner/repo/tree/main", { client })
|
|
44
|
+
*
|
|
45
|
+
* @warning **Keys are validated but NOT sanitized.** You must ensure keys are valid
|
|
46
|
+
* GitHub file paths before calling any method. Invalid keys throw an error.
|
|
47
|
+
* Requirements: non-empty, no leading/trailing `/`, no `//`, no `.`/`..` segments, no null bytes.
|
|
32
48
|
*/
|
|
33
49
|
export default class KeyvGithub
|
|
34
50
|
extends EventEmitter
|
|
@@ -46,6 +62,10 @@ export default class KeyvGithub
|
|
|
46
62
|
}
|
|
47
63
|
rest: Octokit["rest"];
|
|
48
64
|
private msg: (key: string, value: string | null) => string;
|
|
65
|
+
private batchMsg: (
|
|
66
|
+
operation: "set" | "delete" | "clear",
|
|
67
|
+
paths: string[],
|
|
68
|
+
) => string;
|
|
49
69
|
readonly enableClear: boolean;
|
|
50
70
|
readonly prefix: string;
|
|
51
71
|
readonly suffix: string;
|
|
@@ -70,7 +90,15 @@ export default class KeyvGithub
|
|
|
70
90
|
: (options.client ?? new Octokit().rest);
|
|
71
91
|
this.msg =
|
|
72
92
|
options.msg ??
|
|
73
|
-
((key, value) =>
|
|
93
|
+
((key, value) =>
|
|
94
|
+
value === null ? `delete ${key} [skip ci]` : `update ${key} [skip ci]`);
|
|
95
|
+
this.batchMsg =
|
|
96
|
+
options.batchMsg ??
|
|
97
|
+
((op, paths) => {
|
|
98
|
+
const n = paths.length;
|
|
99
|
+
if (op === "clear") return `clear: remove ${n} files [skip ci]`;
|
|
100
|
+
return `batch ${op} ${n} files [skip ci]`;
|
|
101
|
+
});
|
|
74
102
|
this.enableClear = options.enableClear ?? false;
|
|
75
103
|
this.prefix = options.prefix ?? "";
|
|
76
104
|
this.suffix = options.suffix ?? "";
|
|
@@ -319,10 +347,11 @@ export default class KeyvGithub
|
|
|
319
347
|
this.toPath(key),
|
|
320
348
|
String(value),
|
|
321
349
|
]);
|
|
350
|
+
const paths = entries.map(([p]) => p);
|
|
322
351
|
const message =
|
|
323
352
|
entries.length === 1
|
|
324
353
|
? this.msg(entries[0]![0], entries[0]![1])
|
|
325
|
-
:
|
|
354
|
+
: this.batchMsg("set", paths);
|
|
326
355
|
await this._batchCommit({ set: entries, message });
|
|
327
356
|
}
|
|
328
357
|
|
|
@@ -361,7 +390,7 @@ export default class KeyvGithub
|
|
|
361
390
|
const message =
|
|
362
391
|
toDelete.length === 1
|
|
363
392
|
? this.msg(toDelete[0]!, null)
|
|
364
|
-
:
|
|
393
|
+
: this.batchMsg("delete", toDelete);
|
|
365
394
|
await this._batchCommit({ delete: toDelete, message });
|
|
366
395
|
return true;
|
|
367
396
|
}
|
|
@@ -397,7 +426,7 @@ export default class KeyvGithub
|
|
|
397
426
|
if (allPaths.length > 0) {
|
|
398
427
|
await this._batchCommit({
|
|
399
428
|
delete: allPaths,
|
|
400
|
-
message:
|
|
429
|
+
message: this.batchMsg("clear", allPaths),
|
|
401
430
|
});
|
|
402
431
|
}
|
|
403
432
|
}
|