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