@hot-updater/firebase 1.0.0-rc.2 → 1.0.0-rc.22

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 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
- //#region src/firebaseDatabaseParserShared.ts
8
- var FirebaseDatabaseDataError = class extends Error {
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
- const FIREBASE_V1_COLLECTION_NAMES = {
489
- apiKeys: "hot_updater_v1_api_keys",
490
- bundleEvents: "hot_updater_v1_bundle_events",
491
- bundleInstallations: "hot_updater_v1_bundle_installations",
492
- bundlePatches: "hot_updater_v1_bundle_patches",
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/firebaseDatabasePersistence.ts
501
- var FirebaseDatabaseAdapterVersionError = class extends Error {
502
- version;
503
- name = "FirebaseDatabaseAdapterVersionError";
504
- constructor(version) {
505
- super(`Unsupported Firebase database adapter version: ${String(version)}`);
506
- this.version = version;
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
- const createFirebaseDatabaseCollections = (db) => ({
510
- bundles: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundles),
511
- bundlePatches: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundlePatches),
512
- bundleEvents: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundleEvents),
513
- bundleInstallations: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundleInstallations),
514
- channels: db.collection(FIREBASE_V1_COLLECTION_NAMES.channels),
515
- apiKeys: db.collection(FIREBASE_V1_COLLECTION_NAMES.apiKeys),
516
- releaseCatalogs: db.collection(FIREBASE_V1_COLLECTION_NAMES.releaseCatalogs),
517
- releases: db.collection(FIREBASE_V1_COLLECTION_NAMES.releases),
518
- settings: db.collection(FIREBASE_V1_COLLECTION_NAMES.settings)
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 firebaseChannelDocumentId = (name) => `name_${Buffer.from(name, "utf8").toString("base64url")}`;
521
- const firebaseChannelIdDocumentId = (id) => `channel_id_${Buffer.from(id, "utf8").toString("base64url")}`;
522
- const firebaseInstallationDocumentId = (id) => `install_${Buffer.from(id, "utf8").toString("base64url")}`;
523
- const requireFirebaseDocumentKey = (model, documentId, row) => {
524
- if (documentId !== (model === "bundle_installations" ? firebaseInstallationDocumentId(row.install_id) : "id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
525
- return row;
526
- };
527
- const documentMap = (model, documents) => {
528
- const rows = /* @__PURE__ */ new Map();
529
- for (const { row } of documents) {
530
- const key = model === "bundle_installations" ? row.install_id : "id" in row ? row.id : row.scope_key;
531
- if (rows.has(key)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
532
- rows.set(key, row);
533
- }
534
- for (const { document, row } of documents) requireFirebaseDocumentKey(model, document.id, row);
535
- return rows;
536
- };
537
- const bundleMap = (snapshot) => documentMap("bundles", snapshot.docs.map((document) => ({
538
- document,
539
- row: parseFirebaseBundleRow(document.data(), `bundles/${document.id}`)
540
- })));
541
- const patchMap = (snapshot) => documentMap("bundle_patches", snapshot.docs.map((document) => ({
542
- document,
543
- row: parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)
544
- })));
545
- const channelMap = (snapshot) => {
546
- const rows = /* @__PURE__ */ new Map();
547
- const names = /* @__PURE__ */ new Set();
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
- bundles: bundleMap(documents[0]),
574
- bundlePatches: patchMap(documents[1]),
575
- bundleEvents: /* @__PURE__ */ new Map(),
576
- bundleInstallations: /* @__PURE__ */ new Map(),
577
- channels: channelMap(documents[2]),
578
- apiKeys: apiKeyMap(documents[3]),
579
- releases: releaseMap(documents[4]),
580
- releaseCatalogs: releaseCatalogMap(documents[5])
581
- };
582
- };
583
- const loadFirebaseDatabaseSnapshot = async (collections) => {
584
- const [bundles, patches, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
585
- collections.bundles.get(),
586
- collections.bundlePatches.get(),
587
- collections.channels.get(),
588
- collections.apiKeys.get(),
589
- collections.releases.get(),
590
- collections.releaseCatalogs.get()
591
- ]);
592
- return toSnapshot([
593
- bundles,
594
- patches,
595
- channels,
596
- apiKeys,
597
- releases,
598
- releaseCatalogs
599
- ]);
600
- };
601
- const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
602
- const [bundles, patches, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
603
- transaction.get(collections.bundles),
604
- transaction.get(collections.bundlePatches),
605
- transaction.get(collections.channels),
606
- transaction.get(collections.apiKeys),
607
- transaction.get(collections.releases),
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 id = exactId(input);
822
- if (id === void 0) return read((database) => database.findOne(input));
823
- await ensureMigrated();
824
- switch (input.model) {
825
- case "bundles": {
826
- const document = await collections.bundles.doc(id).get();
827
- return document.exists ? requireFirebaseDocumentKey("bundles", document.id, parseFirebaseBundleRow(document.data(), `bundles/${document.id}`)) : null;
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
- findMany: async (input) => {
841
- if (input.model === "bundle_events" || input.model === "bundle_installations") {
842
- await ensureMigrated();
843
- const collection = input.model === "bundle_events" ? collections.bundleEvents : collections.bundleInstallations;
844
- let query = applyFirebaseWhere(collection, input.where ?? []);
845
- for (const order of input.orderBy ?? []) {
846
- if (order.nulls !== void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
847
- query = query.orderBy(order.field, order.direction);
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
- if (input.model === "channels") {
852
- await ensureMigrated();
853
- return queryFirebaseDatabaseRows(await loadFirebaseChannels(collections), input);
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 read((database) => database.findMany(input));
856
- },
857
- insertChannel: async (input) => {
858
- await ensureMigrated();
859
- return db.runTransaction(async (transaction) => {
860
- const reference = collections.channels.doc(firebaseChannelDocumentId(input.row.name));
861
- const idReference = collections.settings.doc(firebaseChannelIdDocumentId(input.row.id));
862
- const [document, idDocument] = await transaction.getAll(reference, idReference);
863
- if (idDocument.exists) {
864
- const row = parseFirebaseChannelRow(idDocument.data(), `${FIREBASE_V1_COLLECTION_NAMES.settings}/${idDocument.id}`);
865
- if (row.id !== input.row.id || row.name !== input.row.name) throw new FirebaseDatabaseConstraintError("channels.id.registry");
866
- }
867
- if (idDocument.exists && !document.exists) throw new FirebaseDatabaseConstraintError("channels.id.unique");
868
- if (document.exists) {
869
- const row = parseFirebaseChannelRow(document.data(), `channels/${document.id}`);
870
- if (document.id !== firebaseChannelDocumentId(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.document-key");
871
- return {
872
- row,
873
- inserted: false
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.createStoragePlugin)({
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;