@hot-updater/cloudflare 0.36.6 → 1.0.0-rc.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/dist/iac/index.cjs +989 -11
- package/dist/iac/index.mjs +992 -15
- package/dist/index.cjs +919 -6989
- package/dist/index.d.cts +11 -47
- package/dist/index.d.mts +11 -47
- package/dist/index.mjs +917 -7006
- package/dist/worker/index.cjs +898 -336
- package/dist/worker/index.d.cts +18 -33
- package/dist/worker/index.d.mts +18 -33
- package/dist/worker/index.mjs +899 -331
- package/package.json +7 -10
- package/sql/bundles.sql +183 -35
- package/src/d1Database.spec.ts +143 -392
- package/src/d1Database.ts +55 -650
- package/src/d1Implementation.spec.ts +252 -0
- package/src/d1Implementation.ts +742 -0
- package/src/d1Query.spec.ts +46 -0
- package/src/d1Query.ts +89 -0
- package/src/d1Rows.spec.ts +111 -0
- package/src/d1Rows.ts +326 -0
- package/src/d1Sql.spec.ts +30 -0
- package/src/d1Sql.ts +175 -0
- package/src/r2S3Storage.ts +93 -137
- package/src/r2Storage.spec.ts +120 -218
- package/src/r2Storage.ts +13 -61
- package/src/r2WorkerStorage.spec.ts +161 -50
- package/src/r2WorkerStorage.ts +85 -118
- package/src/worker/d1Database.spec.ts +33 -0
- package/src/worker/d1Database.ts +52 -0
- package/src/worker/index.ts +3 -31
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +5968 -4701
- package/worker/dist/index.js.map +4 -4
- package/worker/migrations/0001_hot-updater_1.0.0.sql +197 -0
- package/worker/migrations/0002_hot-updater_artifact_byte_sizes.sql +11 -0
- package/worker/src/databaseMigration.integration.spec.ts +114 -0
- package/worker/src/databasePlugins.integration.spec.ts +292 -0
- package/worker/src/index.integration.spec.ts +102 -294
- package/worker/src/index.ts +14 -66
- package/worker/src/storagePlugin.integration.spec.ts +59 -0
- package/worker/wrangler.json +5 -2
- package/src/cloudflareWorkerDatabase.spec.ts +0 -260
- package/src/cloudflareWorkerDatabase.ts +0 -651
- package/src/r2WranglerStorage.ts +0 -193
- package/worker/migrations/0001_hot-updater_init.sql +0 -16
- package/worker/migrations/0002_hot-updater_0.13.0.sql +0 -8
- package/worker/migrations/0003_hot-updater_0.18.0.sql +0 -38
- package/worker/migrations/0004_hot-updater_0.29.0.sql +0 -11
- package/worker/migrations/0005_hot-updater_0.31.0.sql +0 -24
|
@@ -1,73 +1,184 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
type CloudflareWorkerStorageEnv,
|
|
6
|
-
r2WorkerStorage,
|
|
7
|
-
} from "./r2WorkerStorage";
|
|
3
|
+
import { r2WorkerStorage } from "./r2WorkerStorage";
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
const createBucket = (
|
|
6
|
+
get: ReturnType<typeof vi.fn>,
|
|
7
|
+
overrides: Partial<R2Bucket> = {},
|
|
8
|
+
) =>
|
|
9
|
+
({
|
|
10
|
+
get,
|
|
11
|
+
head: vi.fn(),
|
|
12
|
+
put: vi.fn(),
|
|
13
|
+
delete: vi.fn(),
|
|
14
|
+
list: vi.fn(),
|
|
15
|
+
...overrides,
|
|
16
|
+
}) as unknown as R2Bucket;
|
|
10
17
|
|
|
11
18
|
describe("r2WorkerStorage", () => {
|
|
12
|
-
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.unstubAllGlobals();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("reads bytes from the R2 binding captured by the implementation", async () => {
|
|
13
24
|
const get = vi.fn(async (key: string) => ({
|
|
14
|
-
|
|
25
|
+
body: new Response(`text:${key}`).body,
|
|
26
|
+
httpEtag: '"etag"',
|
|
27
|
+
size: `text:${key}`.length,
|
|
28
|
+
writeHttpMetadata: vi.fn(),
|
|
15
29
|
}));
|
|
16
30
|
const storage = r2WorkerStorage({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
bucket: createBucket(get),
|
|
32
|
+
bucketName: "bundles",
|
|
33
|
+
downloadUrlSigningKey: "test-signing-key",
|
|
34
|
+
});
|
|
20
35
|
|
|
21
|
-
await
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
27
|
-
request: new Request("https://updates.example.com"),
|
|
28
|
-
} satisfies TestContext),
|
|
29
|
-
).resolves.toBe("text:app/manifest.json");
|
|
36
|
+
const { response } = await storage.get({
|
|
37
|
+
storageUri: "r2://bundles/app/manifest.json",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(await response?.text()).toBe("text:app/manifest.json");
|
|
30
41
|
expect(get).toHaveBeenCalledWith("app/manifest.json");
|
|
31
42
|
});
|
|
32
43
|
|
|
33
|
-
it("
|
|
34
|
-
const get = vi.fn(
|
|
35
|
-
text: async () => `text:${key}`,
|
|
36
|
-
}));
|
|
44
|
+
it("rejects URIs owned by a different R2 binding", async () => {
|
|
45
|
+
const get = vi.fn();
|
|
37
46
|
const storage = r2WorkerStorage({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
bucket: createBucket(get),
|
|
48
|
+
bucketName: "bundles",
|
|
49
|
+
downloadUrlSigningKey: "test-signing-key",
|
|
50
|
+
});
|
|
41
51
|
|
|
42
|
-
await
|
|
43
|
-
"r2://
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
BUCKET: { get },
|
|
47
|
-
JWT_SECRET: "secret",
|
|
48
|
-
},
|
|
49
|
-
request: new Request("https://updates.example.com"),
|
|
50
|
-
} satisfies TestContext,
|
|
52
|
+
await expect(
|
|
53
|
+
storage.get({ storageUri: "r2://other/app/manifest.json" }),
|
|
54
|
+
).rejects.toThrow(
|
|
55
|
+
'Bucket name mismatch: expected "bundles", but found "other".',
|
|
51
56
|
);
|
|
52
|
-
|
|
53
|
-
expect(get).toHaveBeenCalledWith("assets/bootsplash/logo-ios@2x.png");
|
|
57
|
+
expect(get).not.toHaveBeenCalled();
|
|
54
58
|
});
|
|
55
59
|
|
|
56
|
-
it("
|
|
60
|
+
it("writes and deletes the exact object key through the R2 binding", async () => {
|
|
61
|
+
let uploaded = "";
|
|
62
|
+
const put = vi.fn(async (_key: string, body: Uint8Array) => {
|
|
63
|
+
uploaded = new TextDecoder().decode(body);
|
|
64
|
+
return null;
|
|
65
|
+
});
|
|
66
|
+
const bucket = createBucket(vi.fn(), {
|
|
67
|
+
put: put as unknown as R2Bucket["put"],
|
|
68
|
+
});
|
|
57
69
|
const storage = r2WorkerStorage({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
bucket,
|
|
71
|
+
bucketName: "bundles",
|
|
72
|
+
downloadUrlSigningKey: "test-signing-key",
|
|
73
|
+
});
|
|
61
74
|
|
|
75
|
+
await storage.put({
|
|
76
|
+
key: "releases/bundle.zip",
|
|
77
|
+
body: new Response("bundle").body!,
|
|
78
|
+
contentType: "application/zip",
|
|
79
|
+
});
|
|
62
80
|
await expect(
|
|
63
|
-
storage.
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
storage.delete({
|
|
82
|
+
storageUri: "r2://bundles/releases/bundle.zip",
|
|
83
|
+
}),
|
|
84
|
+
).resolves.toEqual({
|
|
85
|
+
deleted: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(bucket.put).toHaveBeenCalledWith(
|
|
89
|
+
"releases/bundle.zip",
|
|
90
|
+
expect.any(Uint8Array),
|
|
91
|
+
{
|
|
92
|
+
httpMetadata: {
|
|
93
|
+
cacheControl: "max-age=31536000",
|
|
94
|
+
contentType: "application/zip",
|
|
66
95
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
).
|
|
70
|
-
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
expect(uploaded).toBe("bundle");
|
|
99
|
+
expect(bucket.delete).toHaveBeenCalledWith("releases/bundle.zip");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("round-trips reserved characters without changing the R2 binding key", async () => {
|
|
103
|
+
const bucket = createBucket(vi.fn());
|
|
104
|
+
const storage = r2WorkerStorage({
|
|
105
|
+
bucket,
|
|
106
|
+
bucketName: "bundles",
|
|
107
|
+
downloadUrlSigningKey: "test-signing-key",
|
|
108
|
+
});
|
|
109
|
+
const key = "릴리스 folder/logo@2x #100%/bundle.zip";
|
|
110
|
+
|
|
111
|
+
const uploaded = await storage.put({
|
|
112
|
+
key,
|
|
113
|
+
body: new Response("bundle").body!,
|
|
114
|
+
contentType: "application/zip",
|
|
115
|
+
});
|
|
116
|
+
expect(uploaded.storageUri).toContain("logo%402x");
|
|
117
|
+
await storage.delete({ storageUri: uploaded.storageUri });
|
|
118
|
+
await storage.delete({ storageUri: uploaded.storageUri });
|
|
119
|
+
|
|
120
|
+
expect(bucket.put).toHaveBeenCalledWith(
|
|
121
|
+
key,
|
|
122
|
+
expect.any(Uint8Array),
|
|
123
|
+
expect.any(Object),
|
|
124
|
+
);
|
|
125
|
+
expect(bucket.delete).toHaveBeenNthCalledWith(1, key);
|
|
126
|
+
expect(bucket.delete).toHaveBeenNthCalledWith(2, key);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("uses a fixed-length stream when the upload length is known", async () => {
|
|
130
|
+
const expectedLengths: number[] = [];
|
|
131
|
+
class TestFixedLengthStream extends TransformStream<
|
|
132
|
+
Uint8Array,
|
|
133
|
+
Uint8Array
|
|
134
|
+
> {
|
|
135
|
+
constructor(expectedLength: number) {
|
|
136
|
+
super();
|
|
137
|
+
expectedLengths.push(expectedLength);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
vi.stubGlobal("FixedLengthStream", TestFixedLengthStream);
|
|
141
|
+
let uploaded = "";
|
|
142
|
+
const put = vi.fn(
|
|
143
|
+
async (_key: string, body: ReadableStream<Uint8Array>) => {
|
|
144
|
+
uploaded = await new Response(body).text();
|
|
145
|
+
return null;
|
|
146
|
+
},
|
|
71
147
|
);
|
|
148
|
+
const storage = r2WorkerStorage({
|
|
149
|
+
bucket: createBucket(vi.fn(), {
|
|
150
|
+
put: put as unknown as R2Bucket["put"],
|
|
151
|
+
}),
|
|
152
|
+
bucketName: "bundles",
|
|
153
|
+
downloadUrlSigningKey: "test-signing-key",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await storage.put({
|
|
157
|
+
key: "releases/bundle.zip",
|
|
158
|
+
body: new Response("bundle").body!,
|
|
159
|
+
contentLength: 6,
|
|
160
|
+
contentType: "application/zip",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(expectedLengths).toEqual([6]);
|
|
164
|
+
expect(uploaded).toBe("bundle");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("returns a stable signed download path for private R2 objects", async () => {
|
|
168
|
+
const storage = r2WorkerStorage({
|
|
169
|
+
bucket: createBucket(vi.fn()),
|
|
170
|
+
bucketName: "bundles",
|
|
171
|
+
downloadUrlSigningKey: "test-signing-key",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const first = await storage.getDownloadUrl({
|
|
175
|
+
storageUri: "r2://bundles/releases/bundle.zip",
|
|
176
|
+
});
|
|
177
|
+
const second = await storage.getDownloadUrl({
|
|
178
|
+
storageUri: "r2://bundles/releases/bundle.zip",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(first).toEqual(second);
|
|
182
|
+
expect(first.url).toMatch(/^\/storage\//);
|
|
72
183
|
});
|
|
73
184
|
});
|
package/src/r2WorkerStorage.ts
CHANGED
|
@@ -1,127 +1,94 @@
|
|
|
1
|
-
import { signToken } from "@hot-updater/js";
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
createStorageKeyBuilder,
|
|
3
|
+
createStorageDownloadUrl,
|
|
4
|
+
createStoragePlugin,
|
|
5
|
+
createStorageUri,
|
|
6
|
+
parseStorageUri,
|
|
7
|
+
type StoragePluginWith,
|
|
7
8
|
} from "@hot-updater/plugin-core";
|
|
8
9
|
|
|
9
|
-
export interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type ContextResolver<TContext, TValue> = (
|
|
17
|
-
context?: HotUpdaterContext<TContext>,
|
|
18
|
-
) => TValue | Promise<TValue>;
|
|
19
|
-
|
|
20
|
-
export interface CloudflareWorkerStorageConfig<
|
|
21
|
-
TContext extends RequestEnvContext<CloudflareWorkerStorageEnv>,
|
|
22
|
-
> {
|
|
23
|
-
jwtSecret?: string | ContextResolver<TContext, string>;
|
|
24
|
-
publicBaseUrl: string | ContextResolver<TContext, string>;
|
|
10
|
+
export interface CloudflareWorkerStorageConfig {
|
|
11
|
+
readonly bucket: R2Bucket;
|
|
12
|
+
readonly bucketName: string;
|
|
13
|
+
readonly basePath?: string;
|
|
14
|
+
readonly downloadUrlSigningKey: string;
|
|
25
15
|
}
|
|
26
16
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return jwtSecret;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const resolveR2BucketFromContext = (
|
|
51
|
-
context?: RequestEnvContext<CloudflareWorkerStorageEnv>,
|
|
52
|
-
) => {
|
|
53
|
-
const bucket = context?.env?.BUCKET;
|
|
54
|
-
|
|
55
|
-
if (!bucket) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
"r2WorkerStorage requires env.BUCKET in the hot updater context.",
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return bucket;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const createPublicObjectPath = (storageUrl: URL) =>
|
|
65
|
-
`${storageUrl.host}${storageUrl.pathname}`;
|
|
66
|
-
|
|
67
|
-
const createR2ObjectKey = (storageUrl: URL) =>
|
|
68
|
-
decodeStorageObjectKey(storageUrl.pathname.replace(/^\/+/, ""));
|
|
69
|
-
|
|
70
|
-
export const r2WorkerStorage = <
|
|
71
|
-
TContext extends RequestEnvContext<CloudflareWorkerStorageEnv> =
|
|
72
|
-
RequestEnvContext<CloudflareWorkerStorageEnv>,
|
|
73
|
-
>(
|
|
74
|
-
config: CloudflareWorkerStorageConfig<TContext>,
|
|
75
|
-
) => {
|
|
76
|
-
return createRuntimeStoragePlugin<
|
|
77
|
-
CloudflareWorkerStorageConfig<TContext>,
|
|
78
|
-
TContext
|
|
79
|
-
>({
|
|
80
|
-
name: "r2WorkerStorage",
|
|
81
|
-
supportedProtocol: "r2",
|
|
82
|
-
factory: (config) => ({
|
|
83
|
-
async readText(storageUri, context) {
|
|
84
|
-
const storageUrl = new URL(storageUri);
|
|
85
|
-
|
|
86
|
-
if (storageUrl.protocol !== "r2:") {
|
|
87
|
-
throw new Error("Invalid R2 storage URI protocol");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const bucket = resolveR2BucketFromContext(context);
|
|
91
|
-
const key = createR2ObjectKey(storageUrl);
|
|
92
|
-
const object = await bucket.get(key);
|
|
93
|
-
if (!object) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return object.text();
|
|
98
|
-
},
|
|
99
|
-
async getDownloadUrl(storageUri, context) {
|
|
100
|
-
const storageUrl = new URL(storageUri);
|
|
101
|
-
|
|
102
|
-
if (storageUrl.protocol !== "r2:") {
|
|
103
|
-
throw new Error("Invalid R2 storage URI protocol");
|
|
104
|
-
}
|
|
17
|
+
export const r2WorkerStorage = (
|
|
18
|
+
config: CloudflareWorkerStorageConfig,
|
|
19
|
+
): StoragePluginWith<
|
|
20
|
+
"put" | "get" | "getDownloadUrl" | "exists" | "delete"
|
|
21
|
+
> => {
|
|
22
|
+
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
23
|
+
const getDownloadUrl = createStorageDownloadUrl(config.downloadUrlSigningKey);
|
|
24
|
+
|
|
25
|
+
const parseAndValidate = (storageUri: string) => {
|
|
26
|
+
const parsed = parseStorageUri(storageUri, "r2");
|
|
27
|
+
if (parsed.bucket !== config.bucketName) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
};
|
|
105
34
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
35
|
+
return createStoragePlugin({
|
|
36
|
+
name: "r2Storage",
|
|
37
|
+
protocol: "r2",
|
|
38
|
+
async put({ key, body, contentLength, contentType }) {
|
|
39
|
+
const storageKey = getStorageKey(key);
|
|
40
|
+
const uploadOptions = {
|
|
41
|
+
httpMetadata: {
|
|
42
|
+
contentType,
|
|
43
|
+
cacheControl: "max-age=31536000",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
if (contentLength === undefined) {
|
|
47
|
+
const bufferedBody = new Uint8Array(
|
|
48
|
+
await new Response(body).arrayBuffer(),
|
|
49
|
+
);
|
|
50
|
+
await config.bucket.put(storageKey, bufferedBody, uploadOptions);
|
|
51
|
+
} else {
|
|
52
|
+
const fixedLengthBody = new FixedLengthStream(contentLength);
|
|
53
|
+
await Promise.all([
|
|
54
|
+
body.pipeTo(fixedLengthBody.writable),
|
|
55
|
+
config.bucket.put(
|
|
56
|
+
storageKey,
|
|
57
|
+
fixedLengthBody.readable,
|
|
58
|
+
uploadOptions,
|
|
111
59
|
),
|
|
112
|
-
resolveContextValue(config.publicBaseUrl, context),
|
|
113
60
|
]);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
storageUri: createStorageUri({
|
|
64
|
+
protocol: "r2",
|
|
65
|
+
bucket: config.bucketName,
|
|
66
|
+
key: storageKey,
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
async get({ storageUri }) {
|
|
71
|
+
const { key } = parseAndValidate(storageUri);
|
|
72
|
+
const object = await config.bucket.get(key);
|
|
73
|
+
if (!object) return { response: null };
|
|
74
|
+
const headers = new Headers();
|
|
75
|
+
object.writeHttpMetadata(headers);
|
|
76
|
+
headers.set("etag", object.httpEtag);
|
|
77
|
+
headers.set("content-length", String(object.size));
|
|
78
|
+
return { response: new Response(object.body, { headers }) };
|
|
79
|
+
},
|
|
80
|
+
async getDownloadUrl({ storageUri }) {
|
|
81
|
+
parseAndValidate(storageUri);
|
|
82
|
+
return getDownloadUrl({ storageUri });
|
|
83
|
+
},
|
|
84
|
+
async exists({ storageUri }) {
|
|
85
|
+
const { key } = parseAndValidate(storageUri);
|
|
86
|
+
return { exists: (await config.bucket.head(key)) !== null };
|
|
87
|
+
},
|
|
88
|
+
async delete({ storageUri }) {
|
|
89
|
+
const { key } = parseAndValidate(storageUri);
|
|
90
|
+
await config.bucket.delete(key);
|
|
91
|
+
return { deleted: true };
|
|
92
|
+
},
|
|
93
|
+
});
|
|
127
94
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { d1Database, type D1Like } from "./d1Database";
|
|
4
|
+
|
|
5
|
+
const database: D1Like = {
|
|
6
|
+
prepare: () => ({
|
|
7
|
+
bind: () => ({
|
|
8
|
+
all: async () => ({
|
|
9
|
+
results: [{ id: "channel-production", name: "production" }],
|
|
10
|
+
}),
|
|
11
|
+
}),
|
|
12
|
+
}),
|
|
13
|
+
batch: async (statements) =>
|
|
14
|
+
Promise.all(statements.map((item) => item.all())),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
it("uses the D1 binding supplied at the Worker composition boundary", async () => {
|
|
18
|
+
const plugin = d1Database(database);
|
|
19
|
+
|
|
20
|
+
expect(plugin.name).toBe("d1Database");
|
|
21
|
+
expect(Object.keys(plugin.models).sort()).toEqual([
|
|
22
|
+
"analytics",
|
|
23
|
+
"apiKeys",
|
|
24
|
+
"bundlePatches",
|
|
25
|
+
"bundles",
|
|
26
|
+
"channels",
|
|
27
|
+
"releaseCatalogs",
|
|
28
|
+
"releases",
|
|
29
|
+
]);
|
|
30
|
+
await expect(plugin.models.channels.list({})).resolves.toEqual({
|
|
31
|
+
channels: [{ id: "channel-production", name: "production" }],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createDatabasePlugin } from "@hot-updater/plugin-core";
|
|
2
|
+
import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
|
|
3
|
+
|
|
4
|
+
import { createD1Implementation } from "../d1Implementation";
|
|
5
|
+
|
|
6
|
+
type D1Result = {
|
|
7
|
+
readonly results?: readonly unknown[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type D1BoundStatement = {
|
|
11
|
+
all: () => Promise<D1Result>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type D1PreparedStatement = {
|
|
15
|
+
bind: (...values: readonly unknown[]) => D1BoundStatement;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type D1Like = {
|
|
19
|
+
prepare(sql: string): D1PreparedStatement;
|
|
20
|
+
batch(statements: D1BoundStatement[]): Promise<readonly D1Result[]>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface CloudflareWorkerDatabaseEnv {
|
|
24
|
+
readonly DB: D1Like;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const d1Database = (database: D1Like) => {
|
|
28
|
+
const implementation = createD1Implementation({
|
|
29
|
+
async query(sql, params) {
|
|
30
|
+
const result = await database
|
|
31
|
+
.prepare(sql)
|
|
32
|
+
.bind(...params)
|
|
33
|
+
.all();
|
|
34
|
+
return result.results ?? [];
|
|
35
|
+
},
|
|
36
|
+
async batch(statements) {
|
|
37
|
+
const results = await database.batch(
|
|
38
|
+
statements.map(({ sql, params }) =>
|
|
39
|
+
database.prepare(sql).bind(...params),
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
return results.map(({ results }) => results ?? []);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const adapter = createDatabasePluginAdapter("d1Database", implementation);
|
|
46
|
+
return createDatabasePlugin({
|
|
47
|
+
name: "d1Database",
|
|
48
|
+
models: adapter.models,
|
|
49
|
+
commit: adapter.commit,
|
|
50
|
+
...(adapter.dispose ? { dispose: adapter.dispose } : {}),
|
|
51
|
+
});
|
|
52
|
+
};
|
package/src/worker/index.ts
CHANGED
|
@@ -1,33 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
type CloudflareWorkerDatabaseEnv,
|
|
7
|
-
d1WorkerDatabase,
|
|
8
|
-
} from "../cloudflareWorkerDatabase";
|
|
9
|
-
import {
|
|
1
|
+
export { d1Database, type D1Like } from "./d1Database";
|
|
2
|
+
export {
|
|
3
|
+
r2WorkerStorage as r2Storage,
|
|
10
4
|
type CloudflareWorkerStorageConfig,
|
|
11
|
-
type CloudflareWorkerStorageEnv,
|
|
12
|
-
r2WorkerStorage,
|
|
13
5
|
} from "../r2WorkerStorage";
|
|
14
|
-
|
|
15
|
-
export type { CloudflareWorkerDatabaseEnv, CloudflareWorkerStorageEnv };
|
|
16
|
-
|
|
17
|
-
export interface CloudflareWorkerRuntimeEnv
|
|
18
|
-
extends CloudflareWorkerDatabaseEnv, CloudflareWorkerStorageEnv {}
|
|
19
|
-
|
|
20
|
-
export type RequestEnvContext<TEnv = CloudflareWorkerRuntimeEnv> =
|
|
21
|
-
BaseRequestEnvContext<TEnv>;
|
|
22
|
-
|
|
23
|
-
export const d1Database = <
|
|
24
|
-
TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> =
|
|
25
|
-
RequestEnvContext<CloudflareWorkerRuntimeEnv>,
|
|
26
|
-
>() => d1WorkerDatabase<TContext>();
|
|
27
|
-
|
|
28
|
-
export const r2Storage = <
|
|
29
|
-
TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> =
|
|
30
|
-
RequestEnvContext<CloudflareWorkerRuntimeEnv>,
|
|
31
|
-
>(
|
|
32
|
-
config: CloudflareWorkerStorageConfig<TContext>,
|
|
33
|
-
) => r2WorkerStorage<TContext>(config);
|
package/worker/dist/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
This folder contains the built output assets for the worker "hot-updater" generated at 2026-08-
|
|
1
|
+
This folder contains the built output assets for the worker "hot-updater" generated at 2026-08-31T01:29:11.045Z.
|