@hot-updater/cloudflare 1.0.0-rc.5 → 1.0.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/index.cjs +90 -924
- package/dist/index.d.cts +16 -6
- package/dist/index.d.mts +16 -6
- package/dist/index.mjs +91 -926
- package/dist/init/index.cjs +11777 -0
- package/dist/init/index.d.cts +5 -1
- package/dist/init/index.d.mts +5 -1
- package/dist/init/index.mjs +11771 -1
- package/dist/worker/index.cjs +117 -916
- package/dist/worker/index.d.cts +45 -11
- package/dist/worker/index.d.mts +45 -11
- package/dist/worker/index.mjs +118 -917
- package/package.json +9 -14
- package/sql/bundles.sql +121 -212
- package/src/d1Database.ts +58 -43
- package/src/d1Executor.ts +84 -0
- package/src/d1Schema.ts +26 -0
- package/src/presignR2Url.ts +103 -0
- package/src/r2S3Storage.ts +20 -19
- package/src/r2Storage.ts +5 -3
- package/src/r2WorkerStorage.ts +55 -17
- package/src/worker/d1Database.ts +21 -39
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +9859 -5696
- package/worker/dist/index.js.map +4 -4
- package/worker/migrations/0001_hot-updater_1.0.0.sql +121 -222
- package/worker/src/index.ts +15 -9
- package/worker/wrangler.json +1 -0
- package/dist/iac/index.cjs +0 -12687
- package/dist/iac/index.d.cts +0 -5
- package/dist/iac/index.d.mts +0 -5
- package/dist/iac/index.mjs +0 -12679
- package/src/d1Database.spec.ts +0 -277
- package/src/d1Implementation.spec.ts +0 -342
- package/src/d1Implementation.ts +0 -781
- package/src/d1Query.spec.ts +0 -46
- package/src/d1Query.ts +0 -92
- package/src/d1Rows.spec.ts +0 -143
- package/src/d1Rows.ts +0 -357
- package/src/d1Sql.spec.ts +0 -30
- package/src/d1Sql.ts +0 -175
- package/src/r2Storage.spec.ts +0 -346
- package/src/r2WorkerStorage.spec.ts +0 -184
- package/src/utils/createWrangler.spec.ts +0 -75
- package/src/worker/d1Database.spec.ts +0 -69
- package/worker/src/databaseMigration.integration.spec.ts +0 -191
- package/worker/src/databasePlugins.integration.spec.ts +0 -326
- package/worker/src/index.integration.spec.ts +0 -190
- package/worker/src/insightsMovement.integration.spec.ts +0 -73
- package/worker/src/storagePlugin.integration.spec.ts +0 -59
- 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
|
|
4
|
+
"version": "1.0.0",
|
|
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,10 @@
|
|
|
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
|
-
"./worker/config": "./worker/wrangler.json",
|
|
26
|
-
"./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
24
|
"./package.json": "./package.json"
|
|
33
25
|
},
|
|
34
26
|
"license": "MIT",
|
|
@@ -52,18 +44,21 @@
|
|
|
52
44
|
"worker/migrations",
|
|
53
45
|
"worker/src",
|
|
54
46
|
"worker/wrangler.json",
|
|
47
|
+
"!**/*.spec.ts",
|
|
48
|
+
"!worker/src/testWorker.ts",
|
|
55
49
|
"package.json"
|
|
56
50
|
],
|
|
57
51
|
"dependencies": {
|
|
58
52
|
"@aws-sdk/client-s3": "3.1066.0",
|
|
59
53
|
"@aws-sdk/lib-storage": "3.1008.0",
|
|
54
|
+
"@aws-sdk/s3-request-presigner": "3.1066.0",
|
|
60
55
|
"cloudflare": "4.2.0",
|
|
61
56
|
"hono": "4.12.34",
|
|
62
57
|
"uuidv7": "^1.0.2",
|
|
63
|
-
"@hot-updater/cli-tools": "1.0.0
|
|
64
|
-
"@hot-updater/
|
|
65
|
-
"@hot-updater/
|
|
66
|
-
"@hot-updater/
|
|
58
|
+
"@hot-updater/cli-tools": "1.0.0",
|
|
59
|
+
"@hot-updater/plugin-core": "1.0.0",
|
|
60
|
+
"@hot-updater/protocol": "1.0.0",
|
|
61
|
+
"@hot-updater/server": "1.0.0"
|
|
67
62
|
},
|
|
68
63
|
"devDependencies": {
|
|
69
64
|
"@cloudflare/vitest-pool-workers": "0.13.0",
|
|
@@ -79,7 +74,7 @@
|
|
|
79
74
|
"vitest": "4.1.4",
|
|
80
75
|
"wrangler": "^4.69.0",
|
|
81
76
|
"xdg-app-paths": "^8.3.0",
|
|
82
|
-
"@hot-updater/test-utils": "1.0.0
|
|
77
|
+
"@hot-updater/test-utils": "1.0.0"
|
|
83
78
|
},
|
|
84
79
|
"scripts": {
|
|
85
80
|
"build": "tsdown && pnpm build:worker",
|
package/sql/bundles.sql
CHANGED
|
@@ -1,212 +1,121 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
CREATE
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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', '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')
|
|
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 bundle_installations (
|
|
153
|
-
install_id TEXT PRIMARY KEY NOT NULL,
|
|
154
|
-
id TEXT NOT NULL,
|
|
155
|
-
user_id TEXT,
|
|
156
|
-
username TEXT,
|
|
157
|
-
to_bundle_id TEXT NOT NULL,
|
|
158
|
-
type TEXT NOT NULL,
|
|
159
|
-
platform TEXT NOT NULL,
|
|
160
|
-
app_version TEXT NOT NULL,
|
|
161
|
-
channel TEXT NOT NULL,
|
|
162
|
-
cohort TEXT NOT NULL,
|
|
163
|
-
received_at_ms REAL NOT NULL,
|
|
164
|
-
CONSTRAINT bundle_installations_type_check CHECK (
|
|
165
|
-
type IN ('UPDATE_APPLIED', 'RECOVERED', 'UNCHANGED')
|
|
166
|
-
),
|
|
167
|
-
CONSTRAINT bundle_installations_platform_check CHECK (
|
|
168
|
-
platform IN ('ios', 'android')
|
|
169
|
-
),
|
|
170
|
-
CONSTRAINT bundle_installations_received_at_check CHECK (received_at_ms >= 0)
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
CREATE TABLE api_keys (
|
|
174
|
-
id TEXT PRIMARY KEY NOT NULL,
|
|
175
|
-
hash TEXT NOT NULL,
|
|
176
|
-
name TEXT NOT NULL,
|
|
177
|
-
prefix TEXT NOT NULL,
|
|
178
|
-
role TEXT NOT NULL,
|
|
179
|
-
created_at_ms REAL NOT NULL,
|
|
180
|
-
revoked_at_ms REAL,
|
|
181
|
-
CONSTRAINT api_keys_role_check CHECK (role = 'client'),
|
|
182
|
-
CONSTRAINT api_keys_created_at_check CHECK (created_at_ms >= 0),
|
|
183
|
-
CONSTRAINT api_keys_revoked_at_check CHECK (
|
|
184
|
-
revoked_at_ms IS NULL OR revoked_at_ms >= 0
|
|
185
|
-
)
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
CREATE TABLE private_hot_updater_settings (
|
|
189
|
-
key TEXT PRIMARY KEY NOT NULL,
|
|
190
|
-
value TEXT NOT NULL DEFAULT '1.0.0'
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
CREATE UNIQUE INDEX channels_name_key ON channels(name);
|
|
194
|
-
CREATE INDEX bundle_patches_bundle_id_idx ON bundle_patches(bundle_id);
|
|
195
|
-
CREATE INDEX bundle_patches_base_bundle_id_idx ON bundle_patches(base_bundle_id);
|
|
196
|
-
CREATE INDEX releases_scope_order_idx ON releases(scope_key, id);
|
|
197
|
-
CREATE INDEX releases_channel_platform_order_idx ON releases(channel_id, platform, id);
|
|
198
|
-
CREATE INDEX releases_bundle_id_idx ON releases(bundle_id);
|
|
199
|
-
CREATE INDEX releases_fingerprint_hash_idx ON releases(fingerprint_hash);
|
|
200
|
-
CREATE INDEX releases_enabled_idx ON releases(enabled);
|
|
201
|
-
CREATE INDEX release_catalogs_channel_idx ON release_catalogs(channel_id);
|
|
202
|
-
CREATE INDEX bundle_events_received_at_idx ON bundle_events(received_at_ms, id);
|
|
203
|
-
CREATE INDEX bundle_events_install_idx ON bundle_events(install_id, type, received_at_ms, id);
|
|
204
|
-
CREATE INDEX bundle_installations_user_id_idx ON bundle_installations(user_id, install_id);
|
|
205
|
-
CREATE INDEX bundle_installations_received_at_idx ON bundle_installations(received_at_ms);
|
|
206
|
-
CREATE UNIQUE INDEX api_keys_hash_key ON api_keys(hash);
|
|
207
|
-
CREATE INDEX api_keys_created_at_idx ON api_keys(created_at_ms, id);
|
|
208
|
-
|
|
209
|
-
CREATE INDEX bundle_events_from_bundle_idx ON bundle_events(type, platform, channel, from_bundle_id, received_at_ms, id);
|
|
210
|
-
CREATE INDEX bundle_events_to_bundle_idx ON bundle_events(type, platform, channel, to_bundle_id, received_at_ms, id);
|
|
211
|
-
CREATE INDEX bundle_installations_scope_idx ON bundle_installations(platform, channel, received_at_ms);
|
|
212
|
-
CREATE INDEX bundle_installations_bundle_idx ON bundle_installations(platform, channel, to_bundle_id, received_at_ms);
|
|
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_movementsByInstall" ON "bundle_events" ("movement_install_id", "received_at_ms", "id");
|
|
42
|
+
|
|
43
|
+
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"));
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS "bundle_events_byDay" ON "bundle_events" ("day", "received_at_ms", "id");
|
|
46
|
+
|
|
47
|
+
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"));
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS "bundle_event_heads_byUser" ON "bundle_event_heads" ("user_id", "install_id");
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS "bundle_event_heads__retention" ON "bundle_event_heads" ("received_at_ms", "install_id");
|
|
52
|
+
|
|
53
|
+
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, "failed_launches" 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"));
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS "insights_overview__retention" ON "insights_overview" ("bucket_start_ms", "identity", "_shard");
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS "insights_sketches" ("identity" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "activity_users" TEXT, "failed_users" TEXT, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS "insights_sketches__retention" ON "insights_sketches" ("bucket_start_ms", "identity", "_shard");
|
|
60
|
+
|
|
61
|
+
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, "failed_launches" 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"));
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS "insights_overview_daily__retention" ON "insights_overview_daily" ("bucket_start_ms", "identity", "_shard");
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS "insights_sketches_daily" ("identity" TEXT NOT NULL, "bucket_start_ms" INTEGER NOT NULL, "_shard" INTEGER NOT NULL, "activity_users" TEXT, "failed_users" TEXT, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS "insights_sketches_daily__retention" ON "insights_sketches_daily" ("bucket_start_ms", "identity", "_shard");
|
|
68
|
+
|
|
69
|
+
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, "failed_launches" INTEGER NOT NULL, "patch_downloads" INTEGER NOT NULL, "patch_fallbacks" INTEGER NOT NULL, "applies" INTEGER NOT NULL, "failed_updates" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
|
|
70
|
+
|
|
71
|
+
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"));
|
|
72
|
+
|
|
73
|
+
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"));
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS "insights_distribution_byScope" ON "insights_distribution" ("channel", "platform", "bucket_start_ms", "app_version", "release_id", "_shard");
|
|
76
|
+
|
|
77
|
+
CREATE INDEX IF NOT EXISTS "insights_distribution_byVersion" ON "insights_distribution" ("channel", "platform", "app_version", "bucket_start_ms", "release_id", "_shard");
|
|
78
|
+
|
|
79
|
+
CREATE INDEX IF NOT EXISTS "insights_distribution__retention" ON "insights_distribution" ("bucket_start_ms", "channel", "platform", "app_version", "release_id", "_shard");
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS "insights_builtin_distribution" ("channel" TEXT NOT NULL, "platform" TEXT NOT NULL, "app_version" TEXT NOT NULL, "release_id" TEXT NOT NULL, "builtin_bundle_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", "builtin_bundle_id", "bucket_start_ms", "_shard"));
|
|
82
|
+
|
|
83
|
+
CREATE INDEX IF NOT EXISTS "insights_builtin_distribution_byScope" ON "insights_builtin_distribution" ("channel", "platform", "bucket_start_ms", "app_version", "release_id", "builtin_bundle_id", "_shard");
|
|
84
|
+
|
|
85
|
+
CREATE INDEX IF NOT EXISTS "insights_builtin_distribution_byVersion" ON "insights_builtin_distribution" ("channel", "platform", "app_version", "bucket_start_ms", "release_id", "builtin_bundle_id", "_shard");
|
|
86
|
+
|
|
87
|
+
CREATE INDEX IF NOT EXISTS "insights_builtin_distribution__retention" ON "insights_builtin_distribution" ("bucket_start_ms", "channel", "platform", "app_version", "release_id", "builtin_bundle_id", "_shard");
|
|
88
|
+
|
|
89
|
+
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"));
|
|
90
|
+
|
|
91
|
+
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");
|
|
92
|
+
|
|
93
|
+
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"));
|
|
94
|
+
|
|
95
|
+
CREATE INDEX IF NOT EXISTS "insights_outcomes__retention" ON "insights_outcomes" ("bucket_start_ms", "platform", "channel", "type", "bundle_ref", "_shard");
|
|
96
|
+
|
|
97
|
+
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"));
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS "insights_failures__retention" ON "insights_failures" ("bucket_start_ms", "platform", "channel", "release_id", "stage", "reason", "detail", "_shard");
|
|
100
|
+
|
|
101
|
+
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"));
|
|
102
|
+
|
|
103
|
+
CREATE INDEX IF NOT EXISTS "api_keys_byCreated" ON "api_keys" ("created_at_ms", "id");
|
|
104
|
+
|
|
105
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "api_keys_hash" ON "api_keys" ("hash");
|
|
106
|
+
|
|
107
|
+
CREATE TABLE IF NOT EXISTS "remote_config_active" ("id" TEXT NOT NULL, "version" INTEGER NOT NULL, "template" TEXT NOT NULL, "updated_at_ms" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
|
|
108
|
+
|
|
109
|
+
CREATE TABLE IF NOT EXISTS "remote_config_versions" ("version" INTEGER NOT NULL, "template" TEXT NOT NULL, "description" TEXT, "update_type" TEXT NOT NULL, "rollback_source" INTEGER, "created_at_ms" INTEGER NOT NULL, "_v" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("version"));
|
|
110
|
+
|
|
111
|
+
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"));
|
|
112
|
+
|
|
113
|
+
INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.engine', '1', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
|
|
114
|
+
|
|
115
|
+
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";
|
|
116
|
+
|
|
117
|
+
INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.insights', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
|
|
118
|
+
|
|
119
|
+
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";
|
|
120
|
+
|
|
121
|
+
INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.remoteConfig', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
|
package/src/d1Database.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
12
|
import {
|
|
6
|
-
|
|
13
|
+
createD1Database,
|
|
7
14
|
D1ExecutionError,
|
|
8
|
-
type
|
|
9
|
-
|
|
15
|
+
type D1ResultLike,
|
|
16
|
+
toSqlResult,
|
|
17
|
+
} from "./d1Executor";
|
|
18
|
+
import { d1SchemaSql } from "./d1Schema";
|
|
19
|
+
|
|
20
|
+
export { D1ExecutionError } from "./d1Executor";
|
|
10
21
|
|
|
11
22
|
export interface D1DatabaseConfig {
|
|
12
23
|
readonly databaseId: string;
|
|
@@ -14,58 +25,62 @@ export interface D1DatabaseConfig {
|
|
|
14
25
|
readonly cloudflareApiToken: string;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
|
|
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 => {
|
|
18
42
|
const cloudflare = new Cloudflare({
|
|
19
43
|
apiToken: config.cloudflareApiToken,
|
|
20
44
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
statements: readonly D1Statement[],
|
|
24
|
-
): Promise<readonly (readonly unknown[])[]> => {
|
|
25
|
-
const first = statements[0];
|
|
45
|
+
const execute = async (statements: readonly SqlStatement[]) => {
|
|
46
|
+
const [first] = statements;
|
|
26
47
|
if (first === undefined) return [];
|
|
27
48
|
const body =
|
|
28
49
|
statements.length === 1
|
|
29
|
-
? {
|
|
30
|
-
|
|
31
|
-
sql: first.sql,
|
|
32
|
-
params: [...first.params],
|
|
33
|
-
}
|
|
34
|
-
: {
|
|
35
|
-
account_id: config.accountId,
|
|
36
|
-
batch: statements.map(({ sql, params }) => ({
|
|
37
|
-
sql,
|
|
38
|
-
params: [...params],
|
|
39
|
-
})),
|
|
40
|
-
};
|
|
50
|
+
? { account_id: config.accountId, ...encode(first) }
|
|
51
|
+
: { account_id: config.accountId, batch: statements.map(encode) };
|
|
41
52
|
// cloudflare@4 predates the D1 REST API's parameterized batch body type.
|
|
42
|
-
const page = await cloudflare.d1.database.query(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 = [];
|
|
46
58
|
for await (const resultPage of page.iterPages()) {
|
|
47
59
|
for (const result of resultPage.result) {
|
|
48
|
-
|
|
49
|
-
results.push(result.results ?? []);
|
|
60
|
+
results.push(toSqlResult(result as D1ResultLike));
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
if (results.length !== statements.length) throw new D1ExecutionError();
|
|
53
64
|
return results;
|
|
54
65
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
createD1Implementation({
|
|
59
|
-
async query(sql, params) {
|
|
60
|
-
return (await execute([{ sql, params }])).flat();
|
|
61
|
-
},
|
|
66
|
+
return {
|
|
67
|
+
...createD1Database({
|
|
68
|
+
query: async (statement) => (await execute([statement]))[0]!,
|
|
62
69
|
batch: execute,
|
|
63
70
|
}),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
};
|
|
71
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
|
+
};
|