@hot-updater/cloudflare 1.0.0-rc.2 → 1.0.0-rc.21

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.
Files changed (51) hide show
  1. package/dist/index.cjs +95 -870
  2. package/dist/index.d.cts +724 -6
  3. package/dist/index.d.mts +724 -6
  4. package/dist/index.mjs +93 -870
  5. package/dist/init/index.cjs +11768 -0
  6. package/dist/init/index.d.cts +5 -1
  7. package/dist/init/index.d.mts +5 -1
  8. package/dist/init/index.mjs +11761 -1
  9. package/dist/worker/index.cjs +59 -859
  10. package/dist/worker/index.d.cts +725 -11
  11. package/dist/worker/index.d.mts +725 -11
  12. package/dist/worker/index.mjs +62 -863
  13. package/package.json +9 -13
  14. package/sql/bundles.sql +109 -190
  15. package/src/d1Database.ts +61 -40
  16. package/src/d1Executor.ts +84 -0
  17. package/src/d1Schema.ts +26 -0
  18. package/src/index.ts +1 -0
  19. package/src/plugins.ts +8 -0
  20. package/src/r2S3Storage.ts +4 -4
  21. package/src/r2Storage.ts +2 -2
  22. package/src/r2WorkerStorage.ts +4 -4
  23. package/src/worker/d1Database.ts +21 -27
  24. package/src/worker/index.ts +1 -0
  25. package/worker/dist/README.md +1 -1
  26. package/worker/dist/index.js +9682 -6206
  27. package/worker/dist/index.js.map +4 -4
  28. package/worker/migrations/0001_hot-updater_1.0.0.sql +109 -205
  29. package/worker/src/index.ts +2 -2
  30. package/dist/iac/index.cjs +0 -12566
  31. package/dist/iac/index.d.cts +0 -9
  32. package/dist/iac/index.d.mts +0 -9
  33. package/dist/iac/index.mjs +0 -12558
  34. package/src/d1Database.spec.ts +0 -178
  35. package/src/d1Implementation.spec.ts +0 -252
  36. package/src/d1Implementation.ts +0 -742
  37. package/src/d1Query.spec.ts +0 -46
  38. package/src/d1Query.ts +0 -89
  39. package/src/d1Rows.spec.ts +0 -111
  40. package/src/d1Rows.ts +0 -326
  41. package/src/d1Sql.spec.ts +0 -30
  42. package/src/d1Sql.ts +0 -175
  43. package/src/r2Storage.spec.ts +0 -346
  44. package/src/r2WorkerStorage.spec.ts +0 -184
  45. package/src/utils/createWrangler.spec.ts +0 -75
  46. package/src/worker/d1Database.spec.ts +0 -33
  47. package/worker/src/databaseMigration.integration.spec.ts +0 -130
  48. package/worker/src/databasePlugins.integration.spec.ts +0 -292
  49. package/worker/src/index.integration.spec.ts +0 -190
  50. package/worker/src/storagePlugin.integration.spec.ts +0 -59
  51. package/worker/src/testWorker.ts +0 -5
