@hot-updater/firebase 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.
- package/dist/firebase/functions/index.cjs +9350 -5404
- package/dist/firebase/public/firestore.indexes.json +29 -58
- package/dist/firebase/public/functions/_package.json +2 -2
- package/dist/index.cjs +185 -898
- package/dist/index.d.cts +731 -5
- package/dist/index.d.mts +731 -5
- package/dist/index.mjs +186 -901
- package/dist/init/index.cjs +7799 -2
- package/dist/init/index.d.cts +42 -3
- package/dist/init/index.d.mts +42 -3
- package/dist/init/index.mjs +7792 -1
- package/package.json +9 -15
- package/dist/iac/index.cjs +0 -8609
- package/dist/iac/index.d.cts +0 -5
- package/dist/iac/index.d.mts +0 -5
- package/dist/iac/index.mjs +0 -8602
package/dist/index.cjs
CHANGED
|
@@ -1,918 +1,196 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
3
|
-
let _hot_updater_plugin_core_internal = require("@hot-updater/plugin-core/internal");
|
|
4
3
|
let firebase_admin_app = require("firebase-admin/app");
|
|
5
4
|
let firebase_admin_firestore = require("firebase-admin/firestore");
|
|
5
|
+
let node_crypto = require("node:crypto");
|
|
6
6
|
let firebase_admin_storage = require("firebase-admin/storage");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
source;
|
|
10
|
-
name = "FirebaseDatabaseDataError";
|
|
11
|
-
constructor(source) {
|
|
12
|
-
super(`Invalid Firebase database data at "${source}".`);
|
|
13
|
-
this.source = source;
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
const record = (value, source) => {
|
|
17
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new FirebaseDatabaseDataError(source);
|
|
18
|
-
return value;
|
|
19
|
-
};
|
|
20
|
-
const property = (value, key) => Reflect.get(value, key);
|
|
21
|
-
const string = (value, source) => {
|
|
22
|
-
if (typeof value !== "string") throw new FirebaseDatabaseDataError(source);
|
|
23
|
-
return value;
|
|
24
|
-
};
|
|
25
|
-
const nullableString = (value, source) => {
|
|
26
|
-
if (value === null || value === void 0) return null;
|
|
27
|
-
return string(value, source);
|
|
28
|
-
};
|
|
29
|
-
const boolean = (value, source) => {
|
|
30
|
-
if (typeof value !== "boolean") throw new FirebaseDatabaseDataError(source);
|
|
31
|
-
return value;
|
|
32
|
-
};
|
|
33
|
-
const number = (value, source) => {
|
|
34
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new FirebaseDatabaseDataError(source);
|
|
35
|
-
return value;
|
|
36
|
-
};
|
|
37
|
-
const byteSize = (value, source) => {
|
|
38
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new FirebaseDatabaseDataError(source);
|
|
39
|
-
return value;
|
|
40
|
-
};
|
|
41
|
-
const stringArray = (value, source) => {
|
|
42
|
-
if (value === null || value === void 0) return null;
|
|
43
|
-
if (!Array.isArray(value)) throw new FirebaseDatabaseDataError(source);
|
|
44
|
-
return value.map((item) => string(item, source));
|
|
45
|
-
};
|
|
46
|
-
const platform = (value, source) => {
|
|
47
|
-
if (value === "android" || value === "ios") return value;
|
|
48
|
-
throw new FirebaseDatabaseDataError(source);
|
|
49
|
-
};
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region src/firebaseDatabaseParser.ts
|
|
52
|
-
const metadata = (value, source) => {
|
|
53
|
-
if (!(0, _hot_updater_plugin_core.isDatabaseMetadataObject)(value)) throw new FirebaseDatabaseDataError(source);
|
|
54
|
-
return value;
|
|
55
|
-
};
|
|
56
|
-
const requiredNullableString = (input, key, source) => {
|
|
57
|
-
if (!Object.hasOwn(input, key)) throw new FirebaseDatabaseDataError(source);
|
|
58
|
-
return nullableString(property(input, key), source);
|
|
59
|
-
};
|
|
60
|
-
const parseFirebaseBundleRow = (value, source) => {
|
|
61
|
-
const input = record(value, source);
|
|
62
|
-
return {
|
|
63
|
-
id: string(property(input, "id"), source),
|
|
64
|
-
platform: platform(property(input, "platform"), source),
|
|
65
|
-
file_hash: string(property(input, "file_hash"), source),
|
|
66
|
-
git_commit_hash: nullableString(property(input, "git_commit_hash"), source),
|
|
67
|
-
storage_uri: string(property(input, "storage_uri"), source),
|
|
68
|
-
archive_byte_size: byteSize(property(input, "archive_byte_size"), source),
|
|
69
|
-
metadata: metadata(property(input, "metadata"), source),
|
|
70
|
-
manifest_storage_uri: nullableString(property(input, "manifest_storage_uri"), source),
|
|
71
|
-
manifest_file_hash: nullableString(property(input, "manifest_file_hash"), source),
|
|
72
|
-
asset_base_storage_uri: nullableString(property(input, "asset_base_storage_uri"), source)
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
const parseFirebaseChannelRow = (value, source) => {
|
|
76
|
-
const input = record(value, source);
|
|
77
|
-
return {
|
|
78
|
-
id: string(property(input, "id"), source),
|
|
79
|
-
name: string(property(input, "name"), source)
|
|
80
|
-
};
|
|
81
|
-
};
|
|
82
|
-
const parseFirebasePatchRow = (value, source) => {
|
|
83
|
-
const input = record(value, source);
|
|
84
|
-
return {
|
|
85
|
-
id: string(property(input, "id"), source),
|
|
86
|
-
bundle_id: string(property(input, "bundle_id"), source),
|
|
87
|
-
base_bundle_id: string(property(input, "base_bundle_id"), source),
|
|
88
|
-
base_file_hash: string(property(input, "base_file_hash"), source),
|
|
89
|
-
patch_file_hash: string(property(input, "patch_file_hash"), source),
|
|
90
|
-
patch_storage_uri: string(property(input, "patch_storage_uri"), source),
|
|
91
|
-
byte_size: byteSize(property(input, "byte_size"), source),
|
|
92
|
-
order_index: number(property(input, "order_index"), source)
|
|
93
|
-
};
|
|
94
|
-
};
|
|
95
|
-
const parseFirebaseBundleEventRow = (value, source) => {
|
|
96
|
-
const input = record(value, source);
|
|
97
|
-
const type = string(property(input, "type"), source);
|
|
98
|
-
const fromBundleId = requiredNullableString(input, "from_bundle_id", source);
|
|
99
|
-
const updateStrategy = requiredNullableString(input, "update_strategy", source);
|
|
100
|
-
if (!((type === "UPDATE_APPLIED" || type === "RECOVERED" || type === "RELEASE_ADOPTED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new FirebaseDatabaseDataError(source);
|
|
101
|
-
return {
|
|
102
|
-
id: string(property(input, "id"), source),
|
|
103
|
-
type,
|
|
104
|
-
install_id: string(property(input, "install_id"), source),
|
|
105
|
-
user_id: nullableString(property(input, "user_id"), source),
|
|
106
|
-
username: nullableString(property(input, "username"), source),
|
|
107
|
-
from_release_id: requiredNullableString(input, "from_release_id", source),
|
|
108
|
-
from_bundle_id: fromBundleId,
|
|
109
|
-
to_release_id: requiredNullableString(input, "to_release_id", source),
|
|
110
|
-
to_bundle_id: string(property(input, "to_bundle_id"), source),
|
|
111
|
-
platform: platform(property(input, "platform"), source),
|
|
112
|
-
app_version: string(property(input, "app_version"), source),
|
|
113
|
-
channel: string(property(input, "channel"), source),
|
|
114
|
-
cohort: string(property(input, "cohort"), source),
|
|
115
|
-
update_strategy: updateStrategy,
|
|
116
|
-
fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
|
|
117
|
-
sdk_version: nullableString(property(input, "sdk_version"), source),
|
|
118
|
-
received_at_ms: number(property(input, "received_at_ms"), source)
|
|
119
|
-
};
|
|
120
|
-
};
|
|
121
|
-
const parseFirebaseInsightsInstallationRow = (value, source) => {
|
|
122
|
-
const input = record(value, source);
|
|
123
|
-
const type = string(property(input, "type"), source);
|
|
124
|
-
if (type !== "UPDATE_APPLIED" && type !== "RECOVERED" && type !== "RELEASE_ADOPTED" && type !== "UNCHANGED") throw new FirebaseDatabaseDataError(source);
|
|
125
|
-
return {
|
|
126
|
-
id: string(property(input, "id"), source),
|
|
127
|
-
type,
|
|
128
|
-
install_id: string(property(input, "install_id"), source),
|
|
129
|
-
user_id: nullableString(property(input, "user_id"), source),
|
|
130
|
-
username: nullableString(property(input, "username"), source),
|
|
131
|
-
to_bundle_id: string(property(input, "to_bundle_id"), source),
|
|
132
|
-
platform: platform(property(input, "platform"), source),
|
|
133
|
-
app_version: string(property(input, "app_version"), source),
|
|
134
|
-
channel: string(property(input, "channel"), source),
|
|
135
|
-
cohort: string(property(input, "cohort"), source),
|
|
136
|
-
received_at_ms: number(property(input, "received_at_ms"), source)
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
|
-
const parseFirebaseApiKeyRow = (value, source) => {
|
|
140
|
-
const input = record(value, source);
|
|
141
|
-
const role = string(property(input, "role"), source);
|
|
142
|
-
if (role !== "client") throw new FirebaseDatabaseDataError(source);
|
|
143
|
-
const revokedAt = property(input, "revoked_at_ms");
|
|
144
|
-
return {
|
|
145
|
-
id: string(property(input, "id"), source),
|
|
146
|
-
hash: string(property(input, "hash"), source),
|
|
147
|
-
name: string(property(input, "name"), source),
|
|
148
|
-
prefix: string(property(input, "prefix"), source),
|
|
149
|
-
role,
|
|
150
|
-
created_at_ms: number(property(input, "created_at_ms"), source),
|
|
151
|
-
revoked_at_ms: revokedAt === null ? null : number(revokedAt, source)
|
|
152
|
-
};
|
|
153
|
-
};
|
|
154
|
-
const parseFirebaseReleaseRow = (value, source) => {
|
|
155
|
-
const input = record(value, source);
|
|
156
|
-
const kind = string(property(input, "kind"), source);
|
|
157
|
-
const strategy = string(property(input, "strategy"), source);
|
|
158
|
-
const operation = string(property(input, "operation"), source);
|
|
159
|
-
const targetCohorts = stringArray(property(input, "target_cohorts"), source);
|
|
160
|
-
if (kind !== "BUNDLE" && kind !== "EMBEDDED" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT" || operation !== "DEPLOY" && operation !== "PROMOTE" && operation !== "ROLLBACK" || targetCohorts === null) throw new FirebaseDatabaseDataError(source);
|
|
161
|
-
return {
|
|
162
|
-
id: string(property(input, "id"), source),
|
|
163
|
-
revision: number(property(input, "revision"), source),
|
|
164
|
-
scope_key: string(property(input, "scope_key"), source),
|
|
165
|
-
channel_id: string(property(input, "channel_id"), source),
|
|
166
|
-
platform: platform(property(input, "platform"), source),
|
|
167
|
-
kind,
|
|
168
|
-
bundle_id: nullableString(property(input, "bundle_id"), source),
|
|
169
|
-
strategy,
|
|
170
|
-
target_app_version: nullableString(property(input, "target_app_version"), source),
|
|
171
|
-
fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
|
|
172
|
-
enabled: boolean(property(input, "enabled"), source),
|
|
173
|
-
should_force_update: boolean(property(input, "should_force_update"), source),
|
|
174
|
-
message: nullableString(property(input, "message"), source),
|
|
175
|
-
rollout_cohort_count: number(property(input, "rollout_cohort_count"), source),
|
|
176
|
-
target_cohorts: targetCohorts,
|
|
177
|
-
operation,
|
|
178
|
-
source_release_id: nullableString(property(input, "source_release_id"), source),
|
|
179
|
-
created_at_ms: number(property(input, "created_at_ms"), source),
|
|
180
|
-
updated_at_ms: number(property(input, "updated_at_ms"), source)
|
|
181
|
-
};
|
|
182
|
-
};
|
|
183
|
-
const parseFirebaseReleaseCatalogRow = (value, source) => {
|
|
184
|
-
const input = record(value, source);
|
|
185
|
-
const strategy = string(property(input, "strategy"), source);
|
|
186
|
-
if (strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") throw new FirebaseDatabaseDataError(source);
|
|
187
|
-
return {
|
|
188
|
-
scope_key: string(property(input, "scope_key"), source),
|
|
189
|
-
catalog_id: string(property(input, "catalog_id"), source),
|
|
190
|
-
strategy,
|
|
191
|
-
channel_id: string(property(input, "channel_id"), source),
|
|
192
|
-
channel_key: string(property(input, "channel_key"), source),
|
|
193
|
-
platform: platform(property(input, "platform"), source),
|
|
194
|
-
fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
|
|
195
|
-
generation: number(property(input, "generation"), source),
|
|
196
|
-
payload: string(property(input, "payload"), source),
|
|
197
|
-
catalog_hash: string(property(input, "catalog_hash"), source),
|
|
198
|
-
byte_size: number(property(input, "byte_size"), source),
|
|
199
|
-
is_tombstone: boolean(property(input, "is_tombstone"), source),
|
|
200
|
-
updated_at_ms: number(property(input, "updated_at_ms"), source)
|
|
201
|
-
};
|
|
202
|
-
};
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region src/firebaseDatabaseQuery.ts
|
|
205
|
-
const compare = (left, right) => {
|
|
206
|
-
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
207
|
-
return String(left).localeCompare(String(right));
|
|
208
|
-
};
|
|
209
|
-
const normalizeStringComparison = (actual, expected, mode) => {
|
|
210
|
-
if (typeof actual !== "string") return null;
|
|
211
|
-
return mode === "insensitive" ? [actual.toLocaleLowerCase(), expected.toLocaleLowerCase()] : [actual, expected];
|
|
212
|
-
};
|
|
213
|
-
const matchesCondition = (row, condition) => {
|
|
214
|
-
const actual = Reflect.get(row, condition.field);
|
|
215
|
-
const expected = condition.value;
|
|
216
|
-
switch (condition.operator ?? "eq") {
|
|
217
|
-
case "eq": {
|
|
218
|
-
if (typeof expected !== "string") return actual === expected;
|
|
219
|
-
const mode = "mode" in condition ? condition.mode : void 0;
|
|
220
|
-
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
221
|
-
return comparison !== null && comparison[0] === comparison[1];
|
|
222
|
-
}
|
|
223
|
-
case "ne": {
|
|
224
|
-
if (actual === null || actual === void 0) return false;
|
|
225
|
-
if (typeof expected !== "string") return actual !== expected;
|
|
226
|
-
const mode = "mode" in condition ? condition.mode : void 0;
|
|
227
|
-
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
228
|
-
return comparison === null || comparison[0] !== comparison[1];
|
|
229
|
-
}
|
|
230
|
-
case "gt":
|
|
231
|
-
if (actual === null || actual === void 0) return false;
|
|
232
|
-
return compare(actual, expected) > 0;
|
|
233
|
-
case "gte":
|
|
234
|
-
if (actual === null || actual === void 0) return false;
|
|
235
|
-
return compare(actual, expected) >= 0;
|
|
236
|
-
case "lt":
|
|
237
|
-
if (actual === null || actual === void 0) return false;
|
|
238
|
-
return compare(actual, expected) < 0;
|
|
239
|
-
case "lte":
|
|
240
|
-
if (actual === null || actual === void 0) return false;
|
|
241
|
-
return compare(actual, expected) <= 0;
|
|
242
|
-
case "in":
|
|
243
|
-
if (!Array.isArray(expected)) return false;
|
|
244
|
-
return expected.some((candidate) => candidate === actual);
|
|
245
|
-
case "not_in": {
|
|
246
|
-
if (!Array.isArray(expected)) return false;
|
|
247
|
-
const values = expected;
|
|
248
|
-
return values.length === 0 || actual !== null && actual !== void 0 && values.every((candidate) => candidate !== actual);
|
|
249
|
-
}
|
|
250
|
-
case "contains": {
|
|
251
|
-
if (typeof expected !== "string") return false;
|
|
252
|
-
const mode = "mode" in condition ? condition.mode : void 0;
|
|
253
|
-
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
254
|
-
return comparison?.[0].includes(comparison[1]) ?? false;
|
|
255
|
-
}
|
|
256
|
-
case "starts_with": {
|
|
257
|
-
if (typeof expected !== "string") return false;
|
|
258
|
-
const mode = "mode" in condition ? condition.mode : void 0;
|
|
259
|
-
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
260
|
-
return comparison?.[0].startsWith(comparison[1]) ?? false;
|
|
261
|
-
}
|
|
262
|
-
case "ends_with": {
|
|
263
|
-
if (typeof expected !== "string") return false;
|
|
264
|
-
const mode = "mode" in condition ? condition.mode : void 0;
|
|
265
|
-
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
266
|
-
return comparison?.[0].endsWith(comparison[1]) ?? false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
const matchesFirebaseDatabaseWhere = (row, where) => {
|
|
271
|
-
const first = where?.[0];
|
|
272
|
-
if (!first) return true;
|
|
273
|
-
let result = matchesCondition(row, first);
|
|
274
|
-
for (const condition of where.slice(1)) {
|
|
275
|
-
const current = matchesCondition(row, condition);
|
|
276
|
-
result = condition.connector === "OR" ? result || current : result && current;
|
|
277
|
-
}
|
|
278
|
-
return result;
|
|
279
|
-
};
|
|
280
|
-
const queryFirebaseDatabaseRows = (rows, input) => {
|
|
281
|
-
const filtered = rows.filter((row) => matchesFirebaseDatabaseWhere(row, input.where));
|
|
282
|
-
const orderBy = input.orderBy;
|
|
283
|
-
if (orderBy !== void 0) filtered.sort((left, right) => {
|
|
284
|
-
for (const clause of orderBy) {
|
|
285
|
-
const leftValue = Reflect.get(left, clause.field);
|
|
286
|
-
const rightValue = Reflect.get(right, clause.field);
|
|
287
|
-
if (leftValue === rightValue) continue;
|
|
288
|
-
if (clause.nulls !== void 0 && (leftValue === null || rightValue === null)) {
|
|
289
|
-
const nullComparison = leftValue === null ? -1 : 1;
|
|
290
|
-
return clause.nulls === "first" ? nullComparison : -nullComparison;
|
|
291
|
-
}
|
|
292
|
-
const comparison = compare(leftValue, rightValue);
|
|
293
|
-
if (comparison !== 0) return clause.direction === "asc" ? comparison : -comparison;
|
|
294
|
-
}
|
|
295
|
-
return 0;
|
|
296
|
-
});
|
|
297
|
-
const distinctOn = input.distinctOn;
|
|
298
|
-
if (distinctOn === void 0) return filtered.slice(input.offset, input.offset + input.limit);
|
|
299
|
-
const seen = /* @__PURE__ */ new Set();
|
|
300
|
-
return filtered.filter((row) => {
|
|
301
|
-
const key = JSON.stringify(distinctOn.fields.map((field) => Reflect.get(row, field)));
|
|
302
|
-
if (seen.has(key)) return false;
|
|
303
|
-
seen.add(key);
|
|
304
|
-
return true;
|
|
305
|
-
}).slice(input.offset, input.offset + input.limit);
|
|
306
|
-
};
|
|
307
|
-
//#endregion
|
|
308
|
-
//#region src/firebaseDatabaseState.ts
|
|
309
|
-
var FirebaseDatabaseConstraintError = class extends Error {
|
|
310
|
-
constraint;
|
|
311
|
-
name = "FirebaseDatabaseConstraintError";
|
|
312
|
-
constructor(constraint) {
|
|
313
|
-
super(`Firebase database constraint failed: ${constraint}`);
|
|
314
|
-
this.constraint = constraint;
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
const cloneFirebaseDatabaseSnapshot = (snapshot) => ({
|
|
318
|
-
bundles: new Map(snapshot.bundles),
|
|
319
|
-
bundlePatches: new Map(snapshot.bundlePatches),
|
|
320
|
-
bundleEvents: new Map(snapshot.bundleEvents),
|
|
321
|
-
bundleInstallations: new Map(snapshot.bundleInstallations),
|
|
322
|
-
channels: new Map(snapshot.channels),
|
|
323
|
-
apiKeys: new Map(snapshot.apiKeys),
|
|
324
|
-
releaseCatalogs: new Map(snapshot.releaseCatalogs),
|
|
325
|
-
releases: new Map(snapshot.releases)
|
|
326
|
-
});
|
|
327
|
-
const requireUnique = (rows, id, model) => {
|
|
328
|
-
if (rows.has(id)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
|
|
329
|
-
};
|
|
330
|
-
const distinctCount = (rows, fields) => {
|
|
331
|
-
if (fields === void 0) return rows.length;
|
|
332
|
-
return new Set(rows.map((row) => JSON.stringify(fields.map((field) => Reflect.get(row, field))))).size;
|
|
333
|
-
};
|
|
334
|
-
const createFirebaseDatabaseState = (snapshot) => ({
|
|
335
|
-
async create(input) {
|
|
336
|
-
switch (input.model) {
|
|
337
|
-
case "bundles":
|
|
338
|
-
requireUnique(snapshot.bundles, input.data.id, input.model);
|
|
339
|
-
snapshot.bundles.set(input.data.id, input.data);
|
|
340
|
-
return input.data;
|
|
341
|
-
case "bundle_patches":
|
|
342
|
-
requireUnique(snapshot.bundlePatches, input.data.id, input.model);
|
|
343
|
-
if (!snapshot.bundles.has(input.data.bundle_id)) throw new FirebaseDatabaseConstraintError("bundle_patches.bundle_id.foreign-key");
|
|
344
|
-
if (!snapshot.bundles.has(input.data.base_bundle_id)) throw new FirebaseDatabaseConstraintError("bundle_patches.base_bundle_id.foreign-key");
|
|
345
|
-
snapshot.bundlePatches.set(input.data.id, input.data);
|
|
346
|
-
return input.data;
|
|
347
|
-
case "bundle_events":
|
|
348
|
-
requireUnique(snapshot.bundleEvents, input.data.id, input.model);
|
|
349
|
-
snapshot.bundleEvents.set(input.data.id, input.data);
|
|
350
|
-
return input.data;
|
|
351
|
-
case "bundle_installations": {
|
|
352
|
-
const current = snapshot.bundleInstallations.get(input.data.install_id);
|
|
353
|
-
if (current && input.onConflict === "ignore") return current;
|
|
354
|
-
if (current) throw new FirebaseDatabaseConstraintError("bundle_installations.install_id.unique");
|
|
355
|
-
snapshot.bundleInstallations.set(input.data.install_id, input.data);
|
|
356
|
-
return input.data;
|
|
357
|
-
}
|
|
358
|
-
case "releases":
|
|
359
|
-
requireUnique(snapshot.releases, input.data.id, input.model);
|
|
360
|
-
if (!snapshot.channels.has(input.data.channel_id)) throw new FirebaseDatabaseConstraintError("releases.channel_id.foreign-key");
|
|
361
|
-
if (input.data.bundle_id !== null && !snapshot.bundles.has(input.data.bundle_id)) throw new FirebaseDatabaseConstraintError("releases.bundle_id.foreign-key");
|
|
362
|
-
snapshot.releases.set(input.data.id, input.data);
|
|
363
|
-
return input.data;
|
|
364
|
-
case "release_catalogs":
|
|
365
|
-
if (snapshot.releaseCatalogs.has(input.data.scope_key)) throw new FirebaseDatabaseConstraintError("release_catalogs.scope_key.unique");
|
|
366
|
-
snapshot.releaseCatalogs.set(input.data.scope_key, input.data);
|
|
367
|
-
return input.data;
|
|
368
|
-
case "channels": {
|
|
369
|
-
const existing = [...snapshot.channels.values()].find(({ name }) => name === input.data.name);
|
|
370
|
-
if (existing && input.onConflict === "ignore") return existing;
|
|
371
|
-
requireUnique(snapshot.channels, input.data.id, input.model);
|
|
372
|
-
if (existing) throw new FirebaseDatabaseConstraintError("channels.name.unique");
|
|
373
|
-
snapshot.channels.set(input.data.id, input.data);
|
|
374
|
-
return input.data;
|
|
375
|
-
}
|
|
376
|
-
case "api_keys": {
|
|
377
|
-
const existing = [...snapshot.apiKeys.values()].find(({ hash }) => hash === input.data.hash);
|
|
378
|
-
if (existing && input.onConflict === "ignore") return existing;
|
|
379
|
-
requireUnique(snapshot.apiKeys, input.data.id, input.model);
|
|
380
|
-
if (existing) throw new FirebaseDatabaseConstraintError("api_keys.hash.unique");
|
|
381
|
-
snapshot.apiKeys.set(input.data.id, input.data);
|
|
382
|
-
return input.data;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
async update(input) {
|
|
387
|
-
if (input.model === "bundle_installations") {
|
|
388
|
-
const current = [...snapshot.bundleInstallations.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
|
|
389
|
-
if (!current) return null;
|
|
390
|
-
const updated = {
|
|
391
|
-
...current,
|
|
392
|
-
...input.update
|
|
393
|
-
};
|
|
394
|
-
snapshot.bundleInstallations.set(current.install_id, updated);
|
|
395
|
-
return updated;
|
|
396
|
-
}
|
|
397
|
-
if (input.model === "api_keys") {
|
|
398
|
-
const current = [...snapshot.apiKeys.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
|
|
399
|
-
if (!current) return null;
|
|
400
|
-
const updated = {
|
|
401
|
-
...current,
|
|
402
|
-
...input.update
|
|
403
|
-
};
|
|
404
|
-
snapshot.apiKeys.set(current.id, updated);
|
|
405
|
-
return updated;
|
|
406
|
-
}
|
|
407
|
-
if (input.model === "releases") {
|
|
408
|
-
const current = [...snapshot.releases.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
|
|
409
|
-
if (!current) return null;
|
|
410
|
-
const updated = {
|
|
411
|
-
...current,
|
|
412
|
-
...input.update
|
|
413
|
-
};
|
|
414
|
-
snapshot.releases.set(current.id, updated);
|
|
415
|
-
return updated;
|
|
416
|
-
}
|
|
417
|
-
if (input.model === "release_catalogs") {
|
|
418
|
-
const current = [...snapshot.releaseCatalogs.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
|
|
419
|
-
if (!current) return null;
|
|
420
|
-
const updated = {
|
|
421
|
-
...current,
|
|
422
|
-
...input.update
|
|
423
|
-
};
|
|
424
|
-
snapshot.releaseCatalogs.set(current.scope_key, updated);
|
|
425
|
-
return updated;
|
|
426
|
-
}
|
|
427
|
-
const current = [...snapshot.bundles.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
|
|
428
|
-
if (!current) return null;
|
|
429
|
-
const updated = {
|
|
430
|
-
...current,
|
|
431
|
-
...input.update
|
|
432
|
-
};
|
|
433
|
-
snapshot.bundles.set(current.id, updated);
|
|
434
|
-
return updated;
|
|
435
|
-
},
|
|
436
|
-
async delete(input) {
|
|
437
|
-
if (input.model === "channels") {
|
|
438
|
-
for (const row of snapshot.channels.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.channels.delete(row.id);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
if (input.model === "bundle_patches") {
|
|
442
|
-
for (const row of snapshot.bundlePatches.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.bundlePatches.delete(row.id);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
if (input.model === "releases") {
|
|
446
|
-
for (const row of snapshot.releases.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.releases.delete(row.id);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
const removedIds = new Set([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)).map(({ id }) => id));
|
|
450
|
-
for (const id of removedIds) snapshot.bundles.delete(id);
|
|
451
|
-
for (const patch of snapshot.bundlePatches.values()) if (removedIds.has(patch.bundle_id) || removedIds.has(patch.base_bundle_id)) snapshot.bundlePatches.delete(patch.id);
|
|
452
|
-
},
|
|
453
|
-
async count(input) {
|
|
454
|
-
switch (input.model) {
|
|
455
|
-
case "bundles": return distinctCount([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
456
|
-
case "bundle_patches": return distinctCount([...snapshot.bundlePatches.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
457
|
-
case "releases": return distinctCount([...snapshot.releases.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
458
|
-
case "bundle_installations": return distinctCount([...snapshot.bundleInstallations.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
459
|
-
case "bundle_events": return distinctCount([...snapshot.bundleEvents.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
460
|
-
}
|
|
461
|
-
},
|
|
462
|
-
async findOne(input) {
|
|
463
|
-
switch (input.model) {
|
|
464
|
-
case "bundles": return [...snapshot.bundles.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
465
|
-
case "api_keys": return [...snapshot.apiKeys.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
466
|
-
case "channels": return [...snapshot.channels.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
467
|
-
case "bundle_patches": return [...snapshot.bundlePatches.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
468
|
-
case "releases": return [...snapshot.releases.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
469
|
-
case "release_catalogs": return [...snapshot.releaseCatalogs.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
470
|
-
case "bundle_installations": return [...snapshot.bundleInstallations.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
471
|
-
}
|
|
472
|
-
},
|
|
473
|
-
async findMany(input) {
|
|
474
|
-
switch (input.model) {
|
|
475
|
-
case "bundles": return queryFirebaseDatabaseRows([...snapshot.bundles.values()], input);
|
|
476
|
-
case "bundle_patches": return queryFirebaseDatabaseRows([...snapshot.bundlePatches.values()], input);
|
|
477
|
-
case "bundle_events": return queryFirebaseDatabaseRows([...snapshot.bundleEvents.values()], input);
|
|
478
|
-
case "bundle_installations": return queryFirebaseDatabaseRows([...snapshot.bundleInstallations.values()], input);
|
|
479
|
-
case "channels": return queryFirebaseDatabaseRows([...snapshot.channels.values()], input);
|
|
480
|
-
case "api_keys": return queryFirebaseDatabaseRows([...snapshot.apiKeys.values()], input);
|
|
481
|
-
case "releases": return queryFirebaseDatabaseRows([...snapshot.releases.values()], input);
|
|
482
|
-
case "release_catalogs": return queryFirebaseDatabaseRows([...snapshot.releaseCatalogs.values()], input);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
//#endregion
|
|
7
|
+
let _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
|
|
8
|
+
let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
|
|
487
9
|
//#region src/firebaseInfrastructureNames.ts
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
bundles: "hot_updater_v1_bundles",
|
|
494
|
-
channels: "hot_updater_v1_channels",
|
|
495
|
-
releaseCatalogs: "hot_updater_v1_release_catalogs",
|
|
496
|
-
releases: "hot_updater_v1_releases",
|
|
497
|
-
settings: "hot_updater_v1_private_settings"
|
|
498
|
-
};
|
|
10
|
+
/**
|
|
11
|
+
* The one collection the storage engine keeps every item in. It namespaces
|
|
12
|
+
* the data, so item partitions are table names with no prefix.
|
|
13
|
+
*/
|
|
14
|
+
const FIREBASE_V1_COLLECTION = "hot_updater_v1";
|
|
499
15
|
//#endregion
|
|
500
|
-
//#region src/
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
16
|
+
//#region src/firestoreStore.ts
|
|
17
|
+
/**
|
|
18
|
+
* Firestore's limits: 10 MiB a request and 1 MiB a document; 500 writes a
|
|
19
|
+
* transaction stays a conservative bound. Indexed values over 1,500 bytes are
|
|
20
|
+
* truncated, so longer keys are refused rather than read inconsistently.
|
|
21
|
+
*/
|
|
22
|
+
const FIRESTORE_LIMITS = {
|
|
23
|
+
items: 500,
|
|
24
|
+
bytes: 1e7,
|
|
25
|
+
itemBytes: 1e6,
|
|
26
|
+
keyBytes: {
|
|
27
|
+
pk: 1500,
|
|
28
|
+
sk: 1500
|
|
507
29
|
}
|
|
508
30
|
};
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
31
|
+
/** `getAll` reads at most this many documents a call. */
|
|
32
|
+
const GET_BATCH = 300;
|
|
33
|
+
/** A document's id: a hash of its key, since keys may hold `/` and outgrow Firestore's 1,500 bytes. */
|
|
34
|
+
const firestoreDocumentId = ({ pk, sk }) => (0, node_crypto.createHash)("sha256").update(JSON.stringify([pk, sk])).digest("hex");
|
|
35
|
+
/**
|
|
36
|
+
* A document is `{ pk, sk, row }`. Row values that are maps or arrays become
|
|
37
|
+
* JSON text inside a map, since Firestore has no nested arrays and does not
|
|
38
|
+
* keep map key order; `row` is exempt from single-field indexing.
|
|
39
|
+
*/
|
|
40
|
+
const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
|
|
41
|
+
const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
|
|
42
|
+
/**
|
|
43
|
+
* The collection's TTL field: a timestamp beside `pk` and `sk` on every item
|
|
44
|
+
* of a row that expires, which Firestore's TTL policy deletes.
|
|
45
|
+
*/
|
|
46
|
+
const FIRESTORE_TTL_FIELD = "expireAt";
|
|
47
|
+
const toDocument = (key, row, expiresAt) => ({
|
|
48
|
+
pk: key.pk,
|
|
49
|
+
sk: key.sk,
|
|
50
|
+
row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
|
|
51
|
+
...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
|
|
519
52
|
});
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
for (const document of snapshot.docs) {
|
|
549
|
-
const row = parseFirebaseChannelRow(document.data(), `channels/${document.id}`);
|
|
550
|
-
if (document.id !== firebaseChannelDocumentId(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.document-key");
|
|
551
|
-
if (rows.has(row.id)) throw new FirebaseDatabaseConstraintError("channels.id.unique");
|
|
552
|
-
if (names.has(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.unique");
|
|
553
|
-
rows.set(row.id, row);
|
|
554
|
-
names.add(row.name);
|
|
555
|
-
}
|
|
556
|
-
return rows;
|
|
557
|
-
};
|
|
558
|
-
const loadFirebaseChannels = async (collections) => [...channelMap(await collections.channels.get()).values()];
|
|
559
|
-
const apiKeyMap = (snapshot) => documentMap("api_keys", snapshot.docs.map((document) => ({
|
|
560
|
-
document,
|
|
561
|
-
row: parseFirebaseApiKeyRow(document.data(), `api_keys/${document.id}`)
|
|
562
|
-
})));
|
|
563
|
-
const releaseMap = (snapshot) => documentMap("releases", snapshot.docs.map((document) => ({
|
|
564
|
-
document,
|
|
565
|
-
row: parseFirebaseReleaseRow(document.data(), `releases/${document.id}`)
|
|
566
|
-
})));
|
|
567
|
-
const releaseCatalogMap = (snapshot) => documentMap("release_catalogs", snapshot.docs.map((document) => ({
|
|
568
|
-
document,
|
|
569
|
-
row: parseFirebaseReleaseCatalogRow(document.data(), `release_catalogs/${document.id}`)
|
|
570
|
-
})));
|
|
571
|
-
const toSnapshot = (documents) => {
|
|
53
|
+
const toRow = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
|
|
54
|
+
const holds = (snapshot, condition) => {
|
|
55
|
+
if (condition === void 0) return true;
|
|
56
|
+
const data = snapshot?.exists ? snapshot.data() : void 0;
|
|
57
|
+
return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
|
|
58
|
+
};
|
|
59
|
+
/** gRPC codes a rerun of the whole write can clear: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL, UNAVAILABLE. */
|
|
60
|
+
const TRANSIENT = /* @__PURE__ */ new Set([
|
|
61
|
+
4,
|
|
62
|
+
8,
|
|
63
|
+
10,
|
|
64
|
+
13,
|
|
65
|
+
14
|
|
66
|
+
]);
|
|
67
|
+
const codeOf = (error) => error.code;
|
|
68
|
+
/** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
|
|
69
|
+
const missingDatabase = (error) => {
|
|
70
|
+
throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The key-value store over one Firestore collection of `{ pk, sk, row }`
|
|
74
|
+
* documents, queried through the composite indexes on (pk, sk) in both
|
|
75
|
+
* directions. A write is one transaction that reads only the documents its
|
|
76
|
+
* ops guard.
|
|
77
|
+
*/
|
|
78
|
+
const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
|
|
79
|
+
const items = firestore.collection(collection);
|
|
80
|
+
const reference = (key) => items.doc(firestoreDocumentId(key));
|
|
572
81
|
return {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
transaction.get(collections.releaseCatalogs)
|
|
609
|
-
]);
|
|
610
|
-
return toSnapshot([
|
|
611
|
-
bundles,
|
|
612
|
-
patches,
|
|
613
|
-
channels,
|
|
614
|
-
apiKeys,
|
|
615
|
-
releases,
|
|
616
|
-
releaseCatalogs
|
|
617
|
-
]);
|
|
618
|
-
};
|
|
619
|
-
const persistCollection = ({ transaction, collection, before, after, documentId }) => {
|
|
620
|
-
for (const [id, row] of before) if (!after.has(id)) transaction.delete(collection.doc(documentId(row)));
|
|
621
|
-
for (const [id, row] of after) if (JSON.stringify(before.get(id)) !== JSON.stringify(row)) transaction.set(collection.doc(documentId(row)), row, { merge: true });
|
|
622
|
-
};
|
|
623
|
-
const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, after }) => {
|
|
624
|
-
persistCollection({
|
|
625
|
-
transaction,
|
|
626
|
-
collection: collections.bundles,
|
|
627
|
-
before: before.bundles,
|
|
628
|
-
after: after.bundles,
|
|
629
|
-
documentId: (row) => row.id
|
|
630
|
-
});
|
|
631
|
-
persistCollection({
|
|
632
|
-
transaction,
|
|
633
|
-
collection: collections.bundlePatches,
|
|
634
|
-
before: before.bundlePatches,
|
|
635
|
-
after: after.bundlePatches,
|
|
636
|
-
documentId: (row) => row.id
|
|
637
|
-
});
|
|
638
|
-
persistCollection({
|
|
639
|
-
transaction,
|
|
640
|
-
collection: collections.channels,
|
|
641
|
-
before: before.channels,
|
|
642
|
-
after: after.channels,
|
|
643
|
-
documentId: (row) => firebaseChannelDocumentId(row.name)
|
|
644
|
-
});
|
|
645
|
-
for (const [id] of before.channels) if (!after.channels.has(id)) transaction.delete(collections.settings.doc(firebaseChannelIdDocumentId(id)));
|
|
646
|
-
for (const [id, row] of after.channels) if (JSON.stringify(before.channels.get(id)) !== JSON.stringify(row)) transaction.set(collections.settings.doc(firebaseChannelIdDocumentId(id)), row);
|
|
647
|
-
persistCollection({
|
|
648
|
-
transaction,
|
|
649
|
-
collection: collections.apiKeys,
|
|
650
|
-
before: before.apiKeys,
|
|
651
|
-
after: after.apiKeys,
|
|
652
|
-
documentId: (row) => row.id
|
|
653
|
-
});
|
|
654
|
-
persistCollection({
|
|
655
|
-
transaction,
|
|
656
|
-
collection: collections.releases,
|
|
657
|
-
before: before.releases,
|
|
658
|
-
after: after.releases,
|
|
659
|
-
documentId: (row) => row.id
|
|
660
|
-
});
|
|
661
|
-
persistCollection({
|
|
662
|
-
transaction,
|
|
663
|
-
collection: collections.releaseCatalogs,
|
|
664
|
-
before: before.releaseCatalogs,
|
|
665
|
-
after: after.releaseCatalogs,
|
|
666
|
-
documentId: (row) => row.scope_key
|
|
667
|
-
});
|
|
668
|
-
};
|
|
669
|
-
const migrateFirebaseDatabase = async (collections) => {
|
|
670
|
-
const versionDocument = collections.settings.doc("database_adapter_version");
|
|
671
|
-
const version = await versionDocument.get();
|
|
672
|
-
const adapterVersion = version.data()?.version;
|
|
673
|
-
if (adapterVersion === 4) return;
|
|
674
|
-
if (version.exists) throw new FirebaseDatabaseAdapterVersionError(adapterVersion);
|
|
675
|
-
if ((await Promise.all([
|
|
676
|
-
collections.bundles.limit(1).get(),
|
|
677
|
-
collections.bundlePatches.limit(1).get(),
|
|
678
|
-
collections.channels.limit(1).get(),
|
|
679
|
-
collections.releases.limit(1).get(),
|
|
680
|
-
collections.releaseCatalogs.limit(1).get(),
|
|
681
|
-
collections.bundleInstallations.limit(1).get(),
|
|
682
|
-
collections.bundleEvents.limit(1).get()
|
|
683
|
-
])).some((snapshot) => !snapshot.empty)) throw new FirebaseDatabaseAdapterVersionError("v0");
|
|
684
|
-
try {
|
|
685
|
-
await versionDocument.create({ version: 4 });
|
|
686
|
-
} catch (error) {
|
|
687
|
-
if ((await versionDocument.get()).data()?.version !== 4) throw error;
|
|
688
|
-
}
|
|
689
|
-
};
|
|
690
|
-
//#endregion
|
|
691
|
-
//#region src/firebaseDatabase.ts
|
|
692
|
-
const exactId = (input) => {
|
|
693
|
-
if (input.where?.length !== 1) return void 0;
|
|
694
|
-
const [condition] = input.where;
|
|
695
|
-
return condition.field === "id" && (condition.operator === void 0 || condition.operator === "eq") && typeof condition.value === "string" ? condition.value : void 0;
|
|
696
|
-
};
|
|
697
|
-
const exactInstallId = (input) => {
|
|
698
|
-
const condition = input.where?.find(({ field, operator }) => field === "install_id" && (operator === void 0 || operator === "eq"));
|
|
699
|
-
return typeof condition?.value === "string" ? condition.value : void 0;
|
|
700
|
-
};
|
|
701
|
-
const firestoreOperator = (operator) => {
|
|
702
|
-
switch (operator ?? "eq") {
|
|
703
|
-
case "eq": return "==";
|
|
704
|
-
case "ne": return "!=";
|
|
705
|
-
case "gt": return ">";
|
|
706
|
-
case "gte": return ">=";
|
|
707
|
-
case "lt": return "<";
|
|
708
|
-
case "lte": return "<=";
|
|
709
|
-
case "in": return "in";
|
|
710
|
-
case "not_in": return "not-in";
|
|
711
|
-
default: return;
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
const applyFirebaseWhere = (initial, where) => {
|
|
715
|
-
let query = initial;
|
|
716
|
-
for (const condition of where) {
|
|
717
|
-
const operator = firestoreOperator(condition.operator);
|
|
718
|
-
if (condition.connector === "OR" || operator === void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
|
|
719
|
-
query = query.where(condition.field, operator, condition.value);
|
|
720
|
-
}
|
|
721
|
-
return query;
|
|
722
|
-
};
|
|
723
|
-
const firebaseDatabase = (config) => {
|
|
724
|
-
const implementation = (() => {
|
|
725
|
-
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
726
|
-
const db = (0, firebase_admin_firestore.getFirestore)(app);
|
|
727
|
-
const collections = createFirebaseDatabaseCollections(db);
|
|
728
|
-
let migration;
|
|
729
|
-
const ensureMigrated = () => {
|
|
730
|
-
migration ??= migrateFirebaseDatabase(collections).catch((error) => {
|
|
731
|
-
migration = void 0;
|
|
732
|
-
throw error;
|
|
733
|
-
});
|
|
734
|
-
return migration;
|
|
735
|
-
};
|
|
736
|
-
const mutate = async (operation) => {
|
|
737
|
-
await ensureMigrated();
|
|
738
|
-
return db.runTransaction(async (transaction) => {
|
|
739
|
-
const before = await loadFirebaseTransactionSnapshot(transaction, collections);
|
|
740
|
-
const after = cloneFirebaseDatabaseSnapshot(before);
|
|
741
|
-
const result = await operation(createFirebaseDatabaseState(after));
|
|
742
|
-
persistFirebaseDatabaseSnapshot({
|
|
743
|
-
transaction,
|
|
744
|
-
collections,
|
|
745
|
-
before,
|
|
746
|
-
after
|
|
747
|
-
});
|
|
748
|
-
return result;
|
|
749
|
-
});
|
|
750
|
-
};
|
|
751
|
-
const read = async (operation) => {
|
|
752
|
-
await ensureMigrated();
|
|
753
|
-
const snapshot = await loadFirebaseDatabaseSnapshot(collections);
|
|
754
|
-
return operation(createFirebaseDatabaseState(snapshot));
|
|
755
|
-
};
|
|
756
|
-
return {
|
|
757
|
-
recordInsights: async ({ event, installation }) => {
|
|
758
|
-
await ensureMigrated();
|
|
759
|
-
await db.runTransaction(async (transaction) => {
|
|
760
|
-
const eventReference = collections.bundleEvents.doc(event.id);
|
|
761
|
-
const installationReference = collections.bundleInstallations.doc(firebaseInstallationDocumentId(installation.install_id));
|
|
762
|
-
const [storedEvent, storedInstallation] = await transaction.getAll(eventReference, installationReference);
|
|
763
|
-
if (storedEvent.exists) return;
|
|
764
|
-
const current = storedInstallation.exists ? requireFirebaseDocumentKey("bundle_installations", storedInstallation.id, parseFirebaseInsightsInstallationRow(storedInstallation.data(), `bundle_installations/${storedInstallation.id}`)) : null;
|
|
765
|
-
transaction.create(eventReference, event);
|
|
766
|
-
if (current === null || installation.received_at_ms > current.received_at_ms || installation.received_at_ms === current.received_at_ms && (0, _hot_updater_plugin_core.compareInsightsText)(installation.id, current.id) > 0) transaction.set(installationReference, installation);
|
|
767
|
-
});
|
|
768
|
-
},
|
|
769
|
-
create: async (input) => {
|
|
770
|
-
if (input.model !== "bundle_events" && input.model !== "bundle_installations") return mutate((database) => database.create(input));
|
|
771
|
-
await ensureMigrated();
|
|
772
|
-
return db.runTransaction(async (transaction) => {
|
|
773
|
-
const collection = input.model === "bundle_events" ? collections.bundleEvents : collections.bundleInstallations;
|
|
774
|
-
const documentId = input.model === "bundle_events" ? input.data.id : firebaseInstallationDocumentId(input.data.install_id);
|
|
775
|
-
const reference = collection.doc(documentId);
|
|
776
|
-
const document = await transaction.get(reference);
|
|
777
|
-
if (document.exists) {
|
|
778
|
-
const row = input.model === "bundle_events" ? requireFirebaseDocumentKey("bundle_events", document.id, parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)) : requireFirebaseDocumentKey("bundle_installations", document.id, parseFirebaseInsightsInstallationRow(document.data(), `bundle_installations/${document.id}`));
|
|
779
|
-
if (input.onConflict === "ignore") return row;
|
|
780
|
-
throw new FirebaseDatabaseConstraintError(`${input.model}.id.unique`);
|
|
781
|
-
}
|
|
782
|
-
transaction.create(reference, input.data);
|
|
783
|
-
return input.data;
|
|
784
|
-
});
|
|
785
|
-
},
|
|
786
|
-
update: async (input) => {
|
|
787
|
-
if (input.model !== "bundle_installations") return mutate((database) => database.update(input));
|
|
788
|
-
const installId = exactInstallId(input);
|
|
789
|
-
if (installId === void 0) return mutate((database) => database.update(input));
|
|
790
|
-
await ensureMigrated();
|
|
791
|
-
return db.runTransaction(async (transaction) => {
|
|
792
|
-
const reference = collections.bundleInstallations.doc(firebaseInstallationDocumentId(installId));
|
|
793
|
-
const document = await transaction.get(reference);
|
|
794
|
-
if (!document.exists) return null;
|
|
795
|
-
const current = requireFirebaseDocumentKey("bundle_installations", document.id, parseFirebaseInsightsInstallationRow(document.data(), `bundle_installations/${document.id}`));
|
|
796
|
-
if (!matchesFirebaseDatabaseWhere(current, input.where)) return null;
|
|
797
|
-
const updated = {
|
|
798
|
-
...current,
|
|
799
|
-
...input.update
|
|
800
|
-
};
|
|
801
|
-
transaction.set(reference, updated, { merge: true });
|
|
802
|
-
return updated;
|
|
803
|
-
});
|
|
804
|
-
},
|
|
805
|
-
delete: (input) => mutate((database) => database.delete(input)),
|
|
806
|
-
count: async (input) => {
|
|
807
|
-
if (input.model !== "bundle_installations" && input.model !== "bundle_events") return read((database) => database.count(input));
|
|
808
|
-
await ensureMigrated();
|
|
809
|
-
let query = applyFirebaseWhere(input.model === "bundle_events" ? collections.bundleEvents : collections.bundleInstallations, input.where ?? []);
|
|
810
|
-
if (input.model === "bundle_events") query = query.orderBy("received_at_ms", "desc").orderBy("id", "desc");
|
|
811
|
-
return (await query.count().get()).data().count;
|
|
812
|
-
},
|
|
813
|
-
findOne: async (input) => {
|
|
814
|
-
if (input.model === "bundle_installations") {
|
|
815
|
-
const installId = exactInstallId(input);
|
|
816
|
-
if (installId === void 0) return read((database) => database.findOne(input));
|
|
817
|
-
await ensureMigrated();
|
|
818
|
-
const document = await collections.bundleInstallations.doc(firebaseInstallationDocumentId(installId)).get();
|
|
819
|
-
return document.exists ? requireFirebaseDocumentKey("bundle_installations", document.id, parseFirebaseInsightsInstallationRow(document.data(), `bundle_installations/${document.id}`)) : null;
|
|
82
|
+
id: "firestore",
|
|
83
|
+
limits: FIRESTORE_LIMITS,
|
|
84
|
+
async get(keys) {
|
|
85
|
+
const found = /* @__PURE__ */ new Map();
|
|
86
|
+
const ids = [...new Set(keys.map(firestoreDocumentId))];
|
|
87
|
+
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
88
|
+
const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
|
|
89
|
+
for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow(snapshot.data()));
|
|
90
|
+
}
|
|
91
|
+
return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
|
|
92
|
+
},
|
|
93
|
+
async query({ pk, gte, lt, order, limit, after }) {
|
|
94
|
+
let query = items.where("pk", "==", pk);
|
|
95
|
+
if (gte !== void 0) query = query.where("sk", ">=", gte);
|
|
96
|
+
if (lt !== void 0) query = query.where("sk", "<", lt);
|
|
97
|
+
query = query.orderBy("sk", order);
|
|
98
|
+
if (after !== void 0) query = query.startAfter(after);
|
|
99
|
+
const size = Math.min(limit, nativePageSize ?? limit);
|
|
100
|
+
const { docs } = await query.limit(size).get().catch(missingDatabase);
|
|
101
|
+
return {
|
|
102
|
+
items: docs.map((document) => ({
|
|
103
|
+
sk: document.get("sk"),
|
|
104
|
+
value: toRow(document.data())
|
|
105
|
+
})),
|
|
106
|
+
more: docs.length === size
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
async write(ops) {
|
|
110
|
+
const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
|
|
111
|
+
const attempt = (reading) => firestore.runTransaction(async (transaction) => {
|
|
112
|
+
const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
|
|
113
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
114
|
+
if (read.length > 0) {
|
|
115
|
+
const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
|
|
116
|
+
read.forEach((position, at) => snapshots.set(position, found[at]));
|
|
820
117
|
}
|
|
821
|
-
const
|
|
822
|
-
if (
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
case "bundle_patches": {
|
|
830
|
-
const document = await collections.bundlePatches.doc(id).get();
|
|
831
|
-
return document.exists ? requireFirebaseDocumentKey("bundle_patches", document.id, parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)) : null;
|
|
832
|
-
}
|
|
833
|
-
case "api_keys": {
|
|
834
|
-
const document = await collections.apiKeys.doc(id).get();
|
|
835
|
-
return document.exists ? requireFirebaseDocumentKey("api_keys", document.id, parseFirebaseApiKeyRow(document.data(), `api_keys/${document.id}`)) : null;
|
|
836
|
-
}
|
|
837
|
-
default: return read((database) => database.findOne(input));
|
|
118
|
+
const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
|
|
119
|
+
if (failed !== void 0) {
|
|
120
|
+
if (!reading && blind.slice(0, failed).includes(true)) return null;
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
failedOp: failed
|
|
124
|
+
};
|
|
838
125
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
126
|
+
for (const [position, op] of ops.entries()) {
|
|
127
|
+
const document = reference(op.key);
|
|
128
|
+
if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
|
|
129
|
+
else if (op.type === "delete") transaction.delete(document);
|
|
130
|
+
else if (op.type === "add") {
|
|
131
|
+
if (snapshots.get(position)?.exists === false) {
|
|
132
|
+
const created = { ...op.init };
|
|
133
|
+
for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
|
|
134
|
+
transaction.set(document, toDocument(op.key, created, op.expiresAt));
|
|
135
|
+
} else {
|
|
136
|
+
const [[name, delta], ...more] = Object.entries(op.by);
|
|
137
|
+
const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
|
|
138
|
+
transaction.update(document, new firebase_admin_firestore.FieldPath("row", name), firebase_admin_firestore.FieldValue.increment(delta), ...more.flatMap(([other, by]) => [new firebase_admin_firestore.FieldPath("row", other), firebase_admin_firestore.FieldValue.increment(by)]), ...ttl);
|
|
139
|
+
}
|
|
848
140
|
}
|
|
849
|
-
return (await query.offset(input.offset).limit(input.limit).get()).docs.map((document) => input.model === "bundle_events" ? requireFirebaseDocumentKey("bundle_events", document.id, parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)) : requireFirebaseDocumentKey("bundle_installations", document.id, parseFirebaseInsightsInstallationRow(document.data(), `bundle_installations/${document.id}`)));
|
|
850
141
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
142
|
+
return { ok: true };
|
|
143
|
+
}, { maxAttempts: 5 });
|
|
144
|
+
try {
|
|
145
|
+
try {
|
|
146
|
+
const result = await attempt(false);
|
|
147
|
+
if (result !== null) return result;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
|
|
854
150
|
}
|
|
855
|
-
return
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
transaction.create(reference, input.row);
|
|
877
|
-
transaction.create(idReference, input.row);
|
|
878
|
-
return {
|
|
879
|
-
row: input.row,
|
|
880
|
-
inserted: true
|
|
881
|
-
};
|
|
882
|
-
});
|
|
883
|
-
},
|
|
884
|
-
deleteChannel: async ({ id }) => {
|
|
885
|
-
await ensureMigrated();
|
|
886
|
-
return db.runTransaction(async (transaction) => {
|
|
887
|
-
const idReference = collections.settings.doc(firebaseChannelIdDocumentId(id));
|
|
888
|
-
const idDocument = await transaction.get(idReference);
|
|
889
|
-
if (!idDocument.exists) return {
|
|
890
|
-
deleted: false,
|
|
891
|
-
reason: "not_found"
|
|
892
|
-
};
|
|
893
|
-
const row = parseFirebaseChannelRow(idDocument.data(), `${FIREBASE_V1_COLLECTION_NAMES.settings}/${idDocument.id}`);
|
|
894
|
-
const reference = collections.channels.doc(firebaseChannelDocumentId(row.name));
|
|
895
|
-
if (!(await transaction.get(reference)).exists || row.id !== id) throw new FirebaseDatabaseConstraintError("channels.id.registry");
|
|
896
|
-
if (!(await transaction.get(collections.releases.where("channel_id", "==", id).limit(1))).empty) return {
|
|
897
|
-
deleted: false,
|
|
898
|
-
reason: "not_empty"
|
|
899
|
-
};
|
|
900
|
-
transaction.delete(reference);
|
|
901
|
-
transaction.delete(idReference);
|
|
902
|
-
return { deleted: true };
|
|
903
|
-
});
|
|
904
|
-
},
|
|
905
|
-
transaction: (callback) => mutate(callback)
|
|
906
|
-
};
|
|
907
|
-
})();
|
|
908
|
-
const adapter = (0, _hot_updater_plugin_core_internal.createDatabasePluginAdapter)("firebaseDatabase", implementation);
|
|
909
|
-
return (0, _hot_updater_plugin_core.createDatabasePlugin)({
|
|
910
|
-
name: "firebaseDatabase",
|
|
911
|
-
models: adapter.models,
|
|
912
|
-
commit: adapter.commit
|
|
913
|
-
});
|
|
151
|
+
return await attempt(true);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (TRANSIENT.has(codeOf(error))) return {
|
|
154
|
+
ok: false,
|
|
155
|
+
retry: true
|
|
156
|
+
};
|
|
157
|
+
return missingDatabase(error);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
/** A plain batch, which reads nothing, 500 deletes at a time. */
|
|
161
|
+
async deleteConsumed(keys) {
|
|
162
|
+
for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
|
|
163
|
+
const batch = firestore.batch();
|
|
164
|
+
for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
|
|
165
|
+
await batch.commit().catch(missingDatabase);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
async dispose() {}
|
|
169
|
+
};
|
|
914
170
|
};
|
|
915
171
|
//#endregion
|
|
172
|
+
//#region src/firebaseDatabase.ts
|
|
173
|
+
const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
|
|
174
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
|
|
175
|
+
return (0, _hot_updater_plugin_core.createKvAdapter)({ store: createFirestoreStore({
|
|
176
|
+
firestore: (0, firebase_admin_firestore.getFirestore)(app),
|
|
177
|
+
collection
|
|
178
|
+
}) });
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Writes the schema settings of core and `plugins`, the plugins the server
|
|
182
|
+
* runs, which the database checks before its first read. Firestore needs no
|
|
183
|
+
* other setup beyond `firestore.indexes.json`; `hot-updater init` runs it
|
|
184
|
+
* after deploying the indexes.
|
|
185
|
+
*/
|
|
186
|
+
const migrateFirebaseDatabase = (config, plugins = []) => (0, _hot_updater_plugin_core.migrateCoreSchema)(adapterOf(config), "firebaseDatabase", plugins);
|
|
187
|
+
/** Hot Updater's database in one Firestore collection, through the storage engine. */
|
|
188
|
+
const firebaseDatabase = (config) => (0, _hot_updater_plugin_core.createEngineDatabase)({
|
|
189
|
+
name: "firebaseDatabase",
|
|
190
|
+
adapter: adapterOf(config),
|
|
191
|
+
aggregateBatching: config.aggregateBatching ?? {}
|
|
192
|
+
});
|
|
193
|
+
//#endregion
|
|
916
194
|
//#region src/firebaseStorage.ts
|
|
917
195
|
const firebaseStorage = (config) => {
|
|
918
196
|
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
@@ -923,7 +201,7 @@ const firebaseStorage = (config) => {
|
|
|
923
201
|
if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
|
|
924
202
|
return parsed;
|
|
925
203
|
};
|
|
926
|
-
return (0, _hot_updater_plugin_core.
|
|
204
|
+
return (0, _hot_updater_plugin_core.createStorageAdapter)({
|
|
927
205
|
name: "firebaseStorage",
|
|
928
206
|
protocol: "gs",
|
|
929
207
|
async put({ key, body, contentType }) {
|
|
@@ -983,5 +261,14 @@ const firebaseStorage = (config) => {
|
|
|
983
261
|
});
|
|
984
262
|
};
|
|
985
263
|
//#endregion
|
|
264
|
+
//#region src/plugins.ts
|
|
265
|
+
/**
|
|
266
|
+
* The plugins the managed Firebase server runs: Insights, and API keys on every
|
|
267
|
+
* client route. Its Cloud Function imports this list.
|
|
268
|
+
*/
|
|
269
|
+
const plugins = [(0, _hot_updater_server_plugins_insights.insights)(), (0, _hot_updater_server_plugins_api_keys.apiKeys)()];
|
|
270
|
+
//#endregion
|
|
986
271
|
exports.firebaseDatabase = firebaseDatabase;
|
|
987
272
|
exports.firebaseStorage = firebaseStorage;
|
|
273
|
+
exports.migrateFirebaseDatabase = migrateFirebaseDatabase;
|
|
274
|
+
exports.plugins = plugins;
|