@hot-updater/firebase 0.36.7 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,371 +1,847 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region \0rolldown/runtime.js
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
- key = keys[i];
12
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
- get: ((k) => from[k]).bind(null, key),
14
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
- });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
- value: mod,
21
- enumerable: true
22
- }) : target, mod));
23
- //#endregion
24
- let _hot_updater_core = require("@hot-updater/core");
25
2
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
3
+ let _hot_updater_plugin_core_internal = require("@hot-updater/plugin-core/internal");
26
4
  let firebase_admin_app = require("firebase-admin/app");
27
5
  let firebase_admin_firestore = require("firebase-admin/firestore");
28
- let fs_promises = require("fs/promises");
29
- fs_promises = __toESM(fs_promises);
30
- let path = require("path");
31
- path = __toESM(path);
32
6
  let firebase_admin_storage = require("firebase-admin/storage");
33
- //#region src/firebaseDatabase.ts
34
- const bundleMatchesQueryWhere = (bundle, where) => {
35
- if (!where) return true;
36
- if (where.channel !== void 0 && bundle.channel !== where.channel) return false;
37
- if (where.platform !== void 0 && bundle.platform !== where.platform) return false;
38
- if (where.enabled !== void 0 && bundle.enabled !== where.enabled) return false;
39
- if (where.id?.eq !== void 0 && bundle.id !== where.id.eq) return false;
40
- if (where.id?.gt !== void 0 && bundle.id.localeCompare(where.id.gt) <= 0) return false;
41
- if (where.id?.gte !== void 0 && bundle.id.localeCompare(where.id.gte) < 0) return false;
42
- if (where.id?.lt !== void 0 && bundle.id.localeCompare(where.id.lt) >= 0) return false;
43
- if (where.id?.lte !== void 0 && bundle.id.localeCompare(where.id.lte) > 0) return false;
44
- if (where.id?.in && !where.id.in.includes(bundle.id)) return false;
45
- if (where.targetAppVersionNotNull && bundle.targetAppVersion === null) return false;
46
- if (where.targetAppVersion !== void 0 && bundle.targetAppVersion !== where.targetAppVersion) return false;
47
- if (where.targetAppVersionIn && !where.targetAppVersionIn.includes(bundle.targetAppVersion ?? "")) return false;
48
- if (where.fingerprintHash !== void 0 && bundle.fingerprintHash !== where.fingerprintHash) return false;
49
- return true;
50
- };
51
- const sortBundles = (bundles, orderBy) => {
52
- const direction = orderBy?.direction ?? "desc";
53
- return bundles.slice().sort((a, b) => {
54
- const result = a.id.localeCompare(b.id);
55
- return direction === "asc" ? result : -result;
56
- });
7
+ //#region src/firebaseDatabaseParserShared.ts
8
+ var FirebaseDatabaseDataError = class extends Error {
9
+ name = "FirebaseDatabaseDataError";
10
+ constructor(source) {
11
+ super(`Invalid Firebase database data at "${source}".`);
12
+ this.source = source;
13
+ }
14
+ };
15
+ const record = (value, source) => {
16
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new FirebaseDatabaseDataError(source);
17
+ return value;
18
+ };
19
+ const property = (value, key) => Reflect.get(value, key);
20
+ const string = (value, source) => {
21
+ if (typeof value !== "string") throw new FirebaseDatabaseDataError(source);
22
+ return value;
23
+ };
24
+ const nullableString = (value, source) => {
25
+ if (value === null || value === void 0) return null;
26
+ return string(value, source);
27
+ };
28
+ const boolean = (value, source) => {
29
+ if (typeof value !== "boolean") throw new FirebaseDatabaseDataError(source);
30
+ return value;
31
+ };
32
+ const number = (value, source) => {
33
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new FirebaseDatabaseDataError(source);
34
+ return value;
35
+ };
36
+ const byteSize = (value, source) => {
37
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new FirebaseDatabaseDataError(source);
38
+ return value;
39
+ };
40
+ const stringArray = (value, source) => {
41
+ if (value === null || value === void 0) return null;
42
+ if (!Array.isArray(value)) throw new FirebaseDatabaseDataError(source);
43
+ return value.map((item) => string(item, source));
44
+ };
45
+ const platform = (value, source) => {
46
+ if (value === "android" || value === "ios") return value;
47
+ throw new FirebaseDatabaseDataError(source);
48
+ };
49
+ //#endregion
50
+ //#region src/firebaseDatabaseParser.ts
51
+ const metadata = (value, source) => {
52
+ if (!(0, _hot_updater_plugin_core.isDatabaseMetadataObject)(value)) throw new FirebaseDatabaseDataError(source);
53
+ return value;
54
+ };
55
+ const requiredNullableString = (input, key, source) => {
56
+ if (!Object.hasOwn(input, key)) throw new FirebaseDatabaseDataError(source);
57
+ return nullableString(property(input, key), source);
58
+ };
59
+ const parseFirebaseBundleRow = (value, source) => {
60
+ const input = record(value, source);
61
+ return {
62
+ id: string(property(input, "id"), source),
63
+ platform: platform(property(input, "platform"), source),
64
+ file_hash: string(property(input, "file_hash"), source),
65
+ git_commit_hash: nullableString(property(input, "git_commit_hash"), source),
66
+ storage_uri: string(property(input, "storage_uri"), source),
67
+ archive_byte_size: byteSize(property(input, "archive_byte_size"), source),
68
+ metadata: metadata(property(input, "metadata"), source),
69
+ manifest_storage_uri: nullableString(property(input, "manifest_storage_uri"), source),
70
+ manifest_file_hash: nullableString(property(input, "manifest_file_hash"), source),
71
+ asset_base_storage_uri: nullableString(property(input, "asset_base_storage_uri"), source)
72
+ };
73
+ };
74
+ const parseFirebaseChannelRow = (value, source) => {
75
+ const input = record(value, source);
76
+ return {
77
+ id: string(property(input, "id"), source),
78
+ name: string(property(input, "name"), source)
79
+ };
80
+ };
81
+ const parseFirebasePatchRow = (value, source) => {
82
+ const input = record(value, source);
83
+ return {
84
+ id: string(property(input, "id"), source),
85
+ bundle_id: string(property(input, "bundle_id"), source),
86
+ base_bundle_id: string(property(input, "base_bundle_id"), source),
87
+ base_file_hash: string(property(input, "base_file_hash"), source),
88
+ patch_file_hash: string(property(input, "patch_file_hash"), source),
89
+ patch_storage_uri: string(property(input, "patch_storage_uri"), source),
90
+ byte_size: byteSize(property(input, "byte_size"), source),
91
+ order_index: number(property(input, "order_index"), source)
92
+ };
93
+ };
94
+ const parseFirebaseBundleEventRow = (value, source) => {
95
+ const input = record(value, source);
96
+ const type = string(property(input, "type"), source);
97
+ const fromBundleId = requiredNullableString(input, "from_bundle_id", source);
98
+ const updateStrategy = requiredNullableString(input, "update_strategy", source);
99
+ 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);
100
+ return {
101
+ id: string(property(input, "id"), source),
102
+ type,
103
+ install_id: string(property(input, "install_id"), source),
104
+ user_id: nullableString(property(input, "user_id"), source),
105
+ username: nullableString(property(input, "username"), source),
106
+ from_release_id: requiredNullableString(input, "from_release_id", source),
107
+ from_bundle_id: fromBundleId,
108
+ to_release_id: requiredNullableString(input, "to_release_id", source),
109
+ to_bundle_id: string(property(input, "to_bundle_id"), source),
110
+ platform: platform(property(input, "platform"), source),
111
+ app_version: string(property(input, "app_version"), source),
112
+ channel: string(property(input, "channel"), source),
113
+ cohort: string(property(input, "cohort"), source),
114
+ update_strategy: updateStrategy,
115
+ fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
116
+ sdk_version: nullableString(property(input, "sdk_version"), source),
117
+ received_at_ms: number(property(input, "received_at_ms"), source)
118
+ };
119
+ };
120
+ const parseFirebaseApiKeyRow = (value, source) => {
121
+ const input = record(value, source);
122
+ const role = string(property(input, "role"), source);
123
+ if (role !== "client") throw new FirebaseDatabaseDataError(source);
124
+ const revokedAt = property(input, "revoked_at_ms");
125
+ return {
126
+ id: string(property(input, "id"), source),
127
+ hash: string(property(input, "hash"), source),
128
+ name: string(property(input, "name"), source),
129
+ prefix: string(property(input, "prefix"), source),
130
+ role,
131
+ created_at_ms: number(property(input, "created_at_ms"), source),
132
+ revoked_at_ms: revokedAt === null ? null : number(revokedAt, source)
133
+ };
57
134
  };