@@ -1,75 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const mocks = vi.hoisted(() => ({
4
- command: vi.fn(),
5
- execa: vi.fn(),
6
- }));
7
-
8
- vi.mock("execa", () => ({
9
- execa: mocks.execa,
10
- }));
11
-
12
- import { createWrangler } from "./createWrangler";
13
-
14
- describe("createWrangler", () => {
15
- beforeEach(() => {
16
- vi.clearAllMocks();
17
- mocks.execa.mockReturnValue(mocks.command);
18
- mocks.command.mockResolvedValue({});
19
- });
20
-
21
- it("marks replay commands as CI so Wrangler skips nested prompts", async () => {
22
- const wrangler = createWrangler({
23
- accountId: "account-id",
24
- cloudflareApiToken: "api-token",
25
- cwd: "/tmp/cloudflare-init",
26
- nonInteractive: true,
27
- stdio: "inherit",
28
- });
29
-
30
- await wrangler("d1", "migrations", "apply", "hot-updater", "--remote");
31
-
32
- expect(mocks.execa).toHaveBeenCalledWith({
33
- cwd: "/tmp/cloudflare-init",
34
- env: {
35
- CI: "true",
36
- CLOUDFLARE_ACCOUNT_ID: "account-id",
37
- CLOUDFLARE_API_TOKEN: "api-token",
38
- },
39
- extendsEnv: true,
40
- shell: false,
41
- stdio: "inherit",
42
- });
43
- expect(mocks.command).toHaveBeenCalledWith("npx", [
44
- "wrangler",
45
- "d1",
46
- "migrations",
47
- "apply",
48
- "hot-updater",
49
- "--remote",
50
- ]);
51
- });
52
-
53
- it("passes shell metacharacters as one argument", async () => {
54
- const wrangler = createWrangler({
55
- accountId: "account-id",
56
- cloudflareApiToken: "api-token",
57
- cwd: "/tmp/cloudflare-init",
58
- stdio: "inherit",
59
- });
60
-
61
- await wrangler("deploy", "--name", "safe; printf INJECTED");
62
-
63
- expect(mocks.execa).toHaveBeenCalledWith(
64
- expect.objectContaining({
65
- shell: false,
66
- }),
67
- );
68
- expect(mocks.command).toHaveBeenCalledWith("npx", [
69
- "wrangler",
70
- "deploy",
71
- "--name",
72
- "safe; printf INJECTED",
73
- ]);
74
- });
75
- });
@@ -1,33 +0,0 @@
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
- "apiKeys",
23
- "bundlePatches",
24
- "bundles",
25
- "channels",
26
- "insights",
27
- "releaseCatalogs",
28
- "releases",
29
- ]);
30
- await expect(plugin.models.channels.list({})).resolves.toEqual({
31
- channels: [{ id: "channel-production", name: "production" }],
32
- });
33
- });
@@ -1,130 +0,0 @@
1
- import { env } from "cloudflare:test";
2
- import { expect, inject, it } from "vitest";
3
-
4
- declare module "vitest" {
5
- export interface ProvidedContext {
6
- d1Migrations: readonly {
7
- readonly name: string;
8
- readonly sql: string;
9
- }[];
10
- }
11
- }
12
-
13
- it("ships a single D1 1.0.0 CREATE migration", () => {
14
- expect(inject("d1Migrations").map(({ name }) => name)).toEqual([
15
- "0001_hot-updater_1.0.0.sql",
16
- ]);
17
- });
18
-
19
- it("creates the current schema with required artifact sizes", async () => {
20
- const [createMigration] = inject("d1Migrations");
21
- await env.DB.prepare(createMigration!.sql).run();
22
- await env.DB.prepare(`
23
- INSERT INTO bundles (
24
- id, platform, file_hash, storage_uri, archive_byte_size, metadata
25
- ) VALUES (
26
- '00000000-0000-0000-0000-000000000001', 'ios', 'hash',
27
- 'storage://bundle', 3000000001, '{}'
28
- )
29
- `).run();
30
- await env.DB.prepare(`
31
- INSERT INTO bundle_patches (
32
- id, bundle_id, base_bundle_id, base_file_hash, patch_file_hash,
33
- patch_storage_uri, byte_size
34
- ) VALUES (
35
- 'patch-1', '00000000-0000-0000-0000-000000000001',
36
- '00000000-0000-0000-0000-000000000001', 'base-hash', 'patch-hash',
37
- 'storage://patch', 3000000002
38
- )
39
- `).run();
40
-
41
- const tables = await env.DB.prepare(`
42
- SELECT name FROM sqlite_master
43
- WHERE type = 'table'
44
- ORDER BY name
45
- `).all<{ name: string }>();
46
-
47
- expect(tables.results.map(({ name }) => name)).toEqual(
48
- expect.arrayContaining([
49
- "bundle_events",
50
- "bundle_patches",
51
- "bundles",
52
- "channels",
53
- "api_keys",
54
- "private_hot_updater_settings",
55
- "release_catalogs",
56
- "releases",
57
- ]),
58
- );
59
-
60
- const version = await env.DB.prepare(
61
- "SELECT value FROM private_hot_updater_settings WHERE key = 'schema.core'",
62
- ).first<string>("value");
63
- expect(version).toBe("1.0.0");
64
-
65
- await env.DB.prepare(
66
- "INSERT INTO channels (id, name) VALUES ('channel-1', 'production')",
67
- ).run();
68
- await env.DB.prepare(`
69
- INSERT INTO releases (
70
- id, revision, scope_key, channel_id, platform, kind, bundle_id,
71
- strategy, target_app_version, fingerprint_hash, enabled,
72
- should_force_update, message, rollout_cohort_count, target_cohorts,
73
- operation, source_release_id, created_at_ms, updated_at_ms
74
- ) VALUES (
75
- '00000000-0000-0000-0000-000000000001', 1, 'scope', 'channel-1',
76
- 'ios', 'BUNDLE', '00000000-0000-0000-0000-000000000001',
77
- 'APP_VERSION', '1.0.0', NULL, 1, 0, NULL, 1000, '[]',
78
- 'DEPLOY', NULL, 0, 0
79
- )
80
- `).run();
81
-
82
- const release = await env.DB.prepare(
83
- "SELECT channel_id, bundle_id FROM releases",
84
- ).first();
85
- expect(release).toEqual({
86
- channel_id: "channel-1",
87
- bundle_id: "00000000-0000-0000-0000-000000000001",
88
- });
89
- const sizes = await env.DB.prepare(`
90
- SELECT bundle.archive_byte_size, patch.byte_size
91
- FROM bundles AS bundle
92
- JOIN bundle_patches AS patch ON patch.bundle_id = bundle.id
93
- `).first();
94
- expect(sizes).toEqual({
95
- archive_byte_size: 3_000_000_001,
96
- byte_size: 3_000_000_002,
97
- });
98
-
99
- for (const size of [-1, Number.MAX_SAFE_INTEGER + 1, null]) {
100
- await expect(
101
- env.DB.prepare("UPDATE bundles SET archive_byte_size = ?")
102
- .bind(size)
103
- .run(),
104
- ).rejects.toThrow(/constraint failed/);
105
- await expect(
106
- env.DB.prepare("UPDATE bundle_patches SET byte_size = ?")
107
- .bind(size)
108
- .run(),
109
- ).rejects.toThrow(/constraint failed/);
110
- }
111
-
112
- await expect(
113
- env.DB.prepare(`
114
- INSERT INTO bundles (id, platform, file_hash, storage_uri)
115
- VALUES ('missing-size', 'ios', 'hash', 'storage://bundle')
116
- `).run(),
117
- ).rejects.toThrow(/NOT NULL constraint failed/);
118
- await expect(
119
- env.DB.prepare(`
120
- INSERT INTO bundle_patches (
121
- id, bundle_id, base_bundle_id, base_file_hash, patch_file_hash,
122
- patch_storage_uri
123
- ) VALUES (
124
- 'missing-size', '00000000-0000-0000-0000-000000000001',
125
- '00000000-0000-0000-0000-000000000001', 'base-hash', 'patch-hash',
126
- 'storage://patch'
127
- )
128
- `).run(),
129
- ).rejects.toThrow(/NOT NULL constraint failed/);
130
- });
@@ -1,292 +0,0 @@
1
- import type {
2
- BundleRow,
3
- ChannelRow,
4
- ReleaseRow,
5
- } from "@hot-updater/plugin-core";
6
- import { setupDatabasePluginTestSuite } from "@hot-updater/test-utils";
7
- import { env } from "cloudflare:test";
8
- import {
9
- afterAll,
10
- beforeAll,
11
- beforeEach,
12
- describe,
13
- expect,
14
- inject,
15
- it,
16
- vi,
17
- } from "vitest";
18
-
19
- import { d1Database } from "../../src/d1Database";
20
- import { d1Database as d1RuntimeDatabase } from "../../src/worker";
21
-
22
- const state = vi.hoisted<{ db: D1Database | undefined }>(() => ({
23
- db: undefined,
24
- }));
25
-
26
- class D1TestStateError extends Error {
27
- readonly name = "D1TestStateError";
28
- }
29
-
30
- const getDb = (): D1Database => {
31
- if (state.db === undefined) {
32
- throw new D1TestStateError();
33
- }
34
- return state.db;
35
- };
36
-
37
- vi.mock("cloudflare", () => ({
38
- default: class MockCloudflare {
39
- readonly d1 = {
40
- database: {
41
- query: async (
42
- _databaseId: string,
43
- input:
44
- | { readonly sql: string; readonly params?: readonly string[] }
45
- | {
46
- readonly batch: readonly {
47
- readonly sql: string;
48
- readonly params?: readonly string[];
49
- }[];
50
- },
51
- ) => {
52
- const statements =
53
- "batch" in input
54
- ? input.batch.map(({ sql, params }) =>
55
- getDb()
56
- .prepare(sql)
57
- .bind(...(params ?? [])),
58
- )
59
- : (() => {
60
- const params = input.params ?? [];
61
- let paramOffset = 0;
62
- return input.sql
63
- .split(";")
64
- .map((sql) => sql.trim())
65
- .filter(Boolean)
66
- .map((sql) => {
67
- const paramCount = sql.match(/\?/g)?.length ?? 0;
68
- const statement = getDb()
69
- .prepare(sql)
70
- .bind(
71
- ...params.slice(
72
- paramOffset,
73
- paramOffset + paramCount,
74
- ),
75
- );
76
- paramOffset += paramCount;
77
- return statement;
78
- });
79
- })();
80
- const results = await getDb().batch(statements);
81
- return {
82
- async *iterPages() {
83
- yield { result: results };
84
- },
85
- };
86
- },
87
- },
88
- };
89
- },
90
- }));
91
-
92
- const reset = async (): Promise<void> => {
93
- await getDb()
94
- .prepare(
95
- "DELETE FROM bundle_events; DELETE FROM api_keys; DELETE FROM bundle_patches; DELETE FROM release_catalogs; DELETE FROM releases; DELETE FROM bundles; DELETE FROM channels;",
96
- )
97
- .run();
98
- };
99
-
100
- const createChannelRow = (name: string): ChannelRow => ({
101
- id: `channel-${name}`,
102
- name,
103
- });
104
-
105
- const createBundleRow = (): BundleRow => ({
106
- id: "00000000-0000-0000-0000-000000000902",
107
- platform: "ios",
108
- file_hash: "hash",
109
- git_commit_hash: null,
110
- storage_uri: "storage://bundle",
111
- archive_byte_size: 3_000_000_001,
112
- metadata: {},
113
- manifest_storage_uri: null,
114
- manifest_file_hash: null,
115
- asset_base_storage_uri: null,
116
- });
117
-
118
- const createReleaseRow = (
119
- channel: ChannelRow,
120
- bundle: BundleRow,
121
- ): ReleaseRow => ({
122
- id: "00000000-0000-7000-8000-000000000903",
123
- revision: 1,
124
- scope_key: `v1:test:${channel.name}:ios:app-version`,
125
- channel_id: channel.id,
126
- platform: bundle.platform,
127
- kind: "BUNDLE",
128
- bundle_id: bundle.id,
129
- strategy: "APP_VERSION",
130
- target_app_version: "1.0.0",
131
- fingerprint_hash: null,
132
- enabled: true,
133
- should_force_update: false,
134
- message: null,
135
- rollout_cohort_count: 1000,
136
- target_cohorts: [],
137
- operation: "DEPLOY",
138
- source_release_id: null,
139
- created_at_ms: 100,
140
- updated_at_ms: 100,
141
- });
142
-
143
- setupDatabasePluginTestSuite({
144
- name: "cloudflare d1 http fixed-model database plugin",
145
- migrate: async () => {
146
- state.db = env.DB;
147
- await getDb().prepare(inject("prepareSql")).run();
148
- },
149
- createPlugin: () =>
150
- d1Database({
151
- accountId: "account-id",
152
- cloudflareApiToken: "api-token",
153
- databaseId: "database-id",
154
- }),
155
- reset,
156
- dispose: () => undefined,
157
- });
158
-
159
- setupDatabasePluginTestSuite({
160
- name: "cloudflare worker d1 fixed-model database plugin",
161
- migrate: () => undefined,
162
- createPlugin: () => d1RuntimeDatabase(env.DB),
163
- reset,
164
- dispose: () => {
165
- state.db = undefined;
166
- },
167
- });
168
-
169
- describe.each([
170
- {
171
- name: "cloudflare d1 http",
172
- createPlugin: () =>
173
- d1Database({
174
- accountId: "account-id",
175
- cloudflareApiToken: "api-token",
176
- databaseId: "database-id",
177
- }),
178
- },
179
- {
180
- name: "cloudflare worker d1",
181
- createPlugin: () => d1RuntimeDatabase(env.DB),
182
- },
183
- ])("$name Channel deletion", ({ createPlugin }) => {
184
- beforeAll(() => {
185
- state.db = env.DB;
186
- });
187
-
188
- beforeEach(async () => {
189
- await reset();
190
- });
191
-
192
- afterAll(() => {
193
- state.db = undefined;
194
- });
195
-
196
- it("deletes only empty channels", async () => {
197
- const plugin = createPlugin();
198
- const channel = createChannelRow("empty");
199
- await plugin.models.channels.insert({
200
- row: channel,
201
- onConflict: "returnExisting",
202
- });
203
-
204
- await expect(
205
- plugin.models.channels.delete({ id: channel.id }),
206
- ).resolves.toEqual({ deleted: true });
207
- await expect(
208
- plugin.models.channels.delete({ id: channel.id }),
209
- ).resolves.toEqual({ deleted: false, reason: "not_found" });
210
- });
211
-
212
- it("rejects direct and committed deletion while a Release references the channel", async () => {
213
- const plugin = createPlugin();
214
- const channel = createChannelRow("active");
215
- const bundle = createBundleRow();
216
- const release = createReleaseRow(channel, bundle);
217
- await plugin.models.channels.insert({
218
- row: channel,
219
- onConflict: "returnExisting",
220
- });
221
- await plugin.commit({
222
- changes: [
223
- { model: "bundles", operation: "insert", row: bundle },
224
- { model: "releases", operation: "insert", row: release },
225
- ],
226
- });
227
-
228
- await expect(
229
- plugin.models.channels.delete({ id: channel.id }),
230
- ).resolves.toEqual({ deleted: false, reason: "not_empty" });
231
- await expect(
232
- plugin.commit({
233
- changes: [
234
- {
235
- model: "channels",
236
- operation: "delete",
237
- where: { id: channel.id },
238
- },
239
- ],
240
- }),
241
- ).resolves.toEqual({
242
- committed: false,
243
- conflict: { changeIndex: 0, reason: "referenced" },
244
- });
245
- await expect(plugin.models.bundles.findById(bundle.id)).resolves.toEqual(
246
- bundle,
247
- );
248
- });
249
-
250
- it("atomically deletes a Release before deleting its newly empty channel", async () => {
251
- const plugin = createPlugin();
252
- const channel = createChannelRow("retired");
253
- const bundle = createBundleRow();
254
- const release = createReleaseRow(channel, bundle);
255
- await plugin.models.channels.insert({
256
- row: channel,
257
- onConflict: "returnExisting",
258
- });
259
- await plugin.commit({
260
- changes: [
261
- { model: "bundles", operation: "insert", row: bundle },
262
- { model: "releases", operation: "insert", row: release },
263
- ],
264
- });
265
-
266
- await expect(
267
- plugin.commit({
268
- changes: [
269
- {
270
- model: "releases",
271
- operation: "delete",
272
- where: { id: release.id },
273
- },
274
- {
275
- model: "channels",
276
- operation: "delete",
277
- where: { id: channel.id },
278
- },
279
- ],
280
- }),
281
- ).resolves.toEqual({ committed: true });
282
- await expect(
283
- plugin.models.releases.findById(release.id),
284
- ).resolves.toBeNull();
285
- await expect(plugin.models.bundles.findById(bundle.id)).resolves.toEqual(
286
- bundle,
287
- );
288
- await expect(plugin.models.channels.list({})).resolves.toEqual({
289
- channels: [],
290
- });
291
- });
292
- });