@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/dist/index.mjs
CHANGED
|
@@ -1,958 +1,121 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
|
|
1
|
+
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter, createStorageAdapter, createStorageKeyBuilder, createStorageUri, createTableStatements, generateEngineSql, parseStorageUri } from "@hot-updater/plugin-core";
|
|
3
2
|
import Cloudflare from "cloudflare";
|
|
4
3
|
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
5
4
|
import { Upload } from "@aws-sdk/lib-storage";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
name = "InvalidD1RowError";
|
|
9
|
-
model;
|
|
10
|
-
constructor(model) {
|
|
11
|
-
super(`D1 returned an invalid ${model} row`);
|
|
12
|
-
this.model = model;
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
-
const stringValue = (row, field, model) => {
|
|
17
|
-
const value = row[field];
|
|
18
|
-
if (typeof value !== "string") throw new InvalidD1RowError(model);
|
|
19
|
-
return value;
|
|
20
|
-
};
|
|
21
|
-
const nullableString = (row, field, model) => {
|
|
22
|
-
const value = row[field];
|
|
23
|
-
if (value === null) return null;
|
|
24
|
-
if (typeof value !== "string") throw new InvalidD1RowError(model);
|
|
25
|
-
return value;
|
|
26
|
-
};
|
|
27
|
-
const numberValue = (row, field, model) => {
|
|
28
|
-
const value = row[field];
|
|
29
|
-
if (typeof value !== "number") throw new InvalidD1RowError(model);
|
|
30
|
-
return value;
|
|
31
|
-
};
|
|
32
|
-
const byteSizeValue = (row, field, model) => {
|
|
33
|
-
const value = row[field];
|
|
34
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new InvalidD1RowError(model);
|
|
35
|
-
return value;
|
|
36
|
-
};
|
|
37
|
-
const booleanValue = (row, field, model) => {
|
|
38
|
-
const value = row[field];
|
|
39
|
-
if (value === true || value === 1) return true;
|
|
40
|
-
if (value === false || value === 0) return false;
|
|
41
|
-
throw new InvalidD1RowError(model);
|
|
42
|
-
};
|
|
43
|
-
const jsonValue = (value, model) => {
|
|
44
|
-
if (typeof value !== "string") throw new InvalidD1RowError(model);
|
|
45
|
-
try {
|
|
46
|
-
return JSON.parse(value);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
if (error instanceof SyntaxError) throw new InvalidD1RowError(model);
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
const metadata = (row) => {
|
|
53
|
-
const value = jsonValue(row["metadata"], "bundles");
|
|
54
|
-
if (!isDatabaseMetadataObject(value)) throw new InvalidD1RowError("bundles");
|
|
55
|
-
return value;
|
|
56
|
-
};
|
|
57
|
-
const bundleRow = (row) => {
|
|
58
|
-
const platform = stringValue(row, "platform", "bundles");
|
|
59
|
-
if (platform !== "ios" && platform !== "android") throw new InvalidD1RowError("bundles");
|
|
60
|
-
return {
|
|
61
|
-
id: stringValue(row, "id", "bundles"),
|
|
62
|
-
platform,
|
|
63
|
-
file_hash: stringValue(row, "file_hash", "bundles"),
|
|
64
|
-
git_commit_hash: nullableString(row, "git_commit_hash", "bundles"),
|
|
65
|
-
storage_uri: stringValue(row, "storage_uri", "bundles"),
|
|
66
|
-
archive_byte_size: row["archive_byte_size"] === void 0 ? 0 : byteSizeValue(row, "archive_byte_size", "bundles"),
|
|
67
|
-
metadata: metadata(row),
|
|
68
|
-
manifest_storage_uri: nullableString(row, "manifest_storage_uri", "bundles"),
|
|
69
|
-
manifest_file_hash: nullableString(row, "manifest_file_hash", "bundles"),
|
|
70
|
-
asset_base_storage_uri: nullableString(row, "asset_base_storage_uri", "bundles")
|
|
71
|
-
};
|
|
72
|
-
};
|
|
73
|
-
const patchRow = (row) => ({
|
|
74
|
-
id: stringValue(row, "id", "bundle_patches"),
|
|
75
|
-
bundle_id: stringValue(row, "bundle_id", "bundle_patches"),
|
|
76
|
-
base_bundle_id: stringValue(row, "base_bundle_id", "bundle_patches"),
|
|
77
|
-
base_file_hash: stringValue(row, "base_file_hash", "bundle_patches"),
|
|
78
|
-
patch_file_hash: stringValue(row, "patch_file_hash", "bundle_patches"),
|
|
79
|
-
patch_storage_uri: stringValue(row, "patch_storage_uri", "bundle_patches"),
|
|
80
|
-
byte_size: byteSizeValue(row, "byte_size", "bundle_patches"),
|
|
81
|
-
order_index: numberValue(row, "order_index", "bundle_patches")
|
|
82
|
-
});
|
|
83
|
-
const channelRow = (row) => ({
|
|
84
|
-
id: stringValue(row, "id", "channels"),
|
|
85
|
-
name: stringValue(row, "name", "channels")
|
|
86
|
-
});
|
|
87
|
-
const eventRow = (row) => {
|
|
88
|
-
const type = stringValue(row, "type", "bundle_events");
|
|
89
|
-
const platform = stringValue(row, "platform", "bundle_events");
|
|
90
|
-
const fromBundleId = nullableString(row, "from_bundle_id", "bundle_events");
|
|
91
|
-
const updateStrategy = nullableString(row, "update_strategy", "bundle_events");
|
|
92
|
-
if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new InvalidD1RowError("bundle_events");
|
|
93
|
-
return {
|
|
94
|
-
id: stringValue(row, "id", "bundle_events"),
|
|
95
|
-
type,
|
|
96
|
-
install_id: stringValue(row, "install_id", "bundle_events"),
|
|
97
|
-
user_id: nullableString(row, "user_id", "bundle_events"),
|
|
98
|
-
username: nullableString(row, "username", "bundle_events"),
|
|
99
|
-
from_release_id: nullableString(row, "from_release_id", "bundle_events"),
|
|
100
|
-
from_bundle_id: fromBundleId,
|
|
101
|
-
to_release_id: nullableString(row, "to_release_id", "bundle_events"),
|
|
102
|
-
to_bundle_id: stringValue(row, "to_bundle_id", "bundle_events"),
|
|
103
|
-
platform,
|
|
104
|
-
app_version: stringValue(row, "app_version", "bundle_events"),
|
|
105
|
-
channel: stringValue(row, "channel", "bundle_events"),
|
|
106
|
-
cohort: stringValue(row, "cohort", "bundle_events"),
|
|
107
|
-
update_strategy: updateStrategy,
|
|
108
|
-
fingerprint_hash: nullableString(row, "fingerprint_hash", "bundle_events"),
|
|
109
|
-
sdk_version: nullableString(row, "sdk_version", "bundle_events"),
|
|
110
|
-
received_at_ms: numberValue(row, "received_at_ms", "bundle_events")
|
|
111
|
-
};
|
|
112
|
-
};
|
|
113
|
-
const installationRow = (row) => {
|
|
114
|
-
const type = stringValue(row, "type", "bundle_installations");
|
|
115
|
-
const platform = stringValue(row, "platform", "bundle_installations");
|
|
116
|
-
if (![
|
|
117
|
-
"UPDATE_APPLIED",
|
|
118
|
-
"RECOVERED",
|
|
119
|
-
"UNCHANGED"
|
|
120
|
-
].includes(type) || platform !== "ios" && platform !== "android") throw new InvalidD1RowError("bundle_installations");
|
|
121
|
-
return {
|
|
122
|
-
id: stringValue(row, "id", "bundle_installations"),
|
|
123
|
-
install_id: stringValue(row, "install_id", "bundle_installations"),
|
|
124
|
-
user_id: nullableString(row, "user_id", "bundle_installations"),
|
|
125
|
-
username: nullableString(row, "username", "bundle_installations"),
|
|
126
|
-
to_bundle_id: stringValue(row, "to_bundle_id", "bundle_installations"),
|
|
127
|
-
type,
|
|
128
|
-
platform,
|
|
129
|
-
app_version: stringValue(row, "app_version", "bundle_installations"),
|
|
130
|
-
channel: stringValue(row, "channel", "bundle_installations"),
|
|
131
|
-
cohort: stringValue(row, "cohort", "bundle_installations"),
|
|
132
|
-
received_at_ms: numberValue(row, "received_at_ms", "bundle_installations")
|
|
133
|
-
};
|
|
134
|
-
};
|
|
135
|
-
const apiKeyRow = (row) => {
|
|
136
|
-
const role = stringValue(row, "role", "api_keys");
|
|
137
|
-
if (role !== "client") throw new InvalidD1RowError("api_keys");
|
|
138
|
-
const revokedAt = row["revoked_at_ms"];
|
|
139
|
-
return {
|
|
140
|
-
id: stringValue(row, "id", "api_keys"),
|
|
141
|
-
hash: stringValue(row, "hash", "api_keys"),
|
|
142
|
-
name: stringValue(row, "name", "api_keys"),
|
|
143
|
-
prefix: stringValue(row, "prefix", "api_keys"),
|
|
144
|
-
role,
|
|
145
|
-
created_at_ms: numberValue(row, "created_at_ms", "api_keys"),
|
|
146
|
-
revoked_at_ms: revokedAt === null ? null : numberValue(row, "revoked_at_ms", "api_keys")
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
|
-
const releaseTargetCohorts = (row) => {
|
|
150
|
-
const value = jsonValue(row["target_cohorts"], "releases");
|
|
151
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new InvalidD1RowError("releases");
|
|
152
|
-
return value.filter((item) => typeof item === "string");
|
|
153
|
-
};
|
|
154
|
-
const releaseRow = (row) => {
|
|
155
|
-
const platform = stringValue(row, "platform", "releases");
|
|
156
|
-
const kind = stringValue(row, "kind", "releases");
|
|
157
|
-
const strategy = stringValue(row, "strategy", "releases");
|
|
158
|
-
const operation = stringValue(row, "operation", "releases");
|
|
159
|
-
if (platform !== "ios" && platform !== "android" || kind !== "BUNDLE" && kind !== "EMBEDDED" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT" || operation !== "DEPLOY" && operation !== "PROMOTE" && operation !== "ROLLBACK") throw new InvalidD1RowError("releases");
|
|
160
|
-
return {
|
|
161
|
-
id: stringValue(row, "id", "releases"),
|
|
162
|
-
revision: numberValue(row, "revision", "releases"),
|
|
163
|
-
scope_key: stringValue(row, "scope_key", "releases"),
|
|
164
|
-
channel_id: stringValue(row, "channel_id", "releases"),
|
|
165
|
-
platform,
|
|
166
|
-
kind,
|
|
167
|
-
bundle_id: nullableString(row, "bundle_id", "releases"),
|
|
168
|
-
strategy,
|
|
169
|
-
target_app_version: nullableString(row, "target_app_version", "releases"),
|
|
170
|
-
fingerprint_hash: nullableString(row, "fingerprint_hash", "releases"),
|
|
171
|
-
enabled: booleanValue(row, "enabled", "releases"),
|
|
172
|
-
should_force_update: booleanValue(row, "should_force_update", "releases"),
|
|
173
|
-
message: nullableString(row, "message", "releases"),
|
|
174
|
-
rollout_cohort_count: numberValue(row, "rollout_cohort_count", "releases"),
|
|
175
|
-
target_cohorts: releaseTargetCohorts(row),
|
|
176
|
-
operation,
|
|
177
|
-
source_release_id: nullableString(row, "source_release_id", "releases"),
|
|
178
|
-
created_at_ms: numberValue(row, "created_at_ms", "releases"),
|
|
179
|
-
updated_at_ms: numberValue(row, "updated_at_ms", "releases")
|
|
180
|
-
};
|
|
181
|
-
};
|
|
182
|
-
const releaseCatalogRow = (row) => {
|
|
183
|
-
const platform = stringValue(row, "platform", "release_catalogs");
|
|
184
|
-
const strategy = stringValue(row, "strategy", "release_catalogs");
|
|
185
|
-
if (platform !== "ios" && platform !== "android" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") throw new InvalidD1RowError("release_catalogs");
|
|
186
|
-
return {
|
|
187
|
-
scope_key: stringValue(row, "scope_key", "release_catalogs"),
|
|
188
|
-
catalog_id: stringValue(row, "catalog_id", "release_catalogs"),
|
|
189
|
-
strategy,
|
|
190
|
-
channel_id: stringValue(row, "channel_id", "release_catalogs"),
|
|
191
|
-
channel_key: stringValue(row, "channel_key", "release_catalogs"),
|
|
192
|
-
platform,
|
|
193
|
-
fingerprint_hash: nullableString(row, "fingerprint_hash", "release_catalogs"),
|
|
194
|
-
generation: numberValue(row, "generation", "release_catalogs"),
|
|
195
|
-
payload: stringValue(row, "payload", "release_catalogs"),
|
|
196
|
-
catalog_hash: stringValue(row, "catalog_hash", "release_catalogs"),
|
|
197
|
-
byte_size: numberValue(row, "byte_size", "release_catalogs"),
|
|
198
|
-
is_tombstone: booleanValue(row, "is_tombstone", "release_catalogs"),
|
|
199
|
-
updated_at_ms: numberValue(row, "updated_at_ms", "release_catalogs")
|
|
200
|
-
};
|
|
201
|
-
};
|
|
202
|
-
function parseD1Row(model, value) {
|
|
203
|
-
if (!isRecord(value)) throw new InvalidD1RowError(model);
|
|
204
|
-
switch (model) {
|
|
205
|
-
case "bundles": return bundleRow(value);
|
|
206
|
-
case "bundle_patches": return patchRow(value);
|
|
207
|
-
case "channels": return channelRow(value);
|
|
208
|
-
case "bundle_events": return eventRow(value);
|
|
209
|
-
case "bundle_installations": return installationRow(value);
|
|
210
|
-
case "api_keys": return apiKeyRow(value);
|
|
211
|
-
case "releases": return releaseRow(value);
|
|
212
|
-
case "release_catalogs": return releaseCatalogRow(value);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
//#endregion
|
|
216
|
-
//#region src/d1Sql.ts
|
|
217
|
-
var InvalidD1PredicateError = class extends Error {
|
|
218
|
-
name = "InvalidD1PredicateError";
|
|
219
|
-
};
|
|
220
|
-
const encodeD1Value = (value) => {
|
|
221
|
-
const encoded = JSON.stringify(value);
|
|
222
|
-
if (encoded === void 0) throw new TypeError("D1 values must be JSON-serializable.");
|
|
223
|
-
return encoded;
|
|
224
|
-
};
|
|
225
|
-
const bind = (value) => ({
|
|
226
|
-
sql: "json_extract(?, '$')",
|
|
227
|
-
params: [encodeD1Value(value)]
|
|
228
|
-
});
|
|
229
|
-
const escapeLikePattern = (value) => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
230
|
-
const stringPredicate = (condition, operator) => {
|
|
231
|
-
if (typeof condition.value !== "string") throw new InvalidD1PredicateError();
|
|
232
|
-
if (condition.mode !== "insensitive") {
|
|
233
|
-
const parameter = bind(condition.value);
|
|
234
|
-
const column = `${condition.field} COLLATE BINARY`;
|
|
235
|
-
const value = `${parameter.sql} COLLATE BINARY`;
|
|
236
|
-
if (operator === "contains" || operator === "starts_with") return {
|
|
237
|
-
sql: `instr(${column}, ${value}) ${operator === "contains" ? "> 0" : "= 1"}`,
|
|
238
|
-
params: parameter.params
|
|
239
|
-
};
|
|
240
|
-
return {
|
|
241
|
-
sql: `substr(${column}, length(${condition.field}) - length(${parameter.sql}) + 1) = ${value}`,
|
|
242
|
-
params: [...parameter.params, ...parameter.params]
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
const literal = escapeLikePattern(condition.value);
|
|
246
|
-
const pattern = operator === "contains" ? `%${literal}%` : operator === "starts_with" ? `${literal}%` : `%${literal}`;
|
|
247
|
-
const parameter = bind(pattern);
|
|
248
|
-
return {
|
|
249
|
-
sql: `lower(${condition.field}) LIKE lower(${parameter.sql}) ESCAPE '\\'`,
|
|
250
|
-
params: parameter.params
|
|
251
|
-
};
|
|
252
|
-
};
|
|
253
|
-
const predicate = (condition) => {
|
|
254
|
-
const operator = condition.operator ?? "eq";
|
|
255
|
-
switch (operator) {
|
|
256
|
-
case "eq":
|
|
257
|
-
case "ne": {
|
|
258
|
-
if (condition.value === null) return {
|
|
259
|
-
sql: `${condition.field} IS ${operator === "ne" ? "NOT " : ""}NULL`,
|
|
260
|
-
params: []
|
|
261
|
-
};
|
|
262
|
-
const parameter = bind(condition.value);
|
|
263
|
-
const insensitive = "mode" in condition && condition.mode === "insensitive";
|
|
264
|
-
const column = insensitive ? `lower(${condition.field})` : condition.field;
|
|
265
|
-
const value = insensitive ? `lower(${parameter.sql})` : parameter.sql;
|
|
266
|
-
return {
|
|
267
|
-
sql: `${column} ${operator === "eq" ? "=" : "<>"} ${value}`,
|
|
268
|
-
params: parameter.params
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
case "gt":
|
|
272
|
-
case "gte":
|
|
273
|
-
case "lt":
|
|
274
|
-
case "lte": {
|
|
275
|
-
const sqlOperator = {
|
|
276
|
-
gt: ">",
|
|
277
|
-
gte: ">=",
|
|
278
|
-
lt: "<",
|
|
279
|
-
lte: "<="
|
|
280
|
-
}[operator];
|
|
281
|
-
const parameter = bind(condition.value);
|
|
282
|
-
return {
|
|
283
|
-
sql: `${condition.field} ${sqlOperator} ${parameter.sql}`,
|
|
284
|
-
params: parameter.params
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
case "in":
|
|
288
|
-
case "not_in":
|
|
289
|
-
if (!Array.isArray(condition.value)) throw new InvalidD1PredicateError();
|
|
290
|
-
if (condition.value.length === 0) return {
|
|
291
|
-
sql: operator === "not_in" ? "1 = 1" : "1 = 0",
|
|
292
|
-
params: []
|
|
293
|
-
};
|
|
294
|
-
return {
|
|
295
|
-
sql: `${condition.field} ${operator === "not_in" ? "NOT " : ""}IN (SELECT value FROM json_each(?))`,
|
|
296
|
-
params: [encodeD1Value(condition.value)]
|
|
297
|
-
};
|
|
298
|
-
case "contains":
|
|
299
|
-
case "starts_with":
|
|
300
|
-
case "ends_with": return stringPredicate(condition, operator);
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
const buildD1Where = (where) => {
|
|
304
|
-
const [first, ...rest] = where ?? [];
|
|
305
|
-
if (first === void 0) return {
|
|
306
|
-
sql: "",
|
|
307
|
-
params: []
|
|
308
|
-
};
|
|
309
|
-
const initial = predicate(first);
|
|
310
|
-
let sql = initial.sql;
|
|
311
|
-
const params = [...initial.params];
|
|
312
|
-
for (const condition of rest) {
|
|
313
|
-
const next = predicate(condition);
|
|
314
|
-
sql = `(${sql} ${condition.connector === "OR" ? "OR" : "AND"} ${next.sql})`;
|
|
315
|
-
params.push(...next.params);
|
|
316
|
-
}
|
|
317
|
-
return {
|
|
318
|
-
sql: ` WHERE ${sql}`,
|
|
319
|
-
params
|
|
320
|
-
};
|
|
321
|
-
};
|
|
322
|
-
const buildD1Order = (orderBy) => orderBy === void 0 ? "" : ` ORDER BY ${orderBy.map((clause) => `${clause.field} ${clause.direction.toUpperCase()}${clause.nulls ? ` NULLS ${clause.nulls.toUpperCase()}` : ""}`).join(", ")}`;
|
|
323
|
-
const d1Placeholders = (count) => Array.from({ length: count }, () => "json_extract(?, '$')").join(", ");
|
|
324
|
-
const encodeD1Values = (values) => values.map(encodeD1Value);
|
|
325
|
-
//#endregion
|
|
326
|
-
//#region src/d1Query.ts
|
|
327
|
-
const d1TableNames = {
|
|
328
|
-
bundles: "bundles",
|
|
329
|
-
bundle_patches: "bundle_patches",
|
|
330
|
-
channels: "channels",
|
|
331
|
-
bundle_events: "bundle_events",
|
|
332
|
-
bundle_installations: "bundle_installations",
|
|
333
|
-
api_keys: "api_keys",
|
|
334
|
-
release_catalogs: "release_catalogs",
|
|
335
|
-
releases: "releases"
|
|
336
|
-
};
|
|
337
|
-
var D1QueryResultError = class extends Error {
|
|
338
|
-
result;
|
|
339
|
-
name = "D1QueryResultError";
|
|
340
|
-
operation = "count";
|
|
341
|
-
constructor(result) {
|
|
342
|
-
super("D1 count query returned a malformed first row");
|
|
343
|
-
this.result = result;
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
const countD1Rows = async (executor, input) => {
|
|
347
|
-
const where = buildD1Where(input.where);
|
|
348
|
-
const table = d1TableNames[input.model];
|
|
349
|
-
const source = input.distinct === void 0 ? `${table}${where.sql}` : `(SELECT DISTINCT ${input.distinct.join(", ")} FROM ${table}${where.sql}) AS distinct_rows`;
|
|
350
|
-
const row = (await executor.query(`SELECT COUNT(*) AS count FROM ${source}`, where.params))[0];
|
|
351
|
-
if (typeof row !== "object" || row === null || Array.isArray(row)) throw new D1QueryResultError(row);
|
|
352
|
-
const count = Reflect.get(row, "count");
|
|
353
|
-
if (typeof count !== "number" || !Number.isFinite(count)) throw new D1QueryResultError(row);
|
|
354
|
-
return count;
|
|
355
|
-
};
|
|
356
|
-
const findManyD1Rows = async (executor, input) => {
|
|
357
|
-
const where = buildD1Where(input.where);
|
|
358
|
-
const orderBy = input.orderBy;
|
|
359
|
-
const order = buildD1Order(orderBy);
|
|
360
|
-
const table = d1TableNames[input.model];
|
|
361
|
-
const source = input.distinctOn === void 0 ? `SELECT * FROM ${table}${where.sql}${order}` : `SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY ${input.distinctOn.fields.join(", ")}${order}) AS __hot_updater_rank FROM ${table}${where.sql}) AS distinct_rows WHERE __hot_updater_rank = 1${order}`;
|
|
362
|
-
const pageParams = encodeD1Values([input.limit, input.offset]);
|
|
363
|
-
const rows = await executor.query(`${source} LIMIT json_extract(?, '$') OFFSET json_extract(?, '$')`, [...where.params, ...pageParams]);
|
|
364
|
-
switch (input.model) {
|
|
365
|
-
case "bundles": return rows.map((row) => parseD1Row("bundles", row));
|
|
366
|
-
case "bundle_patches": return rows.map((row) => parseD1Row("bundle_patches", row));
|
|
367
|
-
case "channels": return rows.map((row) => parseD1Row("channels", row));
|
|
368
|
-
case "bundle_events": return rows.map((row) => parseD1Row("bundle_events", row));
|
|
369
|
-
case "bundle_installations": return rows.map((row) => parseD1Row("bundle_installations", row));
|
|
370
|
-
case "api_keys": return rows.map((row) => parseD1Row("api_keys", row));
|
|
371
|
-
case "releases": return rows.map((row) => parseD1Row("releases", row));
|
|
372
|
-
case "release_catalogs": return rows.map((row) => parseD1Row("release_catalogs", row));
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
//#endregion
|
|
376
|
-
//#region src/d1Implementation.ts
|
|
5
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
6
|
+
//#region src/d1Executor.ts
|
|
377
7
|
var D1ExecutionError = class extends Error {
|
|
378
8
|
name = "D1ExecutionError";
|
|
379
9
|
constructor() {
|
|
380
10
|
super("D1 did not successfully execute every requested statement");
|
|
381
11
|
}
|
|
382
12
|
};
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const clauses = [];
|
|
386
|
-
const params = [];
|
|
387
|
-
for (const expectation of expectations) {
|
|
388
|
-
const isRelease = expectation.model === "releases";
|
|
389
|
-
const table = isRelease ? "releases" : "release_catalogs";
|
|
390
|
-
const keyField = isRelease ? "id" : "scope_key";
|
|
391
|
-
const versionField = isRelease ? "revision" : "generation";
|
|
392
|
-
const key = isRelease ? expectation.id : expectation.scopeKey;
|
|
393
|
-
const version = isRelease ? expectation.revision : expectation.generation;
|
|
394
|
-
if (version === null) {
|
|
395
|
-
clauses.push(`NOT EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$'))`);
|
|
396
|
-
params.push(...encodeD1Values([key]));
|
|
397
|
-
} else {
|
|
398
|
-
clauses.push(`EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$') AND ${versionField} = json_extract(?, '$'))`);
|
|
399
|
-
params.push(...encodeD1Values([key, version]));
|
|
400
|
-
}
|
|
401
|
-
}
|
|
13
|
+
const toSqlResult = (result) => {
|
|
14
|
+
if (result.success === false) throw new D1ExecutionError();
|
|
402
15
|
return {
|
|
403
|
-
|
|
404
|
-
|
|
16
|
+
rows: result.results ?? [],
|
|
17
|
+
changes: result.meta?.changes ?? 0
|
|
405
18
|
};
|
|
406
19
|
};
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const value = Reflect.get(row, field);
|
|
410
|
-
return typeof value === "number" ? value : null;
|
|
411
|
-
};
|
|
412
|
-
const expectationConflict = async (executor, expectations) => {
|
|
413
|
-
for (const expectation of expectations) {
|
|
414
|
-
const isRelease = expectation.model === "releases";
|
|
415
|
-
const table = isRelease ? "releases" : "release_catalogs";
|
|
416
|
-
const keyField = isRelease ? "id" : "scope_key";
|
|
417
|
-
const versionField = isRelease ? "revision" : "generation";
|
|
418
|
-
const key = isRelease ? expectation.id : expectation.scopeKey;
|
|
419
|
-
const expectedVersion = isRelease ? expectation.revision : expectation.generation;
|
|
420
|
-
const rows = await executor.query(`SELECT ${versionField} FROM ${table} WHERE ${keyField} = json_extract(?, '$') LIMIT 1`, encodeD1Values([key]));
|
|
421
|
-
const actualVersion = readVersion(rows[0], versionField);
|
|
422
|
-
if (actualVersion !== expectedVersion) return {
|
|
423
|
-
committed: false,
|
|
424
|
-
conflict: {
|
|
425
|
-
actualVersion,
|
|
426
|
-
changeIndex: -1,
|
|
427
|
-
expectedVersion,
|
|
428
|
-
key,
|
|
429
|
-
model: expectation.model,
|
|
430
|
-
reason: "version_conflict"
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
return null;
|
|
435
|
-
};
|
|
436
|
-
const isExpectationConflictError = (error) => {
|
|
20
|
+
/** D1 reports SQLite errors in the message; the SQL core classifies codes. */
|
|
21
|
+
const withSqliteCode = (error) => {
|
|
437
22
|
const message = error instanceof Error ? error.message : String(error);
|
|
438
|
-
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
];
|
|
462
|
-
const channelValues = (row) => [row.id, row.name];
|
|
463
|
-
const insertQuery = (input, guard, conflictMode = "returnExisting") => {
|
|
464
|
-
let columns;
|
|
465
|
-
let values;
|
|
466
|
-
switch (input.model) {
|
|
467
|
-
case "bundles":
|
|
468
|
-
columns = [
|
|
469
|
-
"id",
|
|
470
|
-
"platform",
|
|
471
|
-
"file_hash",
|
|
472
|
-
"git_commit_hash",
|
|
473
|
-
"storage_uri",
|
|
474
|
-
"archive_byte_size",
|
|
475
|
-
"metadata",
|
|
476
|
-
"manifest_storage_uri",
|
|
477
|
-
"manifest_file_hash",
|
|
478
|
-
"asset_base_storage_uri"
|
|
479
|
-
];
|
|
480
|
-
values = bundleValues(input.data);
|
|
481
|
-
break;
|
|
482
|
-
case "bundle_patches":
|
|
483
|
-
columns = [
|
|
484
|
-
"id",
|
|
485
|
-
"bundle_id",
|
|
486
|
-
"base_bundle_id",
|
|
487
|
-
"base_file_hash",
|
|
488
|
-
"patch_file_hash",
|
|
489
|
-
"patch_storage_uri",
|
|
490
|
-
"byte_size",
|
|
491
|
-
"order_index"
|
|
492
|
-
];
|
|
493
|
-
values = patchValues(input.data);
|
|
494
|
-
break;
|
|
495
|
-
case "channels":
|
|
496
|
-
columns = ["id", "name"];
|
|
497
|
-
values = channelValues(input.data);
|
|
498
|
-
break;
|
|
499
|
-
case "bundle_events":
|
|
500
|
-
case "bundle_installations":
|
|
501
|
-
case "api_keys":
|
|
502
|
-
case "release_catalogs":
|
|
503
|
-
case "releases":
|
|
504
|
-
columns = Object.keys(input.data);
|
|
505
|
-
values = Object.values(input.data);
|
|
506
|
-
}
|
|
507
|
-
const conflict = input.onConflict !== "ignore" ? "" : input.model === "channels" ? conflictMode === "ignore" ? " ON CONFLICT(name) DO NOTHING" : " ON CONFLICT(name) DO UPDATE SET name = excluded.name" : input.model === "api_keys" ? conflictMode === "ignore" ? " ON CONFLICT(hash) DO NOTHING" : " ON CONFLICT(hash) DO UPDATE SET hash = excluded.hash" : " ON CONFLICT(install_id) DO UPDATE SET install_id = excluded.install_id";
|
|
508
|
-
return {
|
|
509
|
-
sql: `INSERT INTO ${d1TableNames[input.model]} (${columns.join(", ")}) ${guard === void 0 ? `VALUES (${d1Placeholders(values.length)})` : `SELECT ${d1Placeholders(values.length)} WHERE ${guard.sql}`}${conflict} RETURNING *`,
|
|
510
|
-
params: [...encodeD1Values(values), ...guard?.params ?? []]
|
|
511
|
-
};
|
|
512
|
-
};
|
|
513
|
-
const updateEntries = (update) => Object.entries(update);
|
|
514
|
-
const updateQuery = (input, guard) => {
|
|
515
|
-
const entries = updateEntries(input.update);
|
|
516
|
-
const where = buildD1Where(input.where);
|
|
517
|
-
if (entries.length === 0) return {
|
|
518
|
-
sql: `SELECT * FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""} LIMIT 1`,
|
|
519
|
-
params: [...where.params, ...guard?.params ?? []]
|
|
520
|
-
};
|
|
521
|
-
const assignments = entries.map(([field]) => `${field} = json_extract(?, '$')`).join(", ");
|
|
522
|
-
return {
|
|
523
|
-
sql: `UPDATE ${d1TableNames[input.model]} SET ${assignments}${where.sql}${guard ? ` AND ${guard.sql}` : ""} RETURNING *`,
|
|
524
|
-
params: [
|
|
525
|
-
...encodeD1Values(entries.map(([, value]) => value)),
|
|
526
|
-
...where.params,
|
|
527
|
-
...guard?.params ?? []
|
|
528
|
-
]
|
|
529
|
-
};
|
|
530
|
-
};
|
|
531
|
-
const deleteQuery = (input, guard) => {
|
|
532
|
-
const where = buildD1Where(input.where);
|
|
533
|
-
return {
|
|
534
|
-
sql: `DELETE FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""}`,
|
|
535
|
-
params: [...where.params, ...guard?.params ?? []]
|
|
536
|
-
};
|
|
537
|
-
};
|
|
538
|
-
const insertedKey = (change) => {
|
|
539
|
-
if (change.operation !== "insert") return void 0;
|
|
540
|
-
if (change.model === "bundles") return `bundles:${change.row.id}`;
|
|
541
|
-
if (change.model === "apiKeys") return `api_keys:${change.row.id}`;
|
|
542
|
-
if (change.model === "releases") return `releases:${change.row.id}`;
|
|
543
|
-
};
|
|
544
|
-
const requiredRow = (change, changeIndex) => {
|
|
545
|
-
if (change.operation !== "update") return void 0;
|
|
546
|
-
if (change.model === "bundles") return {
|
|
547
|
-
model: "bundles",
|
|
548
|
-
id: change.where.id,
|
|
549
|
-
changeIndex
|
|
550
|
-
};
|
|
551
|
-
if (change.model === "apiKeys") return {
|
|
552
|
-
model: "api_keys",
|
|
553
|
-
id: change.where.id,
|
|
554
|
-
changeIndex
|
|
555
|
-
};
|
|
556
|
-
if (change.model === "releases") return {
|
|
557
|
-
model: "releases",
|
|
558
|
-
id: change.where.id,
|
|
559
|
-
changeIndex
|
|
560
|
-
};
|
|
561
|
-
};
|
|
562
|
-
const channelDeletePrecondition = (changes, changeIndex, channelId) => {
|
|
563
|
-
const releaseEffects = /* @__PURE__ */ new Map();
|
|
564
|
-
for (const change of changes.slice(0, changeIndex)) {
|
|
565
|
-
if (change.model !== "releases") continue;
|
|
566
|
-
switch (change.operation) {
|
|
567
|
-
case "insert":
|
|
568
|
-
releaseEffects.set(change.row.id, change.row.channel_id);
|
|
569
|
-
break;
|
|
570
|
-
case "update": break;
|
|
571
|
-
case "delete": releaseEffects.set(change.where.id, null);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return {
|
|
575
|
-
id: channelId,
|
|
576
|
-
excludedReleaseIds: [...releaseEffects.keys()],
|
|
577
|
-
addedReferenceReleaseIds: [...releaseEffects].filter(([, finalChannelId]) => finalChannelId === channelId).map(([releaseId]) => releaseId)
|
|
578
|
-
};
|
|
579
|
-
};
|
|
580
|
-
const changeQuery = (change, guard) => {
|
|
581
|
-
switch (change.model) {
|
|
582
|
-
case "bundles": switch (change.operation) {
|
|
583
|
-
case "insert": return insertQuery({
|
|
584
|
-
model: "bundles",
|
|
585
|
-
data: change.row
|
|
586
|
-
}, guard);
|
|
587
|
-
case "update": return updateQuery({
|
|
588
|
-
model: "bundles",
|
|
589
|
-
where: [{
|
|
590
|
-
field: "id",
|
|
591
|
-
value: change.where.id
|
|
592
|
-
}],
|
|
593
|
-
update: change.update
|
|
594
|
-
}, guard);
|
|
595
|
-
case "delete": return deleteQuery({
|
|
596
|
-
model: "bundles",
|
|
597
|
-
where: [{
|
|
598
|
-
field: "id",
|
|
599
|
-
value: change.where.id
|
|
600
|
-
}]
|
|
601
|
-
}, guard);
|
|
602
|
-
}
|
|
603
|
-
case "bundlePatches": return change.operation === "insert" ? insertQuery({
|
|
604
|
-
model: "bundle_patches",
|
|
605
|
-
data: change.row
|
|
606
|
-
}, guard) : deleteQuery({
|
|
607
|
-
model: "bundle_patches",
|
|
608
|
-
where: [{
|
|
609
|
-
field: "bundle_id",
|
|
610
|
-
value: change.where.bundleId
|
|
611
|
-
}]
|
|
612
|
-
}, guard);
|
|
613
|
-
case "releases": switch (change.operation) {
|
|
614
|
-
case "insert": return insertQuery({
|
|
615
|
-
model: "releases",
|
|
616
|
-
data: change.row
|
|
617
|
-
}, guard);
|
|
618
|
-
case "update": return updateQuery({
|
|
619
|
-
model: "releases",
|
|
620
|
-
where: [{
|
|
621
|
-
field: "id",
|
|
622
|
-
value: change.where.id
|
|
623
|
-
}],
|
|
624
|
-
update: change.update
|
|
625
|
-
}, guard);
|
|
626
|
-
case "delete": return deleteQuery({
|
|
627
|
-
model: "releases",
|
|
628
|
-
where: [{
|
|
629
|
-
field: "id",
|
|
630
|
-
value: change.where.id
|
|
631
|
-
}]
|
|
632
|
-
}, guard);
|
|
633
|
-
}
|
|
634
|
-
case "releaseCatalogs": return {
|
|
635
|
-
sql: `INSERT INTO release_catalogs (${Object.keys(change.row).join(", ")}) SELECT ${d1Placeholders(Object.keys(change.row).length)} WHERE ${guard.sql} ON CONFLICT(scope_key) DO UPDATE SET ${Object.keys(change.row).filter((field) => field !== "scope_key").map((field) => `${field} = excluded.${field}`).join(", ")} RETURNING *`,
|
|
636
|
-
params: [...encodeD1Values(Object.values(change.row)), ...guard.params]
|
|
637
|
-
};
|
|
638
|
-
case "channels": return change.operation === "insert" ? insertQuery({
|
|
639
|
-
model: "channels",
|
|
640
|
-
data: change.row,
|
|
641
|
-
onConflict: change.onConflict
|
|
642
|
-
}, guard, "ignore") : {
|
|
643
|
-
sql: `DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) AND ${guard.sql}`,
|
|
644
|
-
params: [...encodeD1Values([change.where.id, change.where.id]), ...guard.params]
|
|
645
|
-
};
|
|
646
|
-
case "apiKeys": return change.operation === "insert" ? insertQuery({
|
|
647
|
-
model: "api_keys",
|
|
648
|
-
data: change.row,
|
|
649
|
-
onConflict: change.onConflict
|
|
650
|
-
}, guard, "ignore") : updateQuery({
|
|
651
|
-
model: "api_keys",
|
|
652
|
-
where: [{
|
|
653
|
-
field: "id",
|
|
654
|
-
value: change.where.id
|
|
655
|
-
}],
|
|
656
|
-
update: { revoked_at_ms: change.update.revokedAtMs }
|
|
657
|
-
}, guard);
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
const createCommitPlan = (input) => {
|
|
661
|
-
const checks = [];
|
|
662
|
-
const statements = [];
|
|
663
|
-
const expectations = input.expectations ?? [];
|
|
664
|
-
if (expectations.length > 0) {
|
|
665
|
-
const guard = expectationGuard(expectations);
|
|
666
|
-
statements.push({
|
|
667
|
-
sql: `SELECT CASE WHEN ${guard.sql} THEN 1 ELSE json_extract('${EXPECTATION_CONFLICT_MARKER}', '$') END AS expectation_guard`,
|
|
668
|
-
params: guard.params
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
const requiredRows = [];
|
|
672
|
-
const channelDeletes = [];
|
|
673
|
-
const inserted = /* @__PURE__ */ new Set();
|
|
674
|
-
for (const [changeIndex, change] of input.changes.entries()) {
|
|
675
|
-
const required = requiredRow(change, changeIndex);
|
|
676
|
-
if (required !== void 0) {
|
|
677
|
-
if (!inserted.has(`${required.model}:${required.id}`)) {
|
|
678
|
-
requiredRows.push(required);
|
|
679
|
-
checks.push({
|
|
680
|
-
changeIndex,
|
|
681
|
-
resultIndex: statements.length,
|
|
682
|
-
conflictWhen: "empty",
|
|
683
|
-
reason: "not_found"
|
|
684
|
-
});
|
|
685
|
-
statements.push({
|
|
686
|
-
sql: `SELECT id FROM ${d1TableNames[required.model]} WHERE id = json_extract(?, '$') LIMIT 1`,
|
|
687
|
-
params: encodeD1Values([required.id])
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
if (change.model === "channels" && change.operation === "delete") {
|
|
692
|
-
const precondition = channelDeletePrecondition(input.changes, changeIndex, change.where.id);
|
|
693
|
-
channelDeletes.push(precondition);
|
|
694
|
-
checks.push({
|
|
695
|
-
changeIndex,
|
|
696
|
-
resultIndex: statements.length,
|
|
697
|
-
conflictWhen: "nonempty",
|
|
698
|
-
reason: "referenced"
|
|
699
|
-
});
|
|
700
|
-
statements.push({
|
|
701
|
-
sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') AND id NOT IN (SELECT value FROM json_each(?)) UNION ALL SELECT value AS id FROM json_each(?) LIMIT 1",
|
|
702
|
-
params: encodeD1Values([
|
|
703
|
-
precondition.id,
|
|
704
|
-
precondition.excludedReleaseIds,
|
|
705
|
-
precondition.addedReferenceReleaseIds
|
|
706
|
-
])
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
const key = insertedKey(change);
|
|
710
|
-
if (key !== void 0) inserted.add(key);
|
|
711
|
-
}
|
|
712
|
-
const guard = {
|
|
713
|
-
sql: `NOT EXISTS (
|
|
714
|
-
SELECT 1 FROM json_each(?) AS required
|
|
715
|
-
WHERE NOT EXISTS (
|
|
716
|
-
SELECT 1 FROM bundles
|
|
717
|
-
WHERE json_extract(required.value, '$.model') = 'bundles'
|
|
718
|
-
AND bundles.id = json_extract(required.value, '$.id')
|
|
719
|
-
UNION ALL
|
|
720
|
-
SELECT 1 FROM api_keys
|
|
721
|
-
WHERE json_extract(required.value, '$.model') = 'api_keys'
|
|
722
|
-
AND api_keys.id = json_extract(required.value, '$.id')
|
|
723
|
-
UNION ALL
|
|
724
|
-
SELECT 1 FROM channels
|
|
725
|
-
WHERE json_extract(required.value, '$.model') = 'channels'
|
|
726
|
-
AND channels.id = json_extract(required.value, '$.id')
|
|
727
|
-
UNION ALL
|
|
728
|
-
SELECT 1 FROM releases
|
|
729
|
-
WHERE json_extract(required.value, '$.model') = 'releases'
|
|
730
|
-
AND releases.id = json_extract(required.value, '$.id')
|
|
731
|
-
)
|
|
732
|
-
) AND NOT EXISTS (
|
|
733
|
-
SELECT 1 FROM json_each(?) AS channel_delete
|
|
734
|
-
WHERE EXISTS (
|
|
735
|
-
SELECT 1 FROM releases
|
|
736
|
-
WHERE releases.channel_id = json_extract(channel_delete.value, '$.id')
|
|
737
|
-
AND releases.id NOT IN (
|
|
738
|
-
SELECT value FROM json_each(
|
|
739
|
-
json_extract(channel_delete.value, '$.excludedReleaseIds')
|
|
740
|
-
)
|
|
741
|
-
)
|
|
742
|
-
UNION ALL
|
|
743
|
-
SELECT value FROM json_each(
|
|
744
|
-
json_extract(channel_delete.value, '$.addedReferenceReleaseIds')
|
|
745
|
-
)
|
|
746
|
-
)
|
|
747
|
-
)`,
|
|
748
|
-
params: encodeD1Values([requiredRows.map(({ model, id }) => ({
|
|
749
|
-
model,
|
|
750
|
-
id
|
|
751
|
-
})), channelDeletes])
|
|
752
|
-
};
|
|
753
|
-
statements.push(...input.changes.map((change) => changeQuery(change, guard)));
|
|
754
|
-
return {
|
|
755
|
-
checks,
|
|
756
|
-
statements
|
|
757
|
-
};
|
|
758
|
-
};
|
|
759
|
-
const resultForPlan = (plan, results) => {
|
|
760
|
-
const missing = plan.checks.find(({ conflictWhen, resultIndex }) => conflictWhen === "empty" ? (results[resultIndex]?.length ?? 0) === 0 : (results[resultIndex]?.length ?? 0) > 0);
|
|
761
|
-
return missing === void 0 ? { committed: true } : {
|
|
762
|
-
committed: false,
|
|
763
|
-
conflict: {
|
|
764
|
-
changeIndex: missing.changeIndex,
|
|
765
|
-
reason: missing.reason
|
|
766
|
-
}
|
|
767
|
-
};
|
|
768
|
-
};
|
|
769
|
-
const insertChannel = async (executor, input) => {
|
|
770
|
-
const insert = insertQuery({
|
|
771
|
-
model: "channels",
|
|
772
|
-
data: input.row,
|
|
773
|
-
onConflict: "ignore"
|
|
774
|
-
}, void 0, "ignore");
|
|
775
|
-
const select = {
|
|
776
|
-
sql: "SELECT id, name FROM channels WHERE name = json_extract(?, '$') LIMIT 1",
|
|
777
|
-
params: encodeD1Values([input.row.name])
|
|
778
|
-
};
|
|
779
|
-
const [insertedRows = [], canonicalRows = []] = await executor.batch([insert, select]);
|
|
780
|
-
return {
|
|
781
|
-
row: parseD1Row("channels", canonicalRows[0]),
|
|
782
|
-
inserted: insertedRows.length > 0
|
|
783
|
-
};
|
|
784
|
-
};
|
|
785
|
-
const deleteChannel = async (executor, input) => {
|
|
786
|
-
const [channelRows = [], referenceRows = [], deletedRows = []] = await executor.batch([
|
|
787
|
-
{
|
|
788
|
-
sql: "SELECT id FROM channels WHERE id = json_extract(?, '$') LIMIT 1",
|
|
789
|
-
params: encodeD1Values([input.id])
|
|
790
|
-
},
|
|
791
|
-
{
|
|
792
|
-
sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') LIMIT 1",
|
|
793
|
-
params: encodeD1Values([input.id])
|
|
794
|
-
},
|
|
795
|
-
{
|
|
796
|
-
sql: "DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) RETURNING id",
|
|
797
|
-
params: encodeD1Values([input.id, input.id])
|
|
798
|
-
}
|
|
799
|
-
]);
|
|
800
|
-
if (channelRows.length === 0) return {
|
|
801
|
-
deleted: false,
|
|
802
|
-
reason: "not_found"
|
|
803
|
-
};
|
|
804
|
-
if (referenceRows.length > 0) return {
|
|
805
|
-
deleted: false,
|
|
806
|
-
reason: "not_empty"
|
|
807
|
-
};
|
|
808
|
-
return deletedRows.length > 0 ? { deleted: true } : {
|
|
809
|
-
deleted: false,
|
|
810
|
-
reason: "not_empty"
|
|
811
|
-
};
|
|
812
|
-
};
|
|
813
|
-
const createD1Implementation = (executor) => ({
|
|
814
|
-
async recordInsights({ event, installation }) {
|
|
815
|
-
const columns = Object.keys(installation);
|
|
816
|
-
const eventInsert = insertQuery({
|
|
817
|
-
model: "bundle_events",
|
|
818
|
-
data: event
|
|
819
|
-
});
|
|
820
|
-
await executor.batch([{
|
|
821
|
-
sql: `INSERT INTO bundle_installations (${columns.join(", ")})
|
|
822
|
-
SELECT ${d1Placeholders(columns.length)}
|
|
823
|
-
WHERE NOT EXISTS (SELECT 1 FROM bundle_events WHERE id = json_extract(?, '$'))
|
|
824
|
-
ON CONFLICT(install_id) DO UPDATE SET ${columns.filter((column) => column !== "install_id").map((column) => `${column} = excluded.${column}`).join(", ")}
|
|
825
|
-
WHERE (excluded.received_at_ms, excluded.id) >
|
|
826
|
-
(bundle_installations.received_at_ms, bundle_installations.id)`,
|
|
827
|
-
params: encodeD1Values([...Object.values(installation), event.id])
|
|
828
|
-
}, {
|
|
829
|
-
...eventInsert,
|
|
830
|
-
sql: eventInsert.sql.replace(" RETURNING *", " ON CONFLICT(id) DO NOTHING")
|
|
831
|
-
}]);
|
|
832
|
-
},
|
|
833
|
-
async create(input) {
|
|
834
|
-
const query = insertQuery(input);
|
|
835
|
-
const rows = await executor.query(query.sql, query.params);
|
|
836
|
-
switch (input.model) {
|
|
837
|
-
case "bundles": return parseD1Row("bundles", rows[0]);
|
|
838
|
-
case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
|
|
839
|
-
case "channels": return parseD1Row("channels", rows[0]);
|
|
840
|
-
case "bundle_events": return parseD1Row("bundle_events", rows[0]);
|
|
841
|
-
case "bundle_installations": return parseD1Row("bundle_installations", rows[0]);
|
|
842
|
-
case "api_keys": return parseD1Row("api_keys", rows[0]);
|
|
843
|
-
case "releases": return parseD1Row("releases", rows[0]);
|
|
844
|
-
case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
|
|
845
|
-
}
|
|
846
|
-
},
|
|
847
|
-
async update(input) {
|
|
848
|
-
const query = updateQuery(input);
|
|
849
|
-
const rows = await executor.query(query.sql, query.params);
|
|
850
|
-
if (rows[0] === void 0) return null;
|
|
851
|
-
switch (input.model) {
|
|
852
|
-
case "bundles": return parseD1Row("bundles", rows[0]);
|
|
853
|
-
case "api_keys": return parseD1Row("api_keys", rows[0]);
|
|
854
|
-
case "releases": return parseD1Row("releases", rows[0]);
|
|
855
|
-
case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
|
|
856
|
-
case "bundle_installations": return parseD1Row("bundle_installations", rows[0]);
|
|
857
|
-
}
|
|
858
|
-
},
|
|
859
|
-
async delete(input) {
|
|
860
|
-
const query = deleteQuery(input);
|
|
861
|
-
await executor.query(query.sql, query.params);
|
|
862
|
-
},
|
|
863
|
-
count: (input) => countD1Rows(executor, input),
|
|
864
|
-
async findOne(input) {
|
|
865
|
-
const where = buildD1Where(input.where);
|
|
866
|
-
const rows = await executor.query(`SELECT * FROM ${d1TableNames[input.model]}${where.sql} LIMIT 1`, where.params);
|
|
867
|
-
if (rows[0] === void 0) return null;
|
|
868
|
-
switch (input.model) {
|
|
869
|
-
case "bundles": return parseD1Row("bundles", rows[0]);
|
|
870
|
-
case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
|
|
871
|
-
case "channels": return parseD1Row("channels", rows[0]);
|
|
872
|
-
case "api_keys": return parseD1Row("api_keys", rows[0]);
|
|
873
|
-
case "releases": return parseD1Row("releases", rows[0]);
|
|
874
|
-
case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
|
|
875
|
-
case "bundle_installations": return parseD1Row("bundle_installations", rows[0]);
|
|
876
|
-
}
|
|
877
|
-
},
|
|
878
|
-
findMany: (input) => findManyD1Rows(executor, input),
|
|
879
|
-
insertChannel: (input) => insertChannel(executor, input),
|
|
880
|
-
deleteChannel: (input) => deleteChannel(executor, input),
|
|
881
|
-
async commit(input) {
|
|
882
|
-
if (input.changes.length === 0) return { committed: true };
|
|
883
|
-
const expectations = input.expectations ?? [];
|
|
884
|
-
const conflict = await expectationConflict(executor, expectations);
|
|
885
|
-
if (conflict !== null) return conflict;
|
|
886
|
-
const plan = createCommitPlan(input);
|
|
887
|
-
try {
|
|
888
|
-
return resultForPlan(plan, await executor.batch(plan.statements));
|
|
889
|
-
} catch (error) {
|
|
890
|
-
if (expectations.length === 0 || !isExpectationConflictError(error)) throw error;
|
|
891
|
-
return await expectationConflict(executor, expectations) ?? {
|
|
892
|
-
committed: false,
|
|
893
|
-
conflict: {
|
|
894
|
-
actualVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
|
|
895
|
-
changeIndex: -1,
|
|
896
|
-
expectedVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
|
|
897
|
-
key: expectations[0].model === "releases" ? expectations[0].id : expectations[0].scopeKey,
|
|
898
|
-
model: expectations[0].model,
|
|
899
|
-
reason: "version_conflict"
|
|
23
|
+
const code = /UNIQUE constraint failed|constraint failed: .*PRIMARY/iu.test(message) ? "SQLITE_CONSTRAINT_UNIQUE" : /SQLITE_BUSY|database is locked/iu.test(message) ? "SQLITE_BUSY" : void 0;
|
|
24
|
+
return code === void 0 ? error : Object.assign(new Error(message, { cause: error }), { code });
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Hot Updater's database on D1: the storage engine through the shared SQL
|
|
28
|
+
* core, fenced by the schema settings. D1 has no interactive transactions,
|
|
29
|
+
* so each write is one atomic batch that evaluates its guards first
|
|
30
|
+
* (`_hu_write`).
|
|
31
|
+
*/
|
|
32
|
+
const createD1Database = (runner) => {
|
|
33
|
+
return createEngineDatabase({
|
|
34
|
+
name: "d1Database",
|
|
35
|
+
adapter: createSqlAdapter({
|
|
36
|
+
executor: {
|
|
37
|
+
dialect: "sqlite",
|
|
38
|
+
execute: (statement) => runner.query(statement).catch((error) => {
|
|
39
|
+
throw withSqliteCode(error);
|
|
40
|
+
}),
|
|
41
|
+
batch: (statements) => runner.batch(statements).catch((error) => {
|
|
42
|
+
throw withSqliteCode(error);
|
|
43
|
+
}),
|
|
44
|
+
transaction: () => {
|
|
45
|
+
throw new Error("D1 has no interactive transactions; writes are batches.");
|
|
900
46
|
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
})
|
|
47
|
+
},
|
|
48
|
+
maxOps: 450,
|
|
49
|
+
maxParams: 100
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/d1Schema.ts
|
|
55
|
+
/**
|
|
56
|
+
* D1's schema: the guard table batch writes need, then the shared SQL
|
|
57
|
+
* schema of core and the target's plugins, settings last. Every statement can
|
|
58
|
+
* run again, so a migration for a server's plugin tables repeats core's.
|
|
59
|
+
*/
|
|
60
|
+
const d1SchemaStatements = ({ schema, settings } = coreTarget) => [...createTableStatements("sqlite", [WRITE_GUARD_TABLE]), ...generateEngineSql("sqlite", schema, settings)];
|
|
61
|
+
/** The schema as a D1 migration, which `wrangler d1 migrations apply` runs. */
|
|
62
|
+
const d1SchemaSql = (target) => `-- HotUpdater.schema\n\n${d1SchemaStatements(target).map((statement) => `${statement};`).join("\n\n")}\n`;
|
|
905
63
|
//#endregion
|
|
906
64
|
//#region src/d1Database.ts
|
|
65
|
+
/** The REST API binds text: each value goes as JSON and is read back with `json_extract`. */
|
|
66
|
+
const encode = ({ sql, params }) => ({
|
|
67
|
+
sql: sql.replaceAll("?", () => "json_extract(?, '$')"),
|
|
68
|
+
params: params.map((value) => JSON.stringify(value ?? null))
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Hot Updater's database on D1 through the Cloudflare REST API, for the CLI
|
|
72
|
+
* and console. `hot-updater db generate` writes a D1 migration of the built-in
|
|
73
|
+
* tables and the server's plugin tables to `migrations`, and
|
|
74
|
+
* `hot-updater db migrate` applies them through the API.
|
|
75
|
+
*/
|
|
907
76
|
const d1Database = (config) => {
|
|
908
77
|
const cloudflare = new Cloudflare({ apiToken: config.cloudflareApiToken });
|
|
909
78
|
const execute = async (statements) => {
|
|
910
|
-
const first = statements
|
|
79
|
+
const [first] = statements;
|
|
911
80
|
if (first === void 0) return [];
|
|
912
81
|
const body = statements.length === 1 ? {
|
|
913
82
|
account_id: config.accountId,
|
|
914
|
-
|
|
915
|
-
params: [...first.params]
|
|
83
|
+
...encode(first)
|
|
916
84
|
} : {
|
|
917
85
|
account_id: config.accountId,
|
|
918
|
-
batch: statements.map(
|
|
919
|
-
sql,
|
|
920
|
-
params: [...params]
|
|
921
|
-
}))
|
|
86
|
+
batch: statements.map(encode)
|
|
922
87
|
};
|
|
923
|
-
const page = await cloudflare.d1.database.query(config.databaseId,
|
|
88
|
+
const page = await cloudflare.d1.database.query(config.databaseId, body);
|
|
924
89
|
const results = [];
|
|
925
|
-
for await (const resultPage of page.iterPages()) for (const result of resultPage.result)
|
|
926
|
-
if (result.success === false) throw new D1ExecutionError();
|
|
927
|
-
results.push(result.results ?? []);
|
|
928
|
-
}
|
|
90
|
+
for await (const resultPage of page.iterPages()) for (const result of resultPage.result) results.push(toSqlResult(result));
|
|
929
91
|
if (results.length !== statements.length) throw new D1ExecutionError();
|
|
930
92
|
return results;
|
|
931
93
|
};
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
});
|
|
94
|
+
return {
|
|
95
|
+
...createD1Database({
|
|
96
|
+
query: async (statement) => (await execute([statement]))[0],
|
|
97
|
+
batch: execute
|
|
98
|
+
}),
|
|
99
|
+
generateSchema: ((version, _name, target = coreTarget) => {
|
|
100
|
+
if (version !== "latest" && version !== coreSettings["schema.core"]) throw new Error(`Invalid version ${version}`);
|
|
101
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
|
|
102
|
+
return {
|
|
103
|
+
code: d1SchemaSql(target),
|
|
104
|
+
path: `migrations/${timestamp}_hot-updater.sql`
|
|
105
|
+
};
|
|
106
|
+
})
|
|
107
|
+
};
|
|
947
108
|
};
|
|
948
109
|
//#endregion
|
|
949
110
|
//#region src/r2S3Storage.ts
|
|
111
|
+
/** Devices download right after the update check that returns the URL. */
|
|
112
|
+
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
950
113
|
const isObjectNotFoundError = (error) => {
|
|
951
114
|
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
952
115
|
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
953
116
|
};
|
|
954
117
|
const createR2S3Storage = (config) => {
|
|
955
|
-
const { accountId, basePath, bucketName,
|
|
118
|
+
const { accountId, basePath, bucketName, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
956
119
|
const client = new S3Client({
|
|
957
120
|
...s3Config,
|
|
958
121
|
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
@@ -960,13 +123,12 @@ const createR2S3Storage = (config) => {
|
|
|
960
123
|
region: region ?? "auto"
|
|
961
124
|
});
|
|
962
125
|
const getStorageKey = createStorageKeyBuilder(basePath);
|
|
963
|
-
const getDownloadUrl = downloadUrlSigningKey ? createStorageDownloadUrl(downloadUrlSigningKey) : void 0;
|
|
964
126
|
const parseAndValidate = (storageUri) => {
|
|
965
127
|
const parsed = parseStorageUri(storageUri, "r2");
|
|
966
128
|
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
967
129
|
return parsed;
|
|
968
130
|
};
|
|
969
|
-
return
|
|
131
|
+
return createStorageAdapter({
|
|
970
132
|
name: "r2Storage",
|
|
971
133
|
protocol: "r2",
|
|
972
134
|
async put({ key, body, contentLength, contentType }) {
|
|
@@ -1005,10 +167,13 @@ const createR2S3Storage = (config) => {
|
|
|
1005
167
|
throw error;
|
|
1006
168
|
}
|
|
1007
169
|
},
|
|
1008
|
-
|
|
1009
|
-
parseAndValidate(storageUri);
|
|
1010
|
-
return
|
|
1011
|
-
|
|
170
|
+
async getDownloadUrl({ storageUri }) {
|
|
171
|
+
const { key } = parseAndValidate(storageUri);
|
|
172
|
+
return { url: await getSignedUrl(client, new GetObjectCommand({
|
|
173
|
+
Bucket: bucketName,
|
|
174
|
+
Key: key
|
|
175
|
+
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
176
|
+
},
|
|
1012
177
|
async exists({ storageUri }) {
|
|
1013
178
|
const { key } = parseAndValidate(storageUri);
|
|
1014
179
|
try {
|
|
@@ -1035,8 +200,8 @@ const createR2S3Storage = (config) => {
|
|
|
1035
200
|
//#endregion
|
|
1036
201
|
//#region src/r2Storage.ts
|
|
1037
202
|
const r2Storage = (config) => {
|
|
1038
|
-
if (!config.credentials) throw new Error("r2Storage requires S3-compatible credentials
|
|
203
|
+
if (!config.credentials) throw new Error("r2Storage requires S3-compatible credentials: pass credentials with an R2 access key ID and secret access key.");
|
|
1039
204
|
return createR2S3Storage(config);
|
|
1040
205
|
};
|
|
1041
206
|
//#endregion
|
|
1042
|
-
export { d1Database, r2Storage };
|
|
207
|
+
export { D1ExecutionError, d1Database, r2Storage };
|