58
- const applyFirestoreQueryableFilters = (query, where) => {
59
- let nextQuery = query;
60
- if (where?.channel) nextQuery = nextQuery.where("channel", "==", where.channel);
61
- if (where?.platform) nextQuery = nextQuery.where("platform", "==", where.platform);
62
- if (where?.enabled !== void 0) nextQuery = nextQuery.where("enabled", "==", where.enabled);
63
- if (where?.fingerprintHash !== void 0 && where.fingerprintHash !== null) nextQuery = nextQuery.where("fingerprint_hash", "==", where.fingerprintHash);
64
- if (where?.targetAppVersion !== void 0 && where.targetAppVersion !== null) nextQuery = nextQuery.where("target_app_version", "==", where.targetAppVersion);
65
- if (where?.id?.eq) nextQuery = nextQuery.where("id", "==", where.id.eq);
66
- if (where?.id?.gt) nextQuery = nextQuery.where("id", ">", where.id.gt);
67
- if (where?.id?.gte) nextQuery = nextQuery.where("id", ">=", where.id.gte);
68
- if (where?.id?.lt) nextQuery = nextQuery.where("id", "<", where.id.lt);
69
- if (where?.id?.lte) nextQuery = nextQuery.where("id", "<=", where.id.lte);
70
- return nextQuery;
71
- };
72
- const requiresInMemoryFiltering = (where) => {
73
- return Boolean(where?.id?.in || where?.targetAppVersionIn || where?.targetAppVersionNotNull || where?.targetAppVersion === null || where?.fingerprintHash === null);
74
- };
75
- const chunkValues = (values, size) => {
76
- const chunks = [];
77
- for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
78
- return chunks;
79
- };
80
- const convertToBundle = (firestoreData) => {
81
- const rawMetadata = firestoreData.metadata;
82
- const storedPatches = firestoreData.patches;
83
- const patches = storedPatches && Array.isArray(storedPatches) ? storedPatches : (0, _hot_updater_core.getBundlePatches)({
84
- metadata: rawMetadata,
85
- patchBaseBundleId: firestoreData.patch_base_bundle_id ?? null,
86
- patchBaseFileHash: firestoreData.patch_base_file_hash ?? null,
87
- patchFileHash: firestoreData.patch_file_hash ?? null,
88
- patchStorageUri: firestoreData.patch_storage_uri ?? null
135
+ const parseFirebaseReleaseRow = (value, source) => {
136
+ const input = record(value, source);
137
+ const kind = string(property(input, "kind"), source);
138
+ const strategy = string(property(input, "strategy"), source);
139
+ const operation = string(property(input, "operation"), source);
140
+ const targetCohorts = stringArray(property(input, "target_cohorts"), source);
141
+ if (kind !== "BUNDLE" && kind !== "EMBEDDED" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT" || operation !== "DEPLOY" && operation !== "PROMOTE" && operation !== "ROLLBACK" || targetCohorts === null) throw new FirebaseDatabaseDataError(source);
142
+ return {
143
+ id: string(property(input, "id"), source),
144
+ revision: number(property(input, "revision"), source),
145
+ scope_key: string(property(input, "scope_key"), source),
146
+ channel_id: string(property(input, "channel_id"), source),
147
+ platform: platform(property(input, "platform"), source),
148
+ kind,
149
+ bundle_id: nullableString(property(input, "bundle_id"), source),
150
+ strategy,
151
+ target_app_version: nullableString(property(input, "target_app_version"), source),
152
+ fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
153
+ enabled: boolean(property(input, "enabled"), source),
154
+ should_force_update: boolean(property(input, "should_force_update"), source),
155
+ message: nullableString(property(input, "message"), source),
156
+ rollout_cohort_count: number(property(input, "rollout_cohort_count"), source),
157
+ target_cohorts: targetCohorts,
158
+ operation,
159
+ source_release_id: nullableString(property(input, "source_release_id"), source),
160
+ created_at_ms: number(property(input, "created_at_ms"), source),
161
+ updated_at_ms: number(property(input, "updated_at_ms"), source)
162
+ };
163
+ };
164
+ const parseFirebaseReleaseCatalogRow = (value, source) => {
165
+ const input = record(value, source);
166
+ const strategy = string(property(input, "strategy"), source);
167
+ if (strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") throw new FirebaseDatabaseDataError(source);
168
+ return {
169
+ scope_key: string(property(input, "scope_key"), source),
170
+ catalog_id: string(property(input, "catalog_id"), source),
171
+ strategy,
172
+ channel_id: string(property(input, "channel_id"), source),
173
+ channel_key: string(property(input, "channel_key"), source),
174
+ platform: platform(property(input, "platform"), source),
175
+ fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
176
+ generation: number(property(input, "generation"), source),
177
+ payload: string(property(input, "payload"), source),
178
+ catalog_hash: string(property(input, "catalog_hash"), source),
179
+ byte_size: number(property(input, "byte_size"), source),
180
+ is_tombstone: boolean(property(input, "is_tombstone"), source),
181
+ updated_at_ms: number(property(input, "updated_at_ms"), source)
182
+ };
183
+ };
184
+ //#endregion
185
+ //#region src/firebaseDatabaseQuery.ts
186
+ const compare = (left, right) => {
187
+ if (typeof left === "number" && typeof right === "number") return left - right;
188
+ return String(left).localeCompare(String(right));
189
+ };
190
+ const normalizeStringComparison = (actual, expected, mode) => {
191
+ if (typeof actual !== "string") return null;
192
+ return mode === "insensitive" ? [actual.toLocaleLowerCase(), expected.toLocaleLowerCase()] : [actual, expected];
193
+ };
194
+ const matchesCondition = (row, condition) => {
195
+ const actual = Reflect.get(row, condition.field);
196
+ const expected = condition.value;
197
+ switch (condition.operator ?? "eq") {
198
+ case "eq": {
199
+ if (typeof expected !== "string") return actual === expected;
200
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
201
+ return comparison !== null && comparison[0] === comparison[1];
202
+ }
203
+ case "ne": {
204
+ if (actual === null || actual === void 0) return false;
205
+ if (typeof expected !== "string") return actual !== expected;
206
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
207
+ return comparison === null || comparison[0] !== comparison[1];
208
+ }
209
+ case "gt":
210
+ if (actual === null || actual === void 0) return false;
211
+ return compare(actual, expected) > 0;
212
+ case "gte":
213
+ if (actual === null || actual === void 0) return false;
214
+ return compare(actual, expected) >= 0;
215
+ case "lt":
216
+ if (actual === null || actual === void 0) return false;
217
+ return compare(actual, expected) < 0;
218
+ case "lte":
219
+ if (actual === null || actual === void 0) return false;
220
+ return compare(actual, expected) <= 0;
221
+ case "in":
222
+ if (!Array.isArray(expected)) return false;
223
+ return expected.some((candidate) => candidate === actual);
224
+ case "not_in": {
225
+ if (!Array.isArray(expected)) return false;
226
+ const values = expected;
227
+ return values.length === 0 || actual !== null && actual !== void 0 && values.every((candidate) => candidate !== actual);
228
+ }
229
+ case "contains": {
230
+ if (typeof expected !== "string") return false;
231
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
232
+ return comparison?.[0].includes(comparison[1]) ?? false;
233
+ }
234
+ case "starts_with": {
235
+ if (typeof expected !== "string") return false;
236
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
237
+ return comparison?.[0].startsWith(comparison[1]) ?? false;
238
+ }
239
+ case "ends_with": {
240
+ if (typeof expected !== "string") return false;
241
+ const comparison = normalizeStringComparison(actual, expected, "mode" in condition ? condition.mode : void 0);
242
+ return comparison?.[0].endsWith(comparison[1]) ?? false;
243
+ }
244
+ }
245
+ };
246
+ const matchesFirebaseDatabaseWhere = (row, where) => {
247
+ const first = where?.[0];
248
+ if (!first) return true;
249
+ let result = matchesCondition(row, first);
250
+ for (const condition of where.slice(1)) {
251
+ const current = matchesCondition(row, condition);
252
+ result = condition.connector === "OR" ? result || current : result && current;
253
+ }
254
+ return result;
255
+ };
256
+ const queryFirebaseDatabaseRows = (rows, input) => {
257
+ const filtered = rows.filter((row) => matchesFirebaseDatabaseWhere(row, input.where));
258
+ const orderBy = input.orderBy;
259
+ if (orderBy !== void 0) filtered.sort((left, right) => {
260
+ for (const clause of orderBy) {
261
+ const leftValue = Reflect.get(left, clause.field);
262
+ const rightValue = Reflect.get(right, clause.field);
263
+ if (leftValue === rightValue) continue;
264
+ if (clause.nulls !== void 0 && (leftValue === null || rightValue === null)) {
265
+ const nullComparison = leftValue === null ? -1 : 1;
266
+ return clause.nulls === "first" ? nullComparison : -nullComparison;
267
+ }
268
+ const comparison = compare(leftValue, rightValue);
269
+ if (comparison !== 0) return clause.direction === "asc" ? comparison : -comparison;
270
+ }
271
+ return 0;
89
272
  });
90
- const primaryPatch = patches[0] ?? null;
273
+ const distinctOn = input.distinctOn;
274
+ if (distinctOn === void 0) return filtered.slice(input.offset, input.offset + input.limit);
275
+ const seen = /* @__PURE__ */ new Set();
276
+ return filtered.filter((row) => {
277
+ const key = JSON.stringify(distinctOn.fields.map((field) => Reflect.get(row, field)));
278
+ if (seen.has(key)) return false;
279
+ seen.add(key);
280
+ return true;
281
+ }).slice(input.offset, input.offset + input.limit);
282
+ };
283
+ //#endregion
284
+ //#region src/firebaseDatabaseState.ts
285
+ var FirebaseDatabaseConstraintError = class extends Error {
286
+ name = "FirebaseDatabaseConstraintError";
287
+ constructor(constraint) {
288
+ super(`Firebase database constraint failed: ${constraint}`);
289
+ this.constraint = constraint;
290
+ }
291
+ };
292
+ const cloneFirebaseDatabaseSnapshot = (snapshot) => ({
293
+ bundles: new Map(snapshot.bundles),
294
+ bundlePatches: new Map(snapshot.bundlePatches),
295
+ bundleEvents: new Map(snapshot.bundleEvents),
296
+ channels: new Map(snapshot.channels),
297
+ apiKeys: new Map(snapshot.apiKeys),
298
+ releaseCatalogs: new Map(snapshot.releaseCatalogs),
299
+ releases: new Map(snapshot.releases)
300
+ });
301
+ const requireUnique = (rows, id, model) => {
302
+ if (rows.has(id)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
303
+ };
304
+ const distinctCount = (rows, fields) => {
305
+ if (fields === void 0) return rows.length;
306
+ return new Set(rows.map((row) => JSON.stringify(fields.map((field) => Reflect.get(row, field))))).size;
307
+ };
308
+ const createFirebaseDatabaseState = (snapshot) => ({
309
+ async create(input) {
310
+ switch (input.model) {
311
+ case "bundles":
312
+ requireUnique(snapshot.bundles, input.data.id, input.model);
313
+ snapshot.bundles.set(input.data.id, input.data);
314
+ return input.data;
315
+ case "bundle_patches":
316
+ requireUnique(snapshot.bundlePatches, input.data.id, input.model);
317
+ if (!snapshot.bundles.has(input.data.bundle_id)) throw new FirebaseDatabaseConstraintError("bundle_patches.bundle_id.foreign-key");
318
+ if (!snapshot.bundles.has(input.data.base_bundle_id)) throw new FirebaseDatabaseConstraintError("bundle_patches.base_bundle_id.foreign-key");
319
+ snapshot.bundlePatches.set(input.data.id, input.data);
320
+ return input.data;
321
+ case "bundle_events":
322
+ requireUnique(snapshot.bundleEvents, input.data.id, input.model);
323
+ snapshot.bundleEvents.set(input.data.id, input.data);
324
+ return input.data;
325
+ case "releases":
326
+ requireUnique(snapshot.releases, input.data.id, input.model);
327
+ if (!snapshot.channels.has(input.data.channel_id)) throw new FirebaseDatabaseConstraintError("releases.channel_id.foreign-key");
328
+ if (input.data.bundle_id !== null && !snapshot.bundles.has(input.data.bundle_id)) throw new FirebaseDatabaseConstraintError("releases.bundle_id.foreign-key");
329
+ snapshot.releases.set(input.data.id, input.data);
330
+ return input.data;
331
+ case "release_catalogs":
332
+ if (snapshot.releaseCatalogs.has(input.data.scope_key)) throw new FirebaseDatabaseConstraintError("release_catalogs.scope_key.unique");
333
+ snapshot.releaseCatalogs.set(input.data.scope_key, input.data);
334
+ return input.data;
335
+ case "channels": {
336
+ const existing = [...snapshot.channels.values()].find(({ name }) => name === input.data.name);
337
+ if (existing && input.onConflict === "ignore") return existing;
338
+ requireUnique(snapshot.channels, input.data.id, input.model);
339
+ if (existing) throw new FirebaseDatabaseConstraintError("channels.name.unique");
340
+ snapshot.channels.set(input.data.id, input.data);
341
+ return input.data;
342
+ }
343
+ case "api_keys": {
344
+ const existing = [...snapshot.apiKeys.values()].find(({ hash }) => hash === input.data.hash);
345
+ if (existing && input.onConflict === "ignore") return existing;
346
+ requireUnique(snapshot.apiKeys, input.data.id, input.model);
347
+ if (existing) throw new FirebaseDatabaseConstraintError("api_keys.hash.unique");
348
+ snapshot.apiKeys.set(input.data.id, input.data);
349
+ return input.data;
350
+ }
351
+ }
352
+ },
353
+ async update(input) {
354
+ if (input.model === "api_keys") {
355
+ const current = [...snapshot.apiKeys.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
356
+ if (!current) return null;
357
+ const updated = {
358
+ ...current,
359
+ ...input.update
360
+ };
361
+ snapshot.apiKeys.set(current.id, updated);
362
+ return updated;
363
+ }
364
+ if (input.model === "releases") {
365
+ const current = [...snapshot.releases.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
366
+ if (!current) return null;
367
+ const updated = {
368
+ ...current,
369
+ ...input.update
370
+ };
371
+ snapshot.releases.set(current.id, updated);
372
+ return updated;
373
+ }
374
+ if (input.model === "release_catalogs") {
375
+ const current = [...snapshot.releaseCatalogs.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
376
+ if (!current) return null;
377
+ const updated = {
378
+ ...current,
379
+ ...input.update
380
+ };
381
+ snapshot.releaseCatalogs.set(current.scope_key, updated);
382
+ return updated;
383
+ }
384
+ const current = [...snapshot.bundles.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
385
+ if (!current) return null;
386
+ const updated = {
387
+ ...current,
388
+ ...input.update
389
+ };
390
+ snapshot.bundles.set(current.id, updated);
391
+ return updated;
392
+ },
393
+ async delete(input) {
394
+ if (input.model === "channels") {
395
+ for (const row of snapshot.channels.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.channels.delete(row.id);
396
+ return;
397
+ }
398
+ if (input.model === "bundle_patches") {
399
+ for (const row of snapshot.bundlePatches.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.bundlePatches.delete(row.id);
400
+ return;
401
+ }
402
+ if (input.model === "releases") {
403
+ for (const row of snapshot.releases.values()) if (matchesFirebaseDatabaseWhere(row, input.where)) snapshot.releases.delete(row.id);
404
+ return;
405
+ }
406
+ const removedIds = new Set([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)).map(({ id }) => id));
407
+ for (const id of removedIds) snapshot.bundles.delete(id);
408
+ for (const patch of snapshot.bundlePatches.values()) if (removedIds.has(patch.bundle_id) || removedIds.has(patch.base_bundle_id)) snapshot.bundlePatches.delete(patch.id);
409
+ },
410
+ async count(input) {
411
+ switch (input.model) {
412
+ case "bundles": return distinctCount([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
413
+ case "bundle_patches": return distinctCount([...snapshot.bundlePatches.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
414
+ case "releases": return distinctCount([...snapshot.releases.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
415
+ }
416
+ },
417
+ async findOne(input) {
418
+ switch (input.model) {
419
+ case "bundles": return [...snapshot.bundles.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
420
+ case "api_keys": return [...snapshot.apiKeys.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
421
+ case "channels": return [...snapshot.channels.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
422
+ case "bundle_patches": return [...snapshot.bundlePatches.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
423
+ case "releases": return [...snapshot.releases.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
424
+ case "release_catalogs": return [...snapshot.releaseCatalogs.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
425
+ }
426
+ },
427
+ async findMany(input) {
428
+ switch (input.model) {
429
+ case "bundles": return queryFirebaseDatabaseRows([...snapshot.bundles.values()], input);
430
+ case "bundle_patches": return queryFirebaseDatabaseRows([...snapshot.bundlePatches.values()], input);
431
+ case "bundle_events": return queryFirebaseDatabaseRows([...snapshot.bundleEvents.values()], input);
432
+ case "channels": return queryFirebaseDatabaseRows([...snapshot.channels.values()], input);
433
+ case "api_keys": return queryFirebaseDatabaseRows([...snapshot.apiKeys.values()], input);
434
+ case "releases": return queryFirebaseDatabaseRows([...snapshot.releases.values()], input);
435
+ case "release_catalogs": return queryFirebaseDatabaseRows([...snapshot.releaseCatalogs.values()], input);
436
+ }
437
+ }
438
+ });
439
+ //#endregion
440
+ //#region src/firebaseInfrastructureNames.ts
441
+ const FIREBASE_V1_COLLECTION_NAMES = {
442
+ apiKeys: "hot_updater_v1_api_keys",
443
+ bundleEvents: "hot_updater_v1_bundle_events",
444
+ bundlePatches: "hot_updater_v1_bundle_patches",
445
+ bundles: "hot_updater_v1_bundles",
446
+ channels: "hot_updater_v1_channels",
447
+ releaseCatalogs: "hot_updater_v1_release_catalogs",
448
+ releases: "hot_updater_v1_releases",
449
+ settings: "hot_updater_v1_private_settings"
450
+ };
451
+ //#endregion
452
+ //#region src/firebaseDatabasePersistence.ts
453
+ var FirebaseDatabaseAdapterVersionError = class extends Error {
454
+ name = "FirebaseDatabaseAdapterVersionError";
455
+ constructor(version) {
456
+ super(`Unsupported Firebase database adapter version: ${String(version)}`);
457
+ this.version = version;
458
+ }
459
+ };
460
+ const createFirebaseDatabaseCollections = (db) => ({
461
+ bundles: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundles),
462
+ bundlePatches: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundlePatches),
463
+ bundleEvents: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundleEvents),
464
+ channels: db.collection(FIREBASE_V1_COLLECTION_NAMES.channels),
465
+ apiKeys: db.collection(FIREBASE_V1_COLLECTION_NAMES.apiKeys),
466
+ releaseCatalogs: db.collection(FIREBASE_V1_COLLECTION_NAMES.releaseCatalogs),
467
+ releases: db.collection(FIREBASE_V1_COLLECTION_NAMES.releases),
468
+ settings: db.collection(FIREBASE_V1_COLLECTION_NAMES.settings)
469
+ });
470
+ const firebaseChannelDocumentId = (name) => `name_${Buffer.from(name, "utf8").toString("base64url")}`;
471
+ const firebaseChannelIdDocumentId = (id) => `channel_id_${Buffer.from(id, "utf8").toString("base64url")}`;
472
+ const requireFirebaseDocumentKey = (model, documentId, row) => {
473
+ if (documentId !== ("id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
474
+ return row;
475
+ };
476
+ const documentMap = (model, documents) => {
477
+ const rows = /* @__PURE__ */ new Map();
478
+ for (const { row } of documents) {
479
+ const key = "id" in row ? row.id : row.scope_key;
480
+ if (rows.has(key)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
481
+ rows.set(key, row);
482
+ }
483
+ for (const { document, row } of documents) requireFirebaseDocumentKey(model, document.id, row);
484
+ return rows;
485
+ };
486
+ const bundleMap = (snapshot) => documentMap("bundles", snapshot.docs.map((document) => ({
487
+ document,
488
+ row: parseFirebaseBundleRow(document.data(), `bundles/${document.id}`)
489
+ })));
490
+ const patchMap = (snapshot) => documentMap("bundle_patches", snapshot.docs.map((document) => ({
491
+ document,
492
+ row: parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)
493
+ })));
494
+ const eventMap = (snapshot) => documentMap("bundle_events", snapshot.docs.map((document) => ({
495
+ document,
496
+ row: parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)
497
+ })));
498
+ const channelMap = (snapshot) => {
499
+ const rows = /* @__PURE__ */ new Map();
500
+ const names = /* @__PURE__ */ new Set();
501
+ for (const document of snapshot.docs) {
502
+ const row = parseFirebaseChannelRow(document.data(), `channels/${document.id}`);
503
+ if (document.id !== firebaseChannelDocumentId(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.document-key");
504
+ if (rows.has(row.id)) throw new FirebaseDatabaseConstraintError("channels.id.unique");
505
+ if (names.has(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.unique");
506
+ rows.set(row.id, row);
507
+ names.add(row.name);
508
+ }
509
+ return rows;
510
+ };
511
+ const loadFirebaseChannels = async (collections) => [...channelMap(await collections.channels.get()).values()];
512
+ const apiKeyMap = (snapshot) => documentMap("api_keys", snapshot.docs.map((document) => ({
513
+ document,
514
+ row: parseFirebaseApiKeyRow(document.data(), `api_keys/${document.id}`)
515
+ })));
516
+ const releaseMap = (snapshot) => documentMap("releases", snapshot.docs.map((document) => ({
517
+ document,
518
+ row: parseFirebaseReleaseRow(document.data(), `releases/${document.id}`)
519
+ })));
520
+ const releaseCatalogMap = (snapshot) => documentMap("release_catalogs", snapshot.docs.map((document) => ({
521
+ document,
522
+ row: parseFirebaseReleaseCatalogRow(document.data(), `release_catalogs/${document.id}`)
523
+ })));
524
+ const toSnapshot = (documents) => {
91
525
  return {
92
- channel: firestoreData.channel,
93
- enabled: Boolean(firestoreData.enabled),
94
- shouldForceUpdate: Boolean(firestoreData.should_force_update),
95
- fileHash: firestoreData.file_hash,
96
- gitCommitHash: firestoreData.git_commit_hash,
97
- id: firestoreData.id,
98
- message: firestoreData.message,
99
- platform: firestoreData.platform,
100
- targetAppVersion: firestoreData.target_app_version,
101
- storageUri: firestoreData.storage_uri,
102
- fingerprintHash: firestoreData.fingerprint_hash,
103
- metadata: (0, _hot_updater_core.stripBundleArtifactMetadata)(rawMetadata),
104
- manifestStorageUri: firestoreData.manifest_storage_uri ?? null,
105
- manifestFileHash: firestoreData.manifest_file_hash ?? null,
106
- assetBaseStorageUri: firestoreData.asset_base_storage_uri ?? null,
107
- patches,
108
- patchBaseBundleId: primaryPatch?.baseBundleId ?? firestoreData.patch_base_bundle_id ?? null,
109
- patchBaseFileHash: primaryPatch?.baseFileHash ?? firestoreData.patch_base_file_hash ?? null,
110
- patchFileHash: primaryPatch?.patchFileHash ?? firestoreData.patch_file_hash ?? null,
111
- patchStorageUri: primaryPatch?.patchStorageUri ?? firestoreData.patch_storage_uri ?? null,
112
- rolloutCohortCount: firestoreData.rollout_cohort_count ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
113
- targetCohorts: firestoreData.target_cohorts ?? null
526
+ bundles: bundleMap(documents[0]),
527
+ bundlePatches: patchMap(documents[1]),
528
+ bundleEvents: eventMap(documents[2]),
529
+ channels: channelMap(documents[3]),
530
+ apiKeys: apiKeyMap(documents[4]),
531
+ releases: releaseMap(documents[5]),
532
+ releaseCatalogs: releaseCatalogMap(documents[6])
114
533
  };
115
534
  };
116
- const firebaseDatabase = (0, _hot_updater_plugin_core.createDatabasePlugin)({
117
- name: "firebaseDatabase",
118
- factory: (config) => {
535
+ const loadFirebaseDatabaseSnapshot = async (collections) => {
536
+ const [bundles, patches, events, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
537
+ collections.bundles.get(),
538
+ collections.bundlePatches.get(),
539
+ collections.bundleEvents.get(),
540
+ collections.channels.get(),
541
+ collections.apiKeys.get(),
542
+ collections.releases.get(),
543
+ collections.releaseCatalogs.get()
544
+ ]);
545
+ return toSnapshot([
546
+ bundles,
547
+ patches,
548
+ events,
549
+ channels,
550
+ apiKeys,
551
+ releases,
552
+ releaseCatalogs
553
+ ]);
554
+ };
555
+ const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
556
+ const [bundles, patches, events, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
557
+ transaction.get(collections.bundles),
558
+ transaction.get(collections.bundlePatches),
559
+ transaction.get(collections.bundleEvents),
560
+ transaction.get(collections.channels),
561
+ transaction.get(collections.apiKeys),
562
+ transaction.get(collections.releases),
563
+ transaction.get(collections.releaseCatalogs)
564
+ ]);
565
+ return toSnapshot([
566
+ bundles,
567
+ patches,
568
+ events,
569
+ channels,
570
+ apiKeys,
571
+ releases,
572
+ releaseCatalogs
573
+ ]);
574
+ };
575
+ const persistCollection = ({ transaction, collection, before, after, documentId }) => {
576
+ for (const [id, row] of before) if (!after.has(id)) transaction.delete(collection.doc(documentId(row)));
577
+ for (const [id, row] of after) if (JSON.stringify(before.get(id)) !== JSON.stringify(row)) transaction.set(collection.doc(documentId(row)), row, { merge: true });
578
+ };
579
+ const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, after }) => {
580
+ persistCollection({
581
+ transaction,
582
+ collection: collections.bundles,
583
+ before: before.bundles,
584
+ after: after.bundles,
585
+ documentId: (row) => row.id
586
+ });
587
+ persistCollection({
588
+ transaction,
589
+ collection: collections.bundlePatches,
590
+ before: before.bundlePatches,
591
+ after: after.bundlePatches,
592
+ documentId: (row) => row.id
593
+ });
594
+ persistCollection({
595
+ transaction,
596
+ collection: collections.bundleEvents,
597
+ before: before.bundleEvents,
598
+ after: after.bundleEvents,
599
+ documentId: (row) => row.id
600
+ });
601
+ persistCollection({
602
+ transaction,
603
+ collection: collections.channels,
604
+ before: before.channels,
605
+ after: after.channels,
606
+ documentId: (row) => firebaseChannelDocumentId(row.name)
607
+ });
608
+ for (const [id] of before.channels) if (!after.channels.has(id)) transaction.delete(collections.settings.doc(firebaseChannelIdDocumentId(id)));
609
+ 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);
610
+ persistCollection({
611
+ transaction,
612
+ collection: collections.apiKeys,
613
+ before: before.apiKeys,
614
+ after: after.apiKeys,
615
+ documentId: (row) => row.id
616
+ });
617
+ persistCollection({
618
+ transaction,
619
+ collection: collections.releases,
620
+ before: before.releases,
621
+ after: after.releases,
622
+ documentId: (row) => row.id
623
+ });
624
+ persistCollection({
625
+ transaction,
626
+ collection: collections.releaseCatalogs,
627
+ before: before.releaseCatalogs,
628
+ after: after.releaseCatalogs,
629
+ documentId: (row) => row.scope_key
630
+ });
631
+ };
632
+ const migrateFirebaseDatabase = async (_db, collections) => {
633
+ const versionDocument = collections.settings.doc("database_adapter_version");
634
+ const version = await versionDocument.get();
635
+ const adapterVersion = version.data()?.version;
636
+ if (adapterVersion === 4) return;
637
+ if (version.exists) throw new FirebaseDatabaseAdapterVersionError(adapterVersion);
638
+ if ((await Promise.all([
639
+ collections.bundles.limit(1).get(),
640
+ collections.bundlePatches.limit(1).get(),
641
+ collections.channels.limit(1).get(),
642
+ collections.releases.limit(1).get(),
643
+ collections.releaseCatalogs.limit(1).get()
644
+ ])).some((snapshot) => !snapshot.empty)) throw new FirebaseDatabaseAdapterVersionError("v0");
645
+ try {
646
+ await versionDocument.create({ version: 4 });
647
+ } catch (error) {
648
+ if ((await versionDocument.get()).data()?.version !== 4) throw error;
649
+ }
650
+ };
651
+ //#endregion
652
+ //#region src/firebaseDatabase.ts
653
+ const exactId = (input) => {
654
+ if (input.where?.length !== 1) return void 0;
655
+ const [condition] = input.where;
656
+ return condition.field === "id" && (condition.operator === void 0 || condition.operator === "eq") && typeof condition.value === "string" ? condition.value : void 0;
657
+ };
658
+ const firebaseDatabase = (config) => {
659
+ const adapter = (0, _hot_updater_plugin_core_internal.createDatabasePluginAdapter)("firebaseDatabase", (() => {
119
660
  const db = (0, firebase_admin_firestore.getFirestore)((0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config));
120
- const bundlesCollection = db.collection("bundles");
121
- const targetAppVersionsCollection = db.collection("target_app_versions");
661
+ const collections = createFirebaseDatabaseCollections(db);
662
+ let migration;
663
+ const ensureMigrated = () => {
664
+ migration ??= migrateFirebaseDatabase(db, collections).catch((error) => {
665
+ migration = void 0;
666
+ throw error;
667
+ });
668
+ return migration;
669
+ };
670
+ const mutate = async (operation) => {
671
+ await ensureMigrated();
672
+ return db.runTransaction(async (transaction) => {
673
+ const before = await loadFirebaseTransactionSnapshot(transaction, collections);
674
+ const after = cloneFirebaseDatabaseSnapshot(before);
675
+ const result = await operation(createFirebaseDatabaseState(after));
676
+ persistFirebaseDatabaseSnapshot({
677
+ transaction,
678
+ collections,
679
+ before,
680
+ after
681
+ });
682
+ return result;
683
+ });
684
+ };
685
+ const read = async (operation) => {
686
+ await ensureMigrated();
687
+ return operation(createFirebaseDatabaseState(await loadFirebaseDatabaseSnapshot(collections)));
688
+ };
122
689
  return {
123
- async getUpdateInfo(args, context) {
124
- const channel = args.channel ?? "production";
125
- const minBundleId = args.minBundleId ?? _hot_updater_core.NIL_UUID;
126
- if (args._updateStrategy === "appVersion") {
127
- const querySnapshot = await targetAppVersionsCollection.where("platform", "==", args.platform).where("channel", "==", channel).select("target_app_version").get();
128
- const compatibleAppVersions = (0, _hot_updater_plugin_core.filterCompatibleAppVersions)(Array.from(new Set(querySnapshot.docs.map((doc) => doc.data().target_app_version).filter((version) => Boolean(version)))), args.appVersion);
129
- const bundles = (compatibleAppVersions.length > 0 ? await Promise.all(chunkValues(compatibleAppVersions, 10).map((versions) => bundlesCollection.where("platform", "==", args.platform).where("channel", "==", channel).where("enabled", "==", true).where("id", ">=", minBundleId).where("target_app_version", "in", versions).get())) : []).flatMap((snapshot) => snapshot.docs.map((doc) => convertToBundle(doc.data())));
130
- return (0, _hot_updater_plugin_core.resolveUpdateInfoFromBundles)({
131
- args: {
132
- ...args,
133
- channel,
134
- minBundleId
135
- },
136
- bundles,
137
- context
138
- });
690
+ create: (input) => mutate((database) => database.create(input)),
691
+ update: (input) => mutate((database) => database.update(input)),
692
+ delete: (input) => mutate((database) => database.delete(input)),
693
+ count: (input) => read((database) => database.count(input)),
694
+ findOne: async (input) => {
695
+ const id = exactId(input);
696
+ if (id === void 0) return read((database) => database.findOne(input));
697
+ await ensureMigrated();
698
+ switch (input.model) {
699
+ case "bundles": {
700
+ const document = await collections.bundles.doc(id).get();
701
+ return document.exists ? requireFirebaseDocumentKey("bundles", document.id, parseFirebaseBundleRow(document.data(), `bundles/${document.id}`)) : null;
702
+ }
703
+ case "bundle_patches": {
704
+ const document = await collections.bundlePatches.doc(id).get();
705
+ return document.exists ? requireFirebaseDocumentKey("bundle_patches", document.id, parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)) : null;
706
+ }
707
+ case "api_keys": {
708
+ const document = await collections.apiKeys.doc(id).get();
709
+ return document.exists ? requireFirebaseDocumentKey("api_keys", document.id, parseFirebaseApiKeyRow(document.data(), `api_keys/${document.id}`)) : null;
710
+ }
711
+ default: return read((database) => database.findOne(input));
139
712
  }
140
- const bundles = (await bundlesCollection.where("platform", "==", args.platform).where("channel", "==", channel).where("enabled", "==", true).where("id", ">=", minBundleId).where("fingerprint_hash", "==", args.fingerprintHash).get()).docs.map((doc) => convertToBundle(doc.data()));
141
- return (0, _hot_updater_plugin_core.resolveUpdateInfoFromBundles)({
142
- args: {
143
- ...args,
144
- channel,
145
- minBundleId
146
- },
147
- bundles,
148
- context
149
- });
150
713
  },
151
- async getBundleById(bundleId) {
152
- const bundleSnap = await bundlesCollection.doc(bundleId).get();
153
- if (!bundleSnap.exists) return null;
154
- return convertToBundle(bundleSnap.data());
714
+ findMany: async (input) => {
715
+ if (input.model !== "channels") return read((database) => database.findMany(input));
716
+ await ensureMigrated();
717
+ return queryFirebaseDatabaseRows(await loadFirebaseChannels(collections), input);
155
718
  },
156
- async getBundles(options) {
157
- const { where, limit, orderBy } = options;
158
- const offset = ("offset" in options ? options.offset : void 0) ?? 0;
159
- let query = applyFirestoreQueryableFilters(bundlesCollection, where);
160
- query = query.orderBy("id", orderBy?.direction === "asc" ? "asc" : "desc");
161
- if (requiresInMemoryFiltering(where)) {
162
- const filteredBundles = sortBundles((await query.get()).docs.map((doc) => convertToBundle(doc.data())).filter((bundle) => bundleMatchesQueryWhere(bundle, where)), orderBy);
163
- const total = filteredBundles.length;
719
+ insertChannel: async (input) => {
720
+ await ensureMigrated();
721
+ return db.runTransaction(async (transaction) => {
722
+ const reference = collections.channels.doc(firebaseChannelDocumentId(input.row.name));
723
+ const idReference = collections.settings.doc(firebaseChannelIdDocumentId(input.row.id));
724
+ const [document, idDocument] = await transaction.getAll(reference, idReference);
725
+ if (idDocument.exists) {
726
+ const row = parseFirebaseChannelRow(idDocument.data(), `${FIREBASE_V1_COLLECTION_NAMES.settings}/${idDocument.id}`);
727
+ if (row.id !== input.row.id || row.name !== input.row.name) throw new FirebaseDatabaseConstraintError("channels.id.registry");
728
+ }
729
+ if (idDocument.exists && !document.exists) throw new FirebaseDatabaseConstraintError("channels.id.unique");
730
+ if (document.exists) {
731
+ const row = parseFirebaseChannelRow(document.data(), `channels/${document.id}`);
732
+ if (document.id !== firebaseChannelDocumentId(row.name)) throw new FirebaseDatabaseConstraintError("channels.name.document-key");
733
+ return {
734
+ row,
735
+ inserted: false
736
+ };
737
+ }
738
+ transaction.create(reference, input.row);
739
+ transaction.create(idReference, input.row);
164
740
  return {
165
- data: filteredBundles.slice(offset, offset + limit),
166
- pagination: (0, _hot_updater_plugin_core.calculatePagination)(total, {
167
- limit,
168
- offset
169
- })
741
+ row: input.row,
742
+ inserted: true
170
743
  };
171
- }
172
- const total = (await query.get()).size;
173
- if (offset > 0) query = query.offset(offset);
174
- if (limit) query = query.limit(limit);
175
- return {
176
- data: sortBundles((await query.get()).docs.map((doc) => convertToBundle(doc.data())), orderBy),
177
- pagination: (0, _hot_updater_plugin_core.calculatePagination)(total, {
178
- limit,
179
- offset
180
- })
181
- };
182
- },
183
- async getChannels() {
184
- const querySnapshot = await db.collection("channels").get();
185
- if (querySnapshot.empty) return [];
186
- const channels = /* @__PURE__ */ new Set();
187
- for (const doc of querySnapshot.docs) {
188
- const data = doc.data();
189
- if (data.name) channels.add(data.name);
190
- }
191
- return Array.from(channels);
744
+ });
192
745
  },
193
- async commitBundle({ changedSets }) {
194
- if (changedSets.length === 0) return;
195
- let isTargetAppVersionChanged = false;
196
- await db.runTransaction(async (transaction) => {
197
- const bundlesSnapshot = await transaction.get(bundlesCollection);
198
- const targetVersionsSnapshot = await transaction.get(db.collection("target_app_versions"));
199
- const channelsSnapshot = await transaction.get(db.collection("channels"));
200
- const bundlesMap = {};
201
- for (const doc of bundlesSnapshot.docs) bundlesMap[doc.id] = doc.data();
202
- for (const { operation, data } of changedSets) {
203
- if (data.targetAppVersion) isTargetAppVersionChanged = true;
204
- if (operation === "insert" || operation === "update") {
205
- bundlesMap[data.id] = {
206
- id: data.id,
207
- channel: data.channel,
208
- enabled: data.enabled,
209
- should_force_update: data.shouldForceUpdate,
210
- file_hash: data.fileHash,
211
- git_commit_hash: data.gitCommitHash || null,
212
- message: data.message || null,
213
- platform: data.platform,
214
- target_app_version: data.targetAppVersion,
215
- storage_uri: data.storageUri,
216
- fingerprint_hash: data.fingerprintHash,
217
- metadata: (0, _hot_updater_core.stripBundleArtifactMetadata)(data.metadata) ?? {},
218
- manifest_storage_uri: (0, _hot_updater_core.getManifestStorageUri)(data),
219
- manifest_file_hash: (0, _hot_updater_core.getManifestFileHash)(data),
220
- asset_base_storage_uri: (0, _hot_updater_core.getAssetBaseStorageUri)(data),
221
- patches: data.patches ?? null,
222
- patch_base_bundle_id: (0, _hot_updater_core.getPatchBaseBundleId)(data),
223
- patch_base_file_hash: (0, _hot_updater_core.getPatchBaseFileHash)(data),
224
- patch_file_hash: (0, _hot_updater_core.getPatchFileHash)(data),
225
- patch_storage_uri: (0, _hot_updater_core.getPatchStorageUri)(data),
226
- rollout_cohort_count: data.rolloutCohortCount ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
227
- target_cohorts: data.targetCohorts ?? null
228
- };
229
- const channelRef = db.collection("channels").doc(data.channel);
230
- transaction.set(channelRef, { name: data.channel }, { merge: true });
231
- } else if (operation === "delete") {
232
- if (!bundlesMap[data.id]) throw new Error(`Bundle with id ${data.id} not found`);
233
- delete bundlesMap[data.id];
234
- isTargetAppVersionChanged = true;
235
- }
236
- }
237
- const requiredTargetVersionKeys = /* @__PURE__ */ new Set();
238
- const requiredChannels = /* @__PURE__ */ new Set();
239
- for (const bundle of Object.values(bundlesMap)) {
240
- if (bundle.target_app_version) {
241
- const key = `${bundle.platform}_${bundle.channel}_${bundle.target_app_version}`;
242
- requiredTargetVersionKeys.add(key);
243
- }
244
- requiredChannels.add(bundle.channel);
245
- }
246
- for (const { operation, data } of changedSets) {
247
- const bundleRef = bundlesCollection.doc(data.id);
248
- if (operation === "insert" || operation === "update") {
249
- transaction.set(bundleRef, {
250
- id: data.id,
251
- channel: data.channel,
252
- enabled: data.enabled,
253
- should_force_update: data.shouldForceUpdate,
254
- file_hash: data.fileHash,
255
- git_commit_hash: data.gitCommitHash || null,
256
- message: data.message || null,
257
- platform: data.platform,
258
- target_app_version: data.targetAppVersion || null,
259
- storage_uri: data.storageUri,
260
- fingerprint_hash: data.fingerprintHash,
261
- metadata: (0, _hot_updater_core.stripBundleArtifactMetadata)(data.metadata) ?? {},
262
- manifest_storage_uri: (0, _hot_updater_core.getManifestStorageUri)(data),
263
- manifest_file_hash: (0, _hot_updater_core.getManifestFileHash)(data),
264
- asset_base_storage_uri: (0, _hot_updater_core.getAssetBaseStorageUri)(data),
265
- patches: data.patches ?? null,
266
- patch_base_bundle_id: (0, _hot_updater_core.getPatchBaseBundleId)(data),
267
- patch_base_file_hash: (0, _hot_updater_core.getPatchBaseFileHash)(data),
268
- patch_file_hash: (0, _hot_updater_core.getPatchFileHash)(data),
269
- patch_storage_uri: (0, _hot_updater_core.getPatchStorageUri)(data),
270
- rollout_cohort_count: data.rolloutCohortCount ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
271
- target_cohorts: data.targetCohorts ?? null
272
- }, { merge: true });
273
- if (data.targetAppVersion) {
274
- const versionDocId = `${data.platform}_${data.channel}_${data.targetAppVersion}`;
275
- const targetAppVersionsRef = db.collection("target_app_versions").doc(versionDocId);
276
- transaction.set(targetAppVersionsRef, {
277
- channel: data.channel,
278
- platform: data.platform,
279
- target_app_version: data.targetAppVersion
280
- }, { merge: true });
281
- }
282
- } else if (operation === "delete") transaction.delete(bundleRef);
283
- }
284
- if (isTargetAppVersionChanged) {
285
- for (const targetDoc of targetVersionsSnapshot.docs) if (!requiredTargetVersionKeys.has(targetDoc.id)) transaction.delete(targetDoc.ref);
286
- }
287
- for (const channelDoc of channelsSnapshot.docs) if (!requiredChannels.has(channelDoc.id)) transaction.delete(channelDoc.ref);
746
+ deleteChannel: async ({ id }) => {
747
+ await ensureMigrated();
748
+ return db.runTransaction(async (transaction) => {
749
+ const idReference = collections.settings.doc(firebaseChannelIdDocumentId(id));
750
+ const idDocument = await transaction.get(idReference);
751
+ if (!idDocument.exists) return {
752
+ deleted: false,
753
+ reason: "not_found"
754
+ };
755
+ const row = parseFirebaseChannelRow(idDocument.data(), `${FIREBASE_V1_COLLECTION_NAMES.settings}/${idDocument.id}`);
756
+ const reference = collections.channels.doc(firebaseChannelDocumentId(row.name));
757
+ if (!(await transaction.get(reference)).exists || row.id !== id) throw new FirebaseDatabaseConstraintError("channels.id.registry");
758
+ if (!(await transaction.get(collections.releases.where("channel_id", "==", id).limit(1))).empty) return {
759
+ deleted: false,
760
+ reason: "not_empty"
761
+ };
762
+ transaction.delete(reference);
763
+ transaction.delete(idReference);
764
+ return { deleted: true };
288
765
  });
289
- }
766
+ },
767
+ transaction: (callback) => mutate(callback)
290
768
  };
291
- }
292
- });
769
+ })());
770
+ return (0, _hot_updater_plugin_core.createDatabasePlugin)({
771
+ name: "firebaseDatabase",
772
+ models: adapter.models,
773
+ commit: adapter.commit
774
+ });
775
+ };
293
776
  //#endregion
294
777
  //#region src/firebaseStorage.ts
295
- const firebaseStorage = (0, _hot_updater_plugin_core.createUniversalStoragePlugin)({
296
- name: "firebaseStorage",
297
- supportedProtocol: "gs",
298
- factory: (config) => {
299
- const bucket = (0, firebase_admin_storage.getStorage)((0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config)).bucket(config.storageBucket);
300
- const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
301
- return {
302
- node: {
303
- async delete(storageUri) {
304
- const { bucket: bucketName, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
305
- if (bucketName !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${bucketName}".`);
306
- try {
307
- const [files] = await bucket.getFiles({ prefix: key });
308
- await Promise.all(files.map((file) => file.delete()));
309
- } catch (e) {
310
- console.error("Error listing or deleting files:", e);
311
- throw new Error("Bundle Not Found");
312
- }
313
- },
314
- async upload(key, filePath) {
315
- try {
316
- const fileContent = await fs_promises.default.readFile(filePath);
317
- const contentType = (0, _hot_updater_plugin_core.getContentType)(filePath);
318
- const storageKey = getStorageKey(key, path.default.basename(filePath));
319
- await bucket.file(storageKey).save(fileContent, { metadata: {
320
- contentType,
321
- cacheControl: "public, max-age=31536000, immutable"
322
- } });
323
- return { storageUri: `gs://${config.storageBucket}/${storageKey}` };
324
- } catch (error) {
325
- console.error("Error uploading bundle:", error);
326
- if (error instanceof Error) throw new Error(`Failed to upload bundle: ${error.message}`);
327
- throw error;
328
- }
329
- },
330
- async exists(storageUri) {
331
- const { bucket: bucketName, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
332
- if (bucketName !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${bucketName}".`);
333
- const [exists] = await bucket.file(key).exists();
334
- return exists;
335
- },
336
- async downloadFile(storageUri, filePath) {
337
- const { bucket: bucketName, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
338
- if (bucketName !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${bucketName}".`);
339
- await fs_promises.default.mkdir(path.default.dirname(filePath), { recursive: true });
340
- await bucket.file(key).download({ destination: filePath });
341
- }
342
- },
343
- runtime: {
344
- async readText(storageUri) {
345
- const { bucket: bucketName, key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
346
- if (bucketName !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${bucketName}".`);
347
- try {
348
- const [contents] = await bucket.file(key).download();
349
- return contents.toString("utf8");
350
- } catch (error) {
351
- if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return null;
352
- throw error;
353
- }
354
- },
355
- async getDownloadUrl(storageUri) {
356
- const { key } = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
357
- if (!key) throw new Error("Invalid Firebase storage URI: missing key");
358
- const [signedUrl] = await bucket.file(key).getSignedUrl({
359
- action: "read",
360
- expires: Date.now() + 3600 * 1e3
361
- });
362
- if (!signedUrl) throw new Error("Failed to generate download URL");
363
- return { fileUrl: signedUrl };
364
- }
778
+ const firebaseStorage = (config) => {
779
+ const bucket = (0, firebase_admin_storage.getStorage)((0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config)).bucket(config.storageBucket);
780
+ const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
781
+ const parseAndValidate = (storageUri) => {
782
+ const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
783
+ if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
784
+ return parsed;
785
+ };
786
+ return (0, _hot_updater_plugin_core.createStoragePlugin)({
787
+ name: "firebaseStorage",
788
+ protocol: "gs",
789
+ async put({ key, body, contentType }) {
790
+ const storageKey = getStorageKey(key);
791
+ const bytes = new Uint8Array(await new Response(body).arrayBuffer());
792
+ await bucket.file(storageKey).save(bytes, { metadata: {
793
+ contentType,
794
+ cacheControl: "public, max-age=31536000, immutable"
795
+ } });
796
+ return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
797
+ bucket: config.storageBucket,
798
+ key: storageKey,
799
+ protocol: "gs"
800
+ }) };
801
+ },
802
+ async get({ storageUri }) {
803
+ const { key } = parseAndValidate(storageUri);
804
+ try {
805
+ const file = bucket.file(key);
806
+ const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
807
+ const headers = new Headers();
808
+ if (metadata.contentType) headers.set("content-type", metadata.contentType);
809
+ headers.set("content-length", String(metadata.size ?? body.byteLength));
810
+ return { response: new Response(body, { headers }) };
811
+ } catch (error) {
812
+ if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
813
+ throw error;
365
814
  }
366
- };
367
- }
368
- });
815
+ },
816
+ async getDownloadUrl({ storageUri }) {
817
+ const { key } = parseAndValidate(storageUri);
818
+ if (config.cdnUrl) {
819
+ const storageUrl = new URL(storageUri);
820
+ const downloadUrl = new URL(config.cdnUrl);
821
+ downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
822
+ downloadUrl.search = "";
823
+ downloadUrl.hash = "";
824
+ return { url: downloadUrl.toString() };
825
+ }
826
+ const [url] = await bucket.file(key).getSignedUrl({
827
+ action: "read",
828
+ expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
829
+ });
830
+ if (!url) throw new Error("Failed to generate Firebase download URL");
831
+ return { url };
832
+ },
833
+ async exists({ storageUri }) {
834
+ const { key } = parseAndValidate(storageUri);
835
+ const [exists] = await bucket.file(key).exists();
836
+ return { exists };
837
+ },
838
+ async delete({ storageUri }) {
839
+ const { key } = parseAndValidate(storageUri);
840
+ await bucket.file(key).delete({ ignoreNotFound: true });
841
+ return { deleted: true };
842
+ }
843
+ });
844
+ };
369
845
  //#endregion
370
846
  exports.firebaseDatabase = firebaseDatabase;
371
847
  exports.firebaseStorage = firebaseStorage;