Skip to content

Commit a4ec41e

Browse files
committed
Accept a structural Netlify Blobs store
Replace the public SDK class parameter with the methods the adapter needs. This prevents bundled private Store declarations from rejecting real SDK instances and keeps Blobs optional. Test doubles now work without an unsafe cast. Compile the documented SDK usage against the built declarations to cover the npm consumer API. Package checks and the Deno, Node.js, and Bun suites passed; the credentialed integration test was skipped. Codex assisted with implementation, regression tests, and this commit. #1029 (comment) Assisted-by: Codex:gpt-5.6-sol Assisted-by: Codex:gpt-6-astra
1 parent 3472a08 commit a4ec41e

6 files changed

Lines changed: 104 additions & 6 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getStore } from "@netlify/blobs";
2+
import { NetlifyBlobsKvStore } from "../dist/mod.js";
3+
4+
new NetlifyBlobsKvStore(getStore({
5+
name: "fedify",
6+
consistency: "strong",
7+
}));

‎packages/netlify/src/kv.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import type {
44
KvStoreListEntry,
55
KvStoreSetOptions,
66
} from "@fedify/fedify/federation";
7-
import type { Store } from "@netlify/blobs";
87
import { decodeBase64Url, encodeBase64Url } from "byte-encodings/base64url";
98
import { isEqual } from "es-toolkit";
9+
import type { NetlifyBlobsStore } from "./types.ts";
1010

1111
const MAX_BLOB_KEY_BYTES = 600;
1212
const BLOB_KEY_PREFIX = "fedify1.";
@@ -19,13 +19,13 @@ const textDecoder = new TextDecoder();
1919
* @since 2.4.0
2020
*/
2121
export class NetlifyBlobsKvStore implements KvStore {
22-
readonly #store: Store;
22+
readonly #store: NetlifyBlobsStore;
2323

2424
/**
2525
* Creates a new Netlify Blobs-backed key-value store.
2626
* @param store The Netlify Blobs store to use.
2727
*/
28-
constructor(store: Store) {
28+
constructor(store: NetlifyBlobsStore) {
2929
this.#store = store;
3030
}
3131

‎packages/netlify/src/mod.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ export {
1818
NetlifyMessageQueueSendError,
1919
type NetlifyQueueEventData,
2020
} from "./mq.ts";
21-
export type { NetlifyAsyncWorkloadsClient } from "./types.ts";
21+
export type {
22+
NetlifyAsyncWorkloadsClient,
23+
NetlifyBlobsStore,
24+
} from "./types.ts";

‎packages/netlify/src/types.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,63 @@
11
import type { NetlifyQueueEventData } from "./mq.ts";
22

3+
/**
4+
* The subset of Netlify's Blobs `Store` used by this package.
5+
*
6+
* The real Netlify Blobs `Store` satisfies this interface. The narrower
7+
* interface also keeps Netlify Blobs optional and makes it possible to supply
8+
* a test double.
9+
*
10+
* @since 2.4.0
11+
*/
12+
export interface NetlifyBlobsStore {
13+
getWithMetadata(
14+
key: string,
15+
options: {
16+
readonly consistency?: "eventual" | "strong";
17+
readonly type: "json";
18+
},
19+
): Promise<
20+
{
21+
readonly data: unknown;
22+
readonly etag?: string;
23+
readonly metadata: Record<string, unknown>;
24+
} | null
25+
>;
26+
27+
setJSON(
28+
key: string,
29+
data: unknown,
30+
options?:
31+
& {
32+
readonly metadata?: Record<string, unknown>;
33+
}
34+
& (
35+
| {
36+
readonly onlyIfNew?: boolean;
37+
readonly onlyIfMatch?: never;
38+
}
39+
| {
40+
readonly onlyIfMatch?: string;
41+
readonly onlyIfNew?: never;
42+
}
43+
),
44+
): Promise<{
45+
readonly etag?: string;
46+
readonly modified: boolean;
47+
}>;
48+
49+
delete(key: string): Promise<void>;
50+
51+
list(options: {
52+
readonly paginate: true;
53+
readonly prefix?: string;
54+
}): AsyncIterable<{
55+
readonly blobs: readonly {
56+
readonly key: string;
57+
}[];
58+
}>;
59+
}
60+
361
/**
462
* The subset of Netlify's `AsyncWorkloadsClient` used by this package.
563
*

‎packages/netlify/test/kv.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "temporal-polyfill/global";
22
import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
33
import { describe, it } from "node:test";
44
import type { KvKey } from "@fedify/fedify/federation";
5-
import { getStore, type SetOptions, type Store } from "@netlify/blobs";
5+
import { getStore, type SetOptions } from "@netlify/blobs";
66
import { encodeBase64Url } from "byte-encodings/base64url";
77
import { NetlifyBlobsKvStore } from "../src/kv.ts";
88

@@ -90,7 +90,7 @@ class MockStore {
9090
}
9191

9292
function createKv(store: MockStore): NetlifyBlobsKvStore {
93-
return new NetlifyBlobsKvStore(store as unknown as Store);
93+
return new NetlifyBlobsKvStore(store);
9494
}
9595

9696
function encodeKey(key: KvKey): string {

‎packages/netlify/test/package.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import { execFileSync } from "node:child_process";
12
import { createRequire } from "node:module";
23
import assert from "node:assert/strict";
34
import { describe, it } from "node:test";
5+
import process from "node:process";
6+
import { fileURLToPath } from "node:url";
7+
8+
const canRunTypeScript = !("Deno" in globalThis);
49

510
describe("package exports", () => {
611
it("loads the ESM distribution", async () => {
@@ -15,4 +20,29 @@ describe("package exports", () => {
1520
assert.equal(typeof module.NetlifyMessageQueue, "function");
1621
assert.equal(typeof module.createNetlifyQueueHandler, "function");
1722
});
23+
24+
it("accepts the Netlify Blobs Store in the public types", {
25+
skip: !canRunTypeScript,
26+
}, () => {
27+
const require = createRequire(import.meta.url);
28+
const tsc = require.resolve("typescript/bin/tsc");
29+
const fixture = fileURLToPath(
30+
new URL("../fixtures/public-types.ts", import.meta.url),
31+
);
32+
33+
execFileSync(process.execPath, [
34+
tsc,
35+
"--noEmit",
36+
"--strict",
37+
"--types",
38+
"node",
39+
"--module",
40+
"NodeNext",
41+
"--moduleResolution",
42+
"NodeNext",
43+
"--target",
44+
"ESNext",
45+
fixture,
46+
]);
47+
});
1848
});

0 commit comments

Comments
 (0)