@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/cloudflare",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.2",
4
+ "version": "1.0.0-rc.21",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -17,18 +17,12 @@
17
17
  "require": "./dist/init/index.cjs"
18
18
  },
19
19
  "./worker": {
20
- "types": "./dist/worker/index.d.cts",
21
20
  "import": "./dist/worker/index.mjs",
22
21
  "require": "./dist/worker/index.cjs",
23
22
  "default": "./dist/worker/index.mjs"
24
23
  },
25
24
  "./worker/config": "./worker/wrangler.json",
26
25
  "./worker/wrangler.json": "./worker/wrangler.json",
27
- "./iac": {
28
- "types": "./dist/iac/index.d.cts",
29
- "import": "./dist/iac/index.mjs",
30
- "require": "./dist/iac/index.cjs"
31
- },
32
26
  "./package.json": "./package.json"
33
27
  },
34
28
  "license": "MIT",
@@ -52,6 +46,8 @@
52
46
  "worker/migrations",
53
47
  "worker/src",
54
48
  "worker/wrangler.json",
49
+ "!**/*.spec.ts",
50
+ "!worker/src/testWorker.ts",
55
51
  "package.json"
56
52
  ],
57
53
  "dependencies": {
@@ -60,10 +56,10 @@
60
56
  "cloudflare": "4.2.0",
61
57
  "hono": "4.12.34",
62
58
  "uuidv7": "^1.0.2",
63
- "@hot-updater/cli-tools": "1.0.0-rc.1",
64
- "@hot-updater/plugin-core": "1.0.0-rc.1",
65
- "@hot-updater/core": "1.0.0-rc.0",
66
- "@hot-updater/server": "1.0.0-rc.1"
59
+ "@hot-updater/protocol": "1.0.0-rc.21",
60
+ "@hot-updater/server": "1.0.0-rc.21",
61
+ "@hot-updater/cli-tools": "1.0.0-rc.21",
62
+ "@hot-updater/plugin-core": "1.0.0-rc.21"
67
63
  },
68
64
  "devDependencies": {
69
65
  "@cloudflare/vitest-pool-workers": "0.13.0",
@@ -75,11 +71,11 @@
75
71
  "toml": "^3.0.0",
76
72
  "@typescript/native": "npm:typescript@^7.0.2",
77
73
  "typescript": "npm:@typescript/typescript6@^6.0.2",
78
- "verkit": "0.3.2",
74
+ "verkit": "0.4.0",
79
75
  "vitest": "4.1.4",
80
76
  "wrangler": "^4.69.0",
81
77
  "xdg-app-paths": "^8.3.0",
82
- "@hot-updater/test-utils": "1.0.0-rc.1"
78
+ "@hot-updater/test-utils": "1.0.0-rc.21"
83
79
  },
84
80
  "scripts": {
85
81
  "build": "tsdown && pnpm build:worker",
package/sql/bundles.sql CHANGED
@@ -1,190 +1,109 @@
1
- CREATE TABLE channels (
2
- id TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
3
- name TEXT COLLATE BINARY NOT NULL,
4
- CONSTRAINT channels_id_length_check CHECK (length(id) BETWEEN 1 AND 255),
5
- CONSTRAINT channels_name_length_check CHECK (length(name) BETWEEN 1 AND 255)
6
- );
7
-
8
- CREATE TABLE bundles (
9
- id TEXT PRIMARY KEY NOT NULL,
10
- platform TEXT NOT NULL,
11
- file_hash TEXT NOT NULL,
12
- git_commit_hash TEXT,
13
- storage_uri TEXT NOT NULL,
14
- archive_byte_size REAL NOT NULL,
15
- metadata TEXT NOT NULL DEFAULT '{}',
16
- manifest_storage_uri TEXT,
17
- manifest_file_hash TEXT,
18
- asset_base_storage_uri TEXT,
19
- CONSTRAINT bundles_archive_byte_size_check CHECK (
20
- archive_byte_size >= 0 AND archive_byte_size <= 9007199254740991
21
- )
22
- );
23
-
24
- CREATE TABLE bundle_patches (
25
- id TEXT PRIMARY KEY NOT NULL,
26
- bundle_id TEXT NOT NULL,
27
- base_bundle_id TEXT NOT NULL,
28
- base_file_hash TEXT NOT NULL,
29
- patch_file_hash TEXT NOT NULL,
30
- patch_storage_uri TEXT NOT NULL,
31
- byte_size REAL NOT NULL,
32
- order_index INTEGER NOT NULL DEFAULT 0,
33
- CONSTRAINT bundle_patches_byte_size_check CHECK (
34
- byte_size >= 0 AND byte_size <= 9007199254740991
35
- ),
36
- CONSTRAINT bundle_patches_bundle_id_fk FOREIGN KEY (bundle_id)
37
- REFERENCES bundles(id) ON UPDATE RESTRICT ON DELETE CASCADE,
38
- CONSTRAINT bundle_patches_base_bundle_id_fk FOREIGN KEY (base_bundle_id)
39
- REFERENCES bundles(id) ON UPDATE RESTRICT ON DELETE CASCADE
40
- );
41
-
42
- CREATE TABLE releases (
43
- id TEXT PRIMARY KEY NOT NULL,
44
- revision INTEGER NOT NULL,
45
- scope_key TEXT COLLATE BINARY NOT NULL,
46
- channel_id TEXT COLLATE BINARY NOT NULL,
47
- platform TEXT NOT NULL,
48
- kind TEXT NOT NULL,
49
- bundle_id TEXT,
50
- strategy TEXT NOT NULL,
51
- target_app_version TEXT,
52
- fingerprint_hash TEXT,
53
- enabled INTEGER NOT NULL,
54
- should_force_update INTEGER NOT NULL,
55
- message TEXT,
56
- rollout_cohort_count INTEGER NOT NULL DEFAULT 1000,
57
- target_cohorts TEXT NOT NULL DEFAULT '[]',
58
- operation TEXT NOT NULL,
59
- source_release_id TEXT,
60
- created_at_ms REAL NOT NULL,
61
- updated_at_ms REAL NOT NULL,
62
- CONSTRAINT releases_revision_check CHECK (revision >= 1),
63
- CONSTRAINT releases_kind_bundle_check CHECK (
64
- (kind = 'BUNDLE' AND bundle_id IS NOT NULL)
65
- OR (kind = 'EMBEDDED' AND bundle_id IS NULL)
66
- ),
67
- CONSTRAINT releases_strategy_target_check CHECK (
68
- (strategy = 'APP_VERSION' AND target_app_version IS NOT NULL AND fingerprint_hash IS NULL)
69
- OR (strategy = 'FINGERPRINT' AND target_app_version IS NULL AND fingerprint_hash IS NOT NULL)
70
- ),
71
- CONSTRAINT releases_rollout_cohort_count_check CHECK (
72
- rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000
73
- ),
74
- CONSTRAINT releases_operation_check CHECK (
75
- operation IN ('DEPLOY', 'PROMOTE', 'ROLLBACK')
76
- ),
77
- CONSTRAINT releases_channel_id_fk FOREIGN KEY (channel_id)
78
- REFERENCES channels(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
79
- CONSTRAINT releases_bundle_id_fk FOREIGN KEY (bundle_id)
80
- REFERENCES bundles(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
81
- CONSTRAINT releases_source_release_id_fk FOREIGN KEY (source_release_id)
82
- REFERENCES releases(id) ON UPDATE RESTRICT ON DELETE SET NULL
83
- );
84
-
85
- CREATE TABLE release_catalogs (
86
- scope_key TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
87
- catalog_id TEXT NOT NULL,
88
- strategy TEXT NOT NULL,
89
- channel_id TEXT COLLATE BINARY NOT NULL,
90
- channel_key TEXT COLLATE BINARY NOT NULL,
91
- platform TEXT NOT NULL,
92
- fingerprint_hash TEXT,
93
- generation REAL NOT NULL,
94
- payload TEXT NOT NULL,
95
- catalog_hash TEXT NOT NULL,
96
- byte_size INTEGER NOT NULL,
97
- is_tombstone INTEGER NOT NULL,
98
- updated_at_ms REAL NOT NULL,
99
- CONSTRAINT release_catalogs_strategy_target_check CHECK (
100
- (strategy = 'APP_VERSION' AND fingerprint_hash IS NULL)
101
- OR (strategy = 'FINGERPRINT' AND fingerprint_hash IS NOT NULL)
102
- ),
103
- CONSTRAINT release_catalogs_generation_check CHECK (
104
- generation >= 1 AND generation <= 9007199254740991
105
- ),
106
- CONSTRAINT release_catalogs_byte_size_check CHECK (
107
- byte_size >= 0 AND byte_size <= 262144
108
- ),
109
- CONSTRAINT release_catalogs_channel_id_fk FOREIGN KEY (channel_id)
110
- REFERENCES channels(id) ON UPDATE RESTRICT ON DELETE RESTRICT
111
- );
112
-
113
- CREATE TABLE bundle_events (
114
- id TEXT PRIMARY KEY NOT NULL,
115
- type TEXT NOT NULL,
116
- install_id TEXT NOT NULL,
117
- user_id TEXT,
118
- username TEXT,
119
- from_release_id TEXT,
120
- from_bundle_id TEXT,
121
- to_release_id TEXT,
122
- to_bundle_id TEXT NOT NULL,
123
- platform TEXT NOT NULL,
124
- app_version TEXT NOT NULL,
125
- channel TEXT NOT NULL,
126
- cohort TEXT NOT NULL,
127
- update_strategy TEXT,
128
- fingerprint_hash TEXT,
129
- sdk_version TEXT,
130
- received_at_ms REAL NOT NULL,
131
- CONSTRAINT bundle_events_type_check CHECK (
132
- type IN ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED')
133
- ),
134
- CONSTRAINT bundle_events_platform_check CHECK (
135
- platform IN ('ios', 'android')
136
- ),
137
- CONSTRAINT bundle_events_shape_check CHECK (
138
- (
139
- type IN ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED')
140
- AND from_bundle_id IS NOT NULL
141
- AND update_strategy IS NOT NULL
142
- AND update_strategy IN ('fingerprint', 'appVersion')
143
- ) OR (
144
- type = 'UNCHANGED'
145
- AND from_bundle_id IS NULL
146
- AND update_strategy IS NULL
147
- )
148
- ),
149
- CONSTRAINT bundle_events_received_at_check CHECK (received_at_ms >= 0)
150
- );
151
-
152
- CREATE TABLE api_keys (
153
- id TEXT PRIMARY KEY NOT NULL,
154
- hash TEXT NOT NULL,
155
- name TEXT NOT NULL,
156
- prefix TEXT NOT NULL,
157
- role TEXT NOT NULL,
158
- created_at_ms REAL NOT NULL,
159
- revoked_at_ms REAL,
160
- CONSTRAINT api_keys_role_check CHECK (role = 'client'),
161
- CONSTRAINT api_keys_created_at_check CHECK (created_at_ms >= 0),
162
- CONSTRAINT api_keys_revoked_at_check CHECK (
163
- revoked_at_ms IS NULL OR revoked_at_ms >= 0
164
- )
165
- );
166
-
167
- CREATE TABLE private_hot_updater_settings (
168
- key TEXT PRIMARY KEY NOT NULL,
169
- value TEXT NOT NULL DEFAULT '1.0.0'
170
- );
171
-
172
- CREATE UNIQUE INDEX channels_name_key ON channels(name);
173
- CREATE INDEX bundle_patches_bundle_id_idx ON bundle_patches(bundle_id);
174
- CREATE INDEX bundle_patches_base_bundle_id_idx ON bundle_patches(base_bundle_id);
175
- CREATE INDEX releases_scope_order_idx ON releases(scope_key, id);
176
- CREATE INDEX releases_channel_platform_order_idx ON releases(channel_id, platform, id);
177
- CREATE INDEX releases_bundle_id_idx ON releases(bundle_id);
178
- CREATE INDEX releases_fingerprint_hash_idx ON releases(fingerprint_hash);
179
- CREATE INDEX releases_enabled_idx ON releases(enabled);
180
- CREATE INDEX release_catalogs_channel_idx ON release_catalogs(channel_id);
181
- CREATE INDEX bundle_events_received_at_idx ON bundle_events(received_at_ms, id);
182
- CREATE INDEX bundle_events_install_idx ON bundle_events(install_id, received_at_ms, id);
183
- CREATE INDEX bundle_events_user_id_idx ON bundle_events(user_id, received_at_ms, id);
184
- CREATE INDEX bundle_events_username_idx ON bundle_events(username, received_at_ms, id);
185
- CREATE INDEX bundle_events_to_bundle_idx ON bundle_events(type, to_bundle_id, received_at_ms, id);
186
- CREATE INDEX bundle_events_from_bundle_idx ON bundle_events(type, from_bundle_id, received_at_ms, id);
187
- CREATE INDEX bundle_events_to_release_idx ON bundle_events(type, to_release_id, received_at_ms, id);
188
- CREATE INDEX bundle_events_from_release_idx ON bundle_events(type, from_release_id, received_at_ms, id);
189
- CREATE UNIQUE INDEX api_keys_hash_key ON api_keys(hash);
190
- CREATE INDEX api_keys_created_at_idx ON api_keys(created_at_ms, id);
1
+ -- HotUpdater.schema
2
+
3
+ CREATE TABLE IF NOT EXISTS "_hu_write" ("id" TEXT NOT NULL, "failed_op" INTEGER, PRIMARY KEY ("id"));
4
+
5
+ CREATE TABLE IF NOT EXISTS "bundles" ("id" TEXT NOT NULL, "platform" TEXT NOT NULL, "git_commit_hash" TEXT, "metadata" TEXT NOT NULL, "manifest_storage_uri" TEXT NOT NULL, "manifest_file_hash" TEXT NOT NULL, "asset_base_storage_uri" TEXT NOT NULL, "_refs_bundle_patches_bundle_id" INTEGER NOT NULL DEFAULT 0, "_refs_bundle_patches_base_bundle_id" INTEGER NOT NULL DEFAULT 0, "_refs_releases_bundle_id" INTEGER NOT NULL DEFAULT 0, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
6
+
7
+ CREATE INDEX IF NOT EXISTS "bundles_byPlatform" ON "bundles" ("platform", "id");
8
+
9
+ CREATE TABLE IF NOT EXISTS "bundle_patches" ("id" TEXT NOT NULL, "bundle_id" TEXT NOT NULL, "base_bundle_id" TEXT NOT NULL, "base_file_hash" TEXT NOT NULL, "patch_file_hash" TEXT NOT NULL, "patch_storage_uri" TEXT NOT NULL, "byte_size" INTEGER NOT NULL, "order_index" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
10
+
11
+ CREATE UNIQUE INDEX IF NOT EXISTS "bundle_patches_pair" ON "bundle_patches" ("bundle_id", "base_bundle_id");
12
+
13
+ CREATE INDEX IF NOT EXISTS "bundle_patches_byBundle" ON "bundle_patches" ("bundle_id", "order_index", "id");
14
+
15
+ CREATE INDEX IF NOT EXISTS "bundle_patches_byBase" ON "bundle_patches" ("base_bundle_id", "bundle_id", "id");
16
+
17
+ CREATE TABLE IF NOT EXISTS "releases" ("id" TEXT NOT NULL, "revision" INTEGER NOT NULL, "scope_key" TEXT NOT NULL, "channel_id" TEXT NOT NULL, "platform" TEXT NOT NULL, "kind" TEXT NOT NULL, "bundle_id" TEXT, "strategy" TEXT NOT NULL, "target_app_version" TEXT, "fingerprint_hash" TEXT, "enabled" INTEGER NOT NULL, "should_force_update" INTEGER NOT NULL, "message" TEXT, "rollout_cohort_count" INTEGER NOT NULL, "target_cohorts" TEXT NOT NULL, "operation" TEXT NOT NULL, "source_release_id" TEXT, "created_at_ms" INTEGER NOT NULL, "updated_at_ms" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
18
+
19
+ CREATE INDEX IF NOT EXISTS "releases_byScope" ON "releases" ("scope_key", "id");
20
+
21
+ CREATE INDEX IF NOT EXISTS "releases_byScopeEnabled" ON "releases" ("scope_key", "enabled", "id");
22
+
23
+ CREATE INDEX IF NOT EXISTS "releases_byChannelPlatform" ON "releases" ("channel_id", "platform", "id");
24
+
25
+ CREATE INDEX IF NOT EXISTS "releases_byChannelPlatformEnabled" ON "releases" ("channel_id", "platform", "enabled", "id");
26
+
27
+ CREATE INDEX IF NOT EXISTS "releases_byBundle" ON "releases" ("bundle_id", "id");
28
+
29
+ CREATE TABLE IF NOT EXISTS "release_catalogs" ("scope_key" TEXT NOT NULL, "catalog_id" TEXT NOT NULL, "strategy" TEXT NOT NULL, "channel_id" TEXT NOT NULL, "channel_key" TEXT NOT NULL, "platform" TEXT NOT NULL, "fingerprint_hash" TEXT, "generation" INTEGER NOT NULL, "payload" TEXT NOT NULL, "catalog_hash" TEXT NOT NULL, "byte_size" INTEGER NOT NULL, "is_tombstone" INTEGER NOT NULL, "updated_at_ms" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("scope_key"));
30
+
31
+ CREATE TABLE IF NOT EXISTS "channels" ("id" TEXT NOT NULL, "name" TEXT NOT NULL, "_refs_releases_channel_id" INTEGER NOT NULL DEFAULT 0, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
32
+
33
+ CREATE INDEX IF NOT EXISTS "channels_all" ON "channels" ("name", "id");
34
+
35
+ CREATE UNIQUE INDEX IF NOT EXISTS "channels_name" ON "channels" ("name");
36
+
37
+ CREATE TABLE IF NOT EXISTS "bundle_totals" ("platform_key" TEXT NOT NULL, "_shard" INTEGER NOT NULL, "bundles" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("platform_key", "_shard"));
38
+
39
+ CREATE TABLE IF NOT EXISTS "bundle_events" ("id" TEXT NOT NULL, "type" TEXT NOT NULL, "install_id" TEXT NOT NULL, "user_id" TEXT, "from_release_id" TEXT, "from_bundle_id" TEXT, "to_release_id" TEXT, "to_bundle_id" TEXT NOT NULL, "platform" TEXT NOT NULL, "app_version" TEXT NOT NULL, "channel" TEXT NOT NULL, "metadata" TEXT NOT NULL, "received_at_ms" INTEGER NOT NULL, "day" INTEGER, "movement_install_id" TEXT, "bundle_ref" TEXT, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
40
+
41
+ CREATE INDEX IF NOT EXISTS "bundle_events_recent" ON "bundle_events" ("channel", "platform", "day", "received_at_ms", "id");
42
+
43
+ CREATE INDEX IF NOT EXISTS "bundle_events_movementsByInstall" ON "bundle_events" ("movement_install_id", "received_at_ms", "id");
44
+
45
+ CREATE TABLE IF NOT EXISTS "bundle_events__byBundle" ("platform" TEXT NOT NULL, "channel" TEXT NOT NULL, "type" TEXT NOT NULL, "bundle_ref" TEXT NOT NULL, "day" INTEGER NOT NULL, "received_at_ms" INTEGER NOT NULL, "id" TEXT NOT NULL, PRIMARY KEY ("platform", "channel", "type", "bundle_ref", "day", "received_at_ms", "id"));
46
+
47
+ CREATE INDEX IF NOT EXISTS "bundle_events_byDay" ON "bundle_events" ("day", "received_at_ms", "id");
48
+
49
+ CREATE TABLE IF NOT EXISTS "bundle_event_heads" ("install_id" TEXT NOT NULL, "id" TEXT NOT NULL, "type" TEXT NOT NULL, "user_id" TEXT, "from_release_id" TEXT, "from_bundle_id" TEXT, "to_release_id" TEXT, "to_bundle_id" TEXT NOT NULL, "platform" TEXT NOT NULL, "app_version" TEXT NOT NULL, "channel" TEXT NOT NULL, "metadata" TEXT NOT NULL, "received_at_ms" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("install_id"));
50
+
51
+ CREATE INDEX IF NOT EXISTS "bundle_event_heads_byUser" ON "bundle_event_heads" ("user_id", "install_id");
52
+
53
+ CREATE INDEX IF NOT EXISTS "bundle_event_heads__retention" ON "bundle_event_heads" ("received_at_ms", "install_id");
54
+
55
+ CREATE TABLE IF NOT EXISTS "insights_overview" ("identity" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "downloads" INTEGER NOT NULL, "launches" INTEGER NOT NULL, "failed_launches" INTEGER NOT NULL, "failed_updates" INTEGER NOT NULL, "patch_downloads" INTEGER NOT NULL, "patch_fallbacks" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
56
+
57
+ CREATE INDEX IF NOT EXISTS "insights_overview__retention" ON "insights_overview" ("bucket_start_ms", "identity", "_shard");
58
+
59
+ CREATE TABLE IF NOT EXISTS "insights_sketches" ("identity" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "launch_users" TEXT, "activity_users" TEXT, "failed_users" TEXT, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
60
+
61
+ CREATE INDEX IF NOT EXISTS "insights_sketches__retention" ON "insights_sketches" ("bucket_start_ms", "identity", "_shard");
62
+
63
+ CREATE TABLE IF NOT EXISTS "insights_overview_daily" ("identity" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "downloads" INTEGER NOT NULL, "launches" INTEGER NOT NULL, "failed_launches" INTEGER NOT NULL, "failed_updates" INTEGER NOT NULL, "patch_downloads" INTEGER NOT NULL, "patch_fallbacks" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
64
+
65
+ CREATE INDEX IF NOT EXISTS "insights_overview_daily__retention" ON "insights_overview_daily" ("bucket_start_ms", "identity", "_shard");
66
+
67
+ CREATE TABLE IF NOT EXISTS "insights_sketches_daily" ("identity" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "launch_users" TEXT, "activity_users" TEXT, "failed_users" TEXT, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
68
+
69
+ CREATE INDEX IF NOT EXISTS "insights_sketches_daily__retention" ON "insights_sketches_daily" ("bucket_start_ms", "identity", "_shard");
70
+
71
+ CREATE TABLE IF NOT EXISTS "insights_overview_lifetime" ("identity" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "downloads" INTEGER NOT NULL, "launches" INTEGER NOT NULL, "failed_launches" INTEGER NOT NULL, "failed_updates" INTEGER NOT NULL, "patch_downloads" INTEGER NOT NULL, "patch_fallbacks" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
72
+
73
+ CREATE TABLE IF NOT EXISTS "insights_sketches_lifetime" ("identity" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "failed_users" TEXT, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
74
+
75
+ CREATE TABLE IF NOT EXISTS "insights_distribution" ("channel" TEXT NOT NULL, "platform" TEXT NOT NULL, "app_version" TEXT NOT NULL, "release_id" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "latest_installations" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("channel", "platform", "app_version", "release_id", "bucket_start_ms", "_shard"));
76
+
77
+ CREATE INDEX IF NOT EXISTS "insights_distribution_byScope" ON "insights_distribution" ("channel", "platform", "bucket_start_ms", "app_version", "release_id", "_shard");
78
+
79
+ CREATE INDEX IF NOT EXISTS "insights_distribution_byVersion" ON "insights_distribution" ("channel", "platform", "app_version", "bucket_start_ms", "release_id", "_shard");
80
+
81
+ CREATE INDEX IF NOT EXISTS "insights_distribution__retention" ON "insights_distribution" ("bucket_start_ms", "channel", "platform", "app_version", "release_id", "_shard");
82
+
83
+ CREATE TABLE IF NOT EXISTS "insights_latest_by_bundle" ("platform" TEXT NOT NULL, "channel" TEXT NOT NULL, "bundle_field" TEXT NOT NULL, "bundle_id" TEXT NOT NULL, "type" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "installations" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("platform", "channel", "bundle_field", "bundle_id", "type", "bucket_start_ms", "_shard"));
84
+
85
+ CREATE INDEX IF NOT EXISTS "insights_latest_by_bundle__retention" ON "insights_latest_by_bundle" ("bucket_start_ms", "platform", "channel", "bundle_field", "bundle_id", "type", "_shard");
86
+
87
+ CREATE TABLE IF NOT EXISTS "insights_outcomes" ("platform" TEXT NOT NULL, "channel" TEXT NOT NULL, "type" TEXT NOT NULL, "bundle_ref" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "events" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("platform", "channel", "type", "bundle_ref", "bucket_start_ms", "_shard"));
88
+
89
+ CREATE INDEX IF NOT EXISTS "insights_outcomes__retention" ON "insights_outcomes" ("bucket_start_ms", "platform", "channel", "type", "bundle_ref", "_shard");
90
+
91
+ CREATE TABLE IF NOT EXISTS "insights_failures" ("platform" TEXT NOT NULL, "channel" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "release_id" TEXT NOT NULL, "stage" TEXT NOT NULL, "reason" TEXT NOT NULL, "detail" TEXT NOT NULL, "_shard" INTEGER NOT NULL, "events" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("platform", "channel", "bucket_start_ms", "release_id", "stage", "reason", "detail", "_shard"));
92
+
93
+ CREATE INDEX IF NOT EXISTS "insights_failures__retention" ON "insights_failures" ("bucket_start_ms", "platform", "channel", "release_id", "stage", "reason", "detail", "_shard");
94
+
95
+ CREATE TABLE IF NOT EXISTS "api_keys" ("id" TEXT NOT NULL, "hash" TEXT NOT NULL, "name" TEXT NOT NULL, "prefix" TEXT NOT NULL, "role" TEXT NOT NULL, "created_at_ms" INTEGER NOT NULL, "revoked_at_ms" INTEGER, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
96
+
97
+ CREATE INDEX IF NOT EXISTS "api_keys_byCreated" ON "api_keys" ("created_at_ms", "id");
98
+
99
+ CREATE UNIQUE INDEX IF NOT EXISTS "api_keys_hash" ON "api_keys" ("hash");
100
+
101
+ CREATE TABLE IF NOT EXISTS "private_hot_updater_settings" ("key" TEXT NOT NULL, "value" TEXT NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("key"));
102
+
103
+ INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.engine', '1', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
104
+
105
+ INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.core', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
106
+
107
+ INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.insights', '1.2.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
108
+
109
+ INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.apiKeys', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
package/src/d1Database.ts CHANGED
@@ -1,8 +1,23 @@
1
- import { createDatabasePlugin } from "@hot-updater/plugin-core";
2
- import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
1
+ import {
2
+ coreSettings,
3
+ coreTarget,
4
+ type SqlStatement,
5
+ } from "@hot-updater/plugin-core";
6
+ import type {
7
+ SchemaGenerator,
8
+ ToolingDatabase,
9
+ } from "@hot-updater/plugin-core";
3
10
  import Cloudflare from "cloudflare";
4
11
 
5
- import { createD1Implementation, type D1Statement } from "./d1Implementation";
12
+ import {
13
+ createD1Database,
14
+ D1ExecutionError,
15
+ type D1ResultLike,
16
+ toSqlResult,
17
+ } from "./d1Executor";
18
+ import { d1SchemaSql } from "./d1Schema";
19
+
20
+ export { D1ExecutionError } from "./d1Executor";
6
21
 
7
22
  export interface D1DatabaseConfig {
8
23
  readonly databaseId: string;
@@ -10,56 +25,62 @@ export interface D1DatabaseConfig {
10
25
  readonly cloudflareApiToken: string;
11
26
  }
12
27
 
13
- export const d1Database = (config: D1DatabaseConfig) => {
28
+ /** The REST API binds text: each value goes as JSON and is read back with `json_extract`. */
29
+ const encode = ({ sql, params }: SqlStatement) => ({
30
+ // A function, since `$'` in a replacement string means "the rest of the input".
31
+ sql: sql.replaceAll("?", () => "json_extract(?, '$')"),
32
+ params: params.map((value) => JSON.stringify(value ?? null)),
33
+ });
34
+
35
+ /**
36
+ * Hot Updater's database on D1 through the Cloudflare REST API, for the CLI
37
+ * and console. `hot-updater db generate` writes a D1 migration of the built-in
38
+ * tables and the server's plugin tables to `migrations`, and
39
+ * `hot-updater db migrate` applies them through the API.
40
+ */
41
+ export const d1Database = (config: D1DatabaseConfig): ToolingDatabase => {
14
42
  const cloudflare = new Cloudflare({
15
43
  apiToken: config.cloudflareApiToken,
16
44
  });
17
-
18
- const execute = async (
19
- statements: readonly D1Statement[],
20
- ): Promise<readonly (readonly unknown[])[]> => {
21
- const first = statements[0];
45
+ const execute = async (statements: readonly SqlStatement[]) => {
46
+ const [first] = statements;
22
47
  if (first === undefined) return [];
23
48
  const body =
24
49
  statements.length === 1
25
- ? {
26
- account_id: config.accountId,
27
- sql: first.sql,
28
- params: [...first.params],
29
- }
30
- : {
31
- account_id: config.accountId,
32
- batch: statements.map(({ sql, params }) => ({
33
- sql,
34
- params: [...params],
35
- })),
36
- };
50
+ ? { account_id: config.accountId, ...encode(first) }
51
+ : { account_id: config.accountId, batch: statements.map(encode) };
37
52
  // cloudflare@4 predates the D1 REST API's parameterized batch body type.
38
- const page = await cloudflare.d1.database.query(config.databaseId, {
39
- ...body,
40
- } as unknown as Parameters<typeof cloudflare.d1.database.query>[1]);
41
- const results: unknown[][] = [];
53
+ const page = await cloudflare.d1.database.query(
54
+ config.databaseId,
55
+ body as unknown as Parameters<typeof cloudflare.d1.database.query>[1],
56
+ );
57
+ const results = [];
42
58
  for await (const resultPage of page.iterPages()) {
43
59
  for (const result of resultPage.result) {
44
- results.push(result.results ?? []);
60
+ results.push(toSqlResult(result as D1ResultLike));
45
61
  }
46
62
  }
63
+ if (results.length !== statements.length) throw new D1ExecutionError();
47
64
  return results;
48
65
  };
49
-
50
- const adapter = createDatabasePluginAdapter(
51
- "d1Database",
52
- createD1Implementation({
53
- async query(sql, params) {
54
- return (await execute([{ sql, params }])).flat();
55
- },
66
+ return {
67
+ ...createD1Database({
68
+ query: async (statement) => (await execute([statement]))[0]!,
56
69
  batch: execute,
57
70
  }),
58
- );
59
- return createDatabasePlugin({
60
- name: "d1Database",
61
- models: adapter.models,
62
- commit: adapter.commit,
63
- ...(adapter.dispose ? { dispose: adapter.dispose } : {}),
64
- });
71
+ generateSchema: ((version, _name, target = coreTarget) => {
72
+ if (version !== "latest" && version !== coreSettings["schema.core"]) {
73
+ throw new Error(`Invalid version ${version}`);
74
+ }
75
+ // Wrangler applies migrations in the order of their leading numbers.
76
+ const timestamp = new Date()
77
+ .toISOString()
78
+ .replace(/\D/g, "")
79
+ .slice(0, 14);
80
+ return {
81
+ code: d1SchemaSql(target),
82
+ path: `migrations/${timestamp}_hot-updater.sql`,
83
+ };
84
+ }) satisfies SchemaGenerator,
85
+ };
65
86
  };
@@ -0,0 +1,84 @@
1
+ import {
2
+ createEngineDatabase,
3
+ createSqlAdapter,
4
+ type SqlExecutor,
5
+ type SqlResult,
6
+ type SqlStatement,
7
+ } from "@hot-updater/plugin-core";
8
+ import type { ToolingDatabase } from "@hot-updater/plugin-core";
9
+
10
+ /** A D1 result, from the binding or the REST API. */
11
+ export interface D1ResultLike {
12
+ readonly results?: readonly unknown[];
13
+ readonly success?: boolean;
14
+ readonly meta?: { readonly changes?: number };
15
+ }
16
+
17
+ export class D1ExecutionError extends Error {
18
+ readonly name = "D1ExecutionError";
19
+ constructor() {
20
+ super("D1 did not successfully execute every requested statement");
21
+ }
22
+ }
23
+
24
+ export const toSqlResult = (result: D1ResultLike): SqlResult => {
25
+ if (result.success === false) throw new D1ExecutionError();
26
+ return {
27
+ rows: (result.results ?? []) as Record<string, unknown>[],
28
+ changes: result.meta?.changes ?? 0,
29
+ };
30
+ };
31
+
32
+ /** D1 reports SQLite errors in the message; the SQL core classifies codes. */
33
+ const withSqliteCode = (error: unknown): unknown => {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ const code = /UNIQUE constraint failed|constraint failed: .*PRIMARY/iu.test(
36
+ message,
37
+ )
38
+ ? "SQLITE_CONSTRAINT_UNIQUE"
39
+ : /SQLITE_BUSY|database is locked/iu.test(message)
40
+ ? "SQLITE_BUSY"
41
+ : undefined;
42
+ return code === undefined
43
+ ? error
44
+ : Object.assign(new Error(message, { cause: error }), { code });
45
+ };
46
+
47
+ /**
48
+ * The most ops one write sends. Each statement of a batch is one query, a
49
+ * Worker invocation allows 1,000 queries on the paid plan, and an op takes at
50
+ * most two statements besides the batch's three.
51
+ */
52
+ export const D1_MAX_OPS = 450;
53
+
54
+ /**
55
+ * Hot Updater's database on D1: the storage engine through the shared SQL
56
+ * core, fenced by the schema settings. D1 has no interactive transactions,
57
+ * so each write is one atomic batch that evaluates its guards first
58
+ * (`_hu_write`).
59
+ */
60
+ export const createD1Database = (runner: {
61
+ query(statement: SqlStatement): Promise<SqlResult>;
62
+ batch(statements: readonly SqlStatement[]): Promise<readonly SqlResult[]>;
63
+ }): ToolingDatabase => {
64
+ const executor: SqlExecutor = {
65
+ dialect: "sqlite",
66
+ execute: (statement) =>
67
+ runner.query(statement).catch((error: unknown) => {
68
+ throw withSqliteCode(error);
69
+ }),
70
+ batch: (statements) =>
71
+ runner.batch(statements).catch((error: unknown) => {
72
+ throw withSqliteCode(error);
73
+ }),
74
+ transaction: () => {
75
+ throw new Error(
76
+ "D1 has no interactive transactions; writes are batches.",
77
+ );
78
+ },
79
+ };
80
+ return createEngineDatabase({
81
+ name: "d1Database",
82
+ adapter: createSqlAdapter({ executor, maxOps: D1_MAX_OPS, maxParams: 100 }),
83
+ });
84
+ };
@@ -0,0 +1,26 @@
1
+ import {
2
+ coreTarget,
3
+ createTableStatements,
4
+ WRITE_GUARD_TABLE,
5
+ generateEngineSql,
6
+ type ToolingTarget,
7
+ } from "@hot-updater/plugin-core";
8
+
9
+ /**
10
+ * D1's schema: the guard table batch writes need, then the shared SQL
11
+ * schema of core and the target's plugins, settings last. Every statement can
12
+ * run again, so a migration for a server's plugin tables repeats core's.
13
+ */
14
+ export const d1SchemaStatements = ({
15
+ schema,
16
+ settings,
17
+ }: ToolingTarget = coreTarget): string[] => [
18
+ ...createTableStatements("sqlite", [WRITE_GUARD_TABLE]),
19
+ ...generateEngineSql("sqlite", schema, settings),
20
+ ];
21
+
22
+ /** The schema as a D1 migration, which `wrangler d1 migrations apply` runs. */
23
+ export const d1SchemaSql = (target?: ToolingTarget): string =>
24
+ `-- HotUpdater.schema\n\n${d1SchemaStatements(target)
25
+ .map((statement) => `${statement};`)
26
+ .join("\n\n")}\n`;
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./d1Database";
2
2
  export * from "./r2Storage";
3
+ export { plugins } from "./plugins";
package/src/plugins.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { apiKeys } from "@hot-updater/server/plugins/api-keys";
2
+ import { insights } from "@hot-updater/server/plugins/insights";
3
+
4
+ /**
5
+ * The plugins the managed Cloudflare server runs: Insights, and API keys on every
6
+ * client route. Its Worker imports this list.
7
+ */
8
+ export const plugins = [insights(), apiKeys()] as const